How to create constraints that use expr() dynamically? - typo3-9.x

A user can enter a letter range like "A-D", by which a query must find all records that start with any of those letters. What I eventually need is a constraints block that looks like this:
$constraints = [
$query->expr()->eq(
'composition.sys_language_uid',
$query->createNamedParameter($language, \PDO::PARAM_INT)
),
$query->expr()->orX(
$query->expr()->like(
'composition.title',
$query->createNamedParameter('A%')
),
$query->expr()->like(
'composition.title',
$query->createNamedParameter('B%')
),
$query->expr()->like(
'composition.title',
$query->createNamedParameter('C%')
),
$query->expr()->like(
'composition.title',
$query->createNamedParameter('D%')
)
)
];
which is a structure, that works well, when I use it as a test. So I know I need to strive for a solution like this.
But, of course, since the letters given are not fixed, but variable, the block within ->orX() needs to be calculated programmatically. This is, where my problem lies.
I tried this:
// A custom helper function that splits a letter range string like "A-D"
// and returns an array like ['A','B','C','D']
$compareLetters = Helper::returnItemOrListAsArray($letter, true);
}
// Create the query
$query = $allDbConnections['composition']->createQueryBuilder();
// Collect constraints
$addConstraints = [];
// Compare first letter against given compareLetters
foreach($compareLetters as $l) {
$addConstraints[] = $query->expr()->like(
'composition.title',
$query->createNamedParameter($l . '%')
);
}
Trying to insert the resulting array like this:
$constraints = [
$query->expr()->eq(
'composition.sys_language_uid',
$query->createNamedParameter($language, \PDO::PARAM_INT)
),
$query->expr()->orX(
implode(',', $addConstraints)
)
]
throws an exception:
Operand should contain 1 column(s)
Currently I have no idea, how to do this differently nor how to interpret the exception. Any hint would be most welcome!

Not sure whether it's a good approach, but I would try out range() function to generate the query, something like this:
<?php
$userInput = 'A-D';
list($start, $end) = explode('-', $userInput);
$selection = [];
foreach (range($start, $end) as $letter) {
$selection[] = $query->expr()->like(
'composition.title',
$query->createNamedParameter($letter . '%')
);
}
$constraints = [
$query->expr()->eq(
'composition.sys_language_uid',
$query->createNamedParameter($language, \PDO::PARAM_INT)
),
$query->expr()->orX(...$selection)
];
So more or less what you already did. Just you are not using the spread operator in your example when calling orX()

Related

Can Laravel automatically switch between column = ? and column IS NULL depending on value?

When building a complex SQL query for Laravel, using ? as placeholders for parameters is great. However when the value is null, the SQL syntax needs to be changed from = ? to IS NULL. Plus, since the number of parameters is one less, I need to pass a different array.
To get it to work, I have written it like this, but there must be a better way:
if ($cohortId === null) {
// sql should be: column IS NULL
$sqlCohortString = "IS NULL";
$params = [
Carbon::today()->subDays(90),
// no cohort id here
];
} else {
// sql should be: column = ?
$sqlCohortString = "= ?";
$params = [
Carbon::today()->subDays(90),
$cohortId
];
}
$query = "SELECT items.`name`,
snapshots.`value`,
snapshots.`taken_at`,
FROM snapshots
INNER JOIN (
SELECT MAX(id) AS id, item_id
FROM snapshots
WHERE `taken_at` > ?
AND snapshots.`cohort_id` $sqlCohortString
GROUP BY item_id
) latest
ON latest.`id` = snapshots.`id`
INNER JOIN items
ON items.`id` = snapshots.`item_id`
ORDER by media_items.`slug` ASC
";
$chartData = DB::select($query, $params);
My question is: does Laravel have a way to detect null values and replace ? more intelligently?
PS: The SQL is for a chart, so I need the single highest snapshot value for each item.
You can use ->when to create a conditional where clause:
$data = DB::table('table')
->when($cohortId === null, function ($query) {
return $query->whereNull('cohort_id');
}, function ($query) use ($cohortId) {
// the "use" keyword provides access to "outer" variables
return $query->where('cohort_id', '=', $cohortId);
})
->where('taken_at', '>', $someDate)
->toSql();

How to return a JSON array from sql table with PhalconPHP

I have several tables that have JSON arrays stored within fields.
Using PHP PDO I am able to retrieve this data without issue using:
$query1 = $database->prepare("SELECT * FROM module_settings
WHERE project_token = ? AND module_id = ? ORDER BY id DESC LIMIT 1");
$query1->execute(array($page["project_token"], 2));
$idx = $query1->fetch(PDO::FETCH_ASSOC);
$idx["settings"] = json_decode($idx["settings"]);
This returns a string like:
{"mid":"","module_id":"1","force_reg_enable":"1","force_reg_page_delay":"2"}
Attempting to gather the same data via PhalconPHP
$result = Modulesettings::findFirst( array(
'conditions' => 'project_token = "' . $token . '"' ,
'columns' => 'settings'
) );
var_dump($result);
Provides a result of
object(Phalcon\Mvc\Model\Row)#61 (1) { ["settings"]=> string(167) "{"text":"<\/a>
<\/a>
","class":""}" }
What do I need to do different in Phalcon to return the string as it is stored in the table?
Thank you.
You have 2 approach
First :
Get the settings with this structure :
$settings = $result->settings;
var_dump($settings);
Second :
First get array from resultset, then using the array element :
$res = $result->toArray();
var_dump($res['settings']);
Try it.
You can decode json right in your Modulesettings model declaration:
// handling result
function afterFetch() {
$this->settings = json_decode($this->settings);
}
// saving. Can use beforeCreate+beforeSave+beforeUpdate
// or write a Json filter.
function beforeValidation() {
$this->settings = json_encode($this->settings);
}

Datatables server side processing and column alias

In Datatables using server side processing, is it possible to use column alias when specifying the columns?
At the moment this works fine with:
$aColumns = array( 'datetime','username', 'user_ip', 'company', 'action' );
but I would like to change the format of the date using date format in MySQL so, in effect, I want to use:
$aColumns = array( 'DATE_FORMAT(datetime, "%d/%m/%Y - %H:%i:%s") as newdate';'username'; 'user_ip';'company'; 'action' );
The problem is that the alias has a comma and the aColumns array is comma seperated so it breaks when later, for example, it comes to:
$sQuery = "
SELECT SQL_CALC_FOUND_ROWS ".str_replace(" , ", " ", implode(", ", $aColumns))."
FROM $sTable
$sWhere
$sOrder
$sLimit
";
Is there a way I can use the alias rather than the original value? Even simply changing the select statement does not work as aColumns is used throughout the script hence it needs that value to be set correctly
Thanks
Yes. I actually just struggled with this issue myself. Because the JSON output is determined through counting the amount of columns in the array, and because of the imploding array, you have to add your column alias to $sQuery instead of the $aColumns array. So you'll actually have one less column in your $aColumns array than you'll need. For example, in mine, I needed an alias called total created from multiplying price and qty. So I put all my unaliased columns in the $aColumns array, like this:
$aColumns = array( 'purchaseID', 'dateOfOrder', 'productID', 'price', 'QTY');
But then, in the $sQuery string that concatenates all the things necessary to create the proper query string, I added my column alias between the implode and FROM. Don't forget to put a comma after the implode though, because it doesn't add it for you. The original $sQuery string looks like this:
$sQuery = "
SELECT SQL_CALC_FOUND_ROWS ".str_replace(" , ", " ", implode(", ", $aColumns))."
FROM `$sTable`
$sWhere
$dateSql
$sOrder
$sLimit
";
But mine, with the column alias added, looks like this:
$sQuery = "
SELECT SQL_CALC_FOUND_ROWS ".str_replace(" , ", " ", implode(", ", $aColumns))."
, `price` * `QTY` AS `total` FROM `$sTable`
$sWhere
$dateSql
$sOrder
$sLimit
";
Finally, the last thing you have to do is alter the actual JSON output to make sure your extra column is accounted for in the FOR loop at the end right before the json_encode, because it inserts items into the $row array, which is what becomes 'aaData' (the returned row data), based on how many columns you've specified in the $aColumns array, and because you left out any you've aliased, the count will be wrong, and you will get an error that looks something like 'requested unknown parameter from data source row'. The original FOR loop looks like this:
while ( $aRow = mysql_fetch_array( $rResult ) )
{
$row = array();
for ( $i=0 ; $i<count($aColumns) ; $i++ )
{
if ( $aColumns[$i] == "version" )
{
/* Special output formatting for 'version' column */
$row[] = ($aRow[ $aColumns[$i] ]=="0") ? '-' : $aRow[ $aColumns[$i] ];
}
else if ( $aColumns[$i] != ' ' )
{
/* General output */
$row[] = $aRow[ $aColumns[$i] ];
}
}
$output['aaData'][] = $row;
}
Like I said, this FOR loop works based off the COUNT of the $aColumns array, and since I've added an alias, it's going to cut my results short. It's not going to return the last element in the array containing the returned columns, so I'm going to alter the code to look like this:
for ( $i=0 ; $i<count($aColumns) + 1; $i++ )
{
if ($i < count($aColumns)){
if ( $aColumns[$i] == "version" )
{
/* Special output formatting for 'version' column */
$row[] = ($aRow[ $aColumns[$i] ]=="0") ? '-' : $aRow[ $aColumns[$i] ];
}
else if ( $aColumns[$i] != ' ' )
{
/* General output */
$row[] = $aRow[ $aColumns[$i] ];
}
}
else {
$row[] = $aRow['total'];
}
}
$output['aaData'][] = $row;
}
All I changed was the counter condition from $i<count($aColumns) to $i<count($aColumns) + 1, because my alias makes the column count one higher than what's in the array. And I've added a wrapping if-else that just says that if the counter, $i, is higher than the number of columns I've specified in the $aColumns array, then we've added all the columns in the array to the output data, so because I only added one extra alias column, then that means I can go ahead and just add that to the $row array, which contains all the output data from the returned rows.
You can actually add in as many aliased columns as you need, you just need to scale the code accordingly. Hope this helps!

Preserve Order of IN in ORM Order

I'm trying to do a query where I preserve the order of the ids in a IN statement. I can't seem to do it with either the Model Manage Query Builder or the standard ORM 'order' array parameter. Am I missing something? I keep getting:
UNEXPECTED TOKEN IDENTIFIER(, NEAR TO 'id`enter code here`,17743,16688,16650
Here's my model manager:
$query = $this->modelsManager->createQuery('SELECT * FROM Projects WHERE id IN ('.implode(',', array_keys($finalIterations)).')
ORDER BY FIELD(id,'.implode(',', array_keys($finalIterations)).'');
It's pretty obvious PhQL doesn't like the FIELD key word. Is there a way for me to do what I'm trying to do with PhQL? It seems I will not be able to do what I need to.
Unfortunately as previously said, this is missing a feature in Phalcon.
Have a look at this function, I've put it into my ModelBase abstract class which is parent class of all my models. It uses PhQL variable binding, so it's safe for handling direct user input.
You could have reimplemented custom \Phalcon\Mvc\Model\Criteria but this solution seems to be easier to work with, at least for me.
ModelBase abstract
public function appendCustomOrder( \Phalcon\Mvc\Model\CriteriaInterface &$criteria, $orderField, array &$orderValues = [] ) {
if(!empty($orderValues)) {
$queryKeys = $bindParams = [];
foreach($orderValues as $key => $id) {
$queryKey = 'pho'.$key;
$queryKeys[] = ':'.$queryKey.':';
$bindParams[$queryKey] = $id;
}
// TODO: add support for multiple orderBy fields
$criteria->orderBy('FIELD('.$orderField.','.implode(',',$queryKeys).')');
// there's no 'addBind' function, need to merge old parameters with new ones
$criteria->bind( array_merge( (array) #$criteria->getParams()['bind'], $bindParams ) );
}
}
Controller usage
$projectIDs = [17743, 16688, 16650];
$projectsModel = new Projects();
$criteria = $projectsModel->query->inWhere( 'id', $projectIDs );
$projectsModel->appendCustomOrder( $criteria, 'id', $projectIDs );
$projectsData = $criteria->execute();
This will generate valid PhQL syntax similar to this one:
SELECT `projects`.`id` AS `id`, `projects`.`title` AS `title`
FROM `projects`
WHERE `projects`.`id` IN (:phi0, :phi1, :phi2)
ORDER BY FIELD(`projects`.`id`, :pho0, :pho1, :pho2)

Dynamic PDO AND and OR

I have a problem with Yii's CDBCrtieria builder. I am trying to make a rather complex query while using the escaping and safe functions provided by PDO.
Here is the query I am basically trying to build:
SELECT * FROM tbl_audit_log
WHERE (model_id = 1 AND model = "Title") OR
(model_id = 1 AND model = "Product") //etc
This is being built dynamically in PHP like:
$model_ids = array(array($model->id, 'Title'));
foreach($model->products as $id => $product){
$model_ids[][] = $product->id;
$model_ids[][] = "Product";
}
So I don't know the values of the WHERE before I build the query. I must find an easy way to build:
WHERE (model_id = 1 AND model = "Title") OR
(model_id = 1 AND model = "Product") //etc
Dynamically.
I have looked through the documentation but the closest thing I see is addCondition which would require complex coding to get working properly.
Does Yii provide any easy way of achieving this without having to deal with writing complex code to name my params etc?
It is indeed a bit more complex, but here's a working solution:
$criteria = new CDbCriteria();
$param_id = 0;
// $model_ids is the one you built in your original code
foreach( $model_ids as $id_pair ) {
$criteria->addCondition( '( model_id = :id' . $param_id . ' AND model = :model' . $param_id . ' )', 'OR' );
$criteria->params[ ':id' . $param_id ] = $id_pair[0];
$criteria->params[ ':model' . $param_id ] = $id_pair[1];
$param_id++;
}
This will generate custom identifiers for each of your parameters so they will all be validated. Then you can just use $criteria in your query.