How to implement conditional ordering? - cakephp

i am trying to found out how to create custom order function in cakephp3 order function in ORM find() method.
Let's suppose i have following model
User
name
...
custom_data
custom_data
type
every user has one custom data, where type is one of [ 20, 30, 40 ].
I need order by this type in following manner
if ( type == 20 ) {
// put in first positions
} else {
// put this records after users with custom_data->type != 20
}
I need to use it in paginator so i need somehing like
$this->Users
->find()
->where([ something ] )
->order( 'ASC' => here is my custom function )
Any suggestions?

First things first, from your description it sounds as if you need to use DESC, given that you say that you want records that match the condition first. In ascending order these records would come last.
That being said, the most simple way would be to use a basic condition, ie SQL wise something like:
ORDER BY type = 20 DESC
It could also be solved using a CASE statement, but it's not really neccessary if you have such simple requirements.
$query = $this->Users->find();
$query
->where(/* .. */)
->orderDesc(
$query->newExpr()->add(['type' => 20])
);
Further ordering statements can be added via additional calls to order(), orderAsc(), orderDesc().
See also
API > \Cake\Database\Query::order()
API > \Cake\Database\Query::orderAsc()
API > \Cake\Database\Query::orderDesc()
Cookbook > Database Access & ORM > Query Builder > Selecting Data

Related

CakePHP 3 - access params of Query object

In CakePHP 3.x I can do this:
$Substances = TableRegistry::get('Substances');
$query = $Substances->find()->where($where_conditions)->select(['id']);
debug($query);
This will show me the Query object.
If I want to get the SQL string I can use debug($query->sql());. This will give me the SQL string with placeholders for any parameters, e.g.
SELECT ... WHERE ... in (:c0,:c1,:c2))
When using debug($query) I can see the values for :c0, :c1, etc:
'params' => [
':c0' => [
'value' => (int) 47,
'type' => 'smallinteger',
'placeholder' => 'c0'
],
':c1' => [
'value' => (int) 93,
'type' => 'smallinteger',
'placeholder' => 'c1'
],
':c2' => [
'value' => (int) 845,
'type' => 'smallinteger',
'placeholder' => 'c2'
],
':c3' => [
'value' => (int) 354,
'type' => 'smallinteger',
'placeholder' => 'c3'
]
]
However, I cannot access them outside the debug statement. For example $query->params() or $query['params'] doesn't give me the parameters - it will error. I want to be able to pass this array to a custom function, so how can I access it?
It's strange because I can use debug($query->sql()) to get the SQL string as above, and params is just another thing in that object, but doesn't seem to be accessible.
I've read How to get params from query object in CakePHP 3 but think that's a different question as it was to do with not seeing the values in the debug statement due to the default depth that debug would provide.
The reason I want to do this is because I want to be able to do a CREATE TABLE AS query that will write the values of the SELECT statement into a new table (Important: see this link for an example of how that works in vanilla MySQL). I can't figure out how to do that with the ORM in Cake, so was planning on writing a custom function. But I need to be able to access both the SQL as well as the parameters bound so that the query can be executed in my own function.
If you know of a solution where I can use the ORM to do the CREATE TABLE AS query, I'm still interested to know about this. However I would like to know if params are accessible outside debug() as well.
Premise: I did not actually understand why you need the params
anyway. The information you need is stored by the query ValueBinder object
so you could simply do
$params = $query->getValueBinder()->bindings();
debug($params);
but for some reason this will get you an empty array. My guess is that the query need some kind of initialization first.
in fact if you run
debug($query);
$params = $query->getValueBinder()->bindings();
debug($params);
you'll see your params. I think someone more expert than me will come and give a full explanation
edit: I noticed that debugging $query calls $query->sql() which in turns calls conection->compileQuery();
so you can do
$query->sql(); // you need this to compile the query
// and generate the bindings
$params = $query->getValueBinder()->bindings();
CakePHP does not provide specific methods for creating such CREATE TABLE AS statements, so you'll have to build that on your own.
Compiling a query as the one shown in your question is simple enough using the query objects sql() method, and as arilia already mentioned, you'll be able to access the parameters bound to that query after is was compiled.
Having the compiled SQL and the associated value binder, you can combine this with a custom raw query to build your CREATE TABLE AS statement. All you need to do is prepare a new statement with the compiled SQL, and attach the value binder via its own attachTo() method.
One thing you might also have to do, is to define custom aliases in your select(), as otherwise you'd end up with columns selected (and created) in the form of Substances_id.
$Substances = TableRegistry::get('Substances');
$selectQuery = $Substances
->find()
->where($where_conditions)
->select(['id' => 'id']); // < create aliases as required
// compile the ORM query, this will populate the value binder
$selectSql = $selectQuery->sql();
// combine table creation SQL and compiled ORM query in a single statement
$createStatement = $Substances
->getConnection()
->prepare('CREATE TABLE dynamic_table AS ' . $selectSql);
// attach the ORM querys value binder, binding all its values to the given statement
$selectQuery->getValueBinder()->attachTo($createStatement);
$success = $createStatement->execute();
This should create SQL similar to:
CREATE TABLE dynamic_table AS
SELECT
id AS id
FROM
substances Substances
WHERE
...
See also
Cookbook > Database Access & ORM > Database Basics > Interacting with Statements
API > \Cake\ORM\Association::attachTo()

get result filtered by count grouped by date in cakephp 3

I am working on CakePHP 3.4
I am building some application which uses API key. One key is limited to 100 calls per day, so I'm using multiple keys and want to loop through it.
In my controller, I'm calling a model's function to get API
$api_key = $this->CustomApi->getKey();
if (!$api_key) {
$this->Flash->error(__('API key not found'));
} else {
...
}
In model CustomApi I want to write getKey() function which will find Api key whose call count of the day is less than 100.
Columns of custom_api table are id, api_key and calls are recorded in api_calls table whose columns are id, user_id, custom_api_id, created
Everytime user access function which requires API, a record is created in api_calls table with the time call has been made and primary key of the custom_api.
My question is, How to get Api key from CustomApi model whose call counts are less than 100 for that day (ie., today)
Edit 2 : My Try
$key = $this
->find()
->select(['CustomApi.id', 'CustomApi.api_key'])
->where(['CustomApi' => /* count is less than 100 */])
->leftJoinWith('ApiCalls', function ($q) {
return $q->where(['ApiCalls.created' => date('Y-m-d')]);
})
->group('CustomApi.id');
How to make a count in where?
You're nearly there, you need to use a HAVING clause, WHERE won't work here, as it works on single results, rather than on aggregated ones, which is what you need here.
SQL functions can be built using the function builder, and in order to create a comparison with a function expression, you'll have to leavarege the expression builder, using for example the lt() (less than) method.
$this
->find()
->select(['CustomApi.id', 'CustomApi.api_key'])
->leftJoinWith('ApiCalls', function ($q) {
return $q->where(['ApiCalls.created' => date('Y-m-d')]);
})
->group('CustomApi.id')
->having(function ($exp, $q) {
return $exp->lt(
$q->func()->count('ApiCalls.id'),
100
);
});
And in case ApiCalls.created isn't a date only column, you'd use a BETWEEN clause, or manually clamp the value using >= and <= comparisons against the beginning and the end of the day.
See also
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
Cookbook > Database Access & ORM > Query Builder > Advanced Conditions
API > \Cake\Database\FunctionsBuilder
API > \Cake\Database\Expression\QueryExpression

Magic of UpdateAll

I wrote the code below in cakephp for and updateAll query like
$this->loadModel('User');
$this->User->updateAll(array('stauts'=>'active'),array());
The above code's equivalent SQL query is generated like this
UPDATE User SET status='active' WHERE 0 = 1;
When I write updateAll in cakephp like below
$this->loadModel('User');
$this->User->updateAll(array('stauts'=>'active'));
This code's equivalent SQL query is generated like this
UPDATE User SET status='active';
I don't know why this happens.
If you do not understand my question let me know in comments, I'll explain in shortly.
It's a safety catch
Conditions are often dynamic based on user input. Consider a controller action like so:
function enableAll() {
$conditions = array();
...
if (whatever) {
// Update only today's records
$conditions['created > '] = $yesterday;
}
if ($this->Auth->user()) {
// Update only my records
$conditions['user_id'] = $this->Auth->user('id');
}
$this->Widget->updateAll(
array('active' => 1),
$conditions
);
}
Logically conditions can be one of two things:
An array matching some or no records
An empty array
When it's an empty array, did the developer mean to update all records, or no records?
CakePHP can't know for sure, but if passed, an empty conditions array is more likely to be an error where the intention was to update nothing. Therefore to protect developers from accidentally updating everything, a condition is used which won't match any records (WHERE 0 = 1 is false - it will match no rows, always.).
That's why this:
// I definitely want to update the whole table
$model->updateAll($update);
is treated differently than this:
// mistake? maybe the conditions have been forgotten...
$model->updateAll($update, array());

CakePHP update field value based on other value in same row?

I have been trying to figure out how to do this and it seems that its not something that many people are trying to do in cakephp or I am just completely misunderstanding the documentation.. I am using the following query below to get a field value so I get a value where the "findbycreated" is 0... this part works fine
$unregisteredemail = $this->Testmodel->findBycreated('0');
$emailaddress = $unregisteredemail['Testmodel']['emailaddress'] ;
$emailpassword = $unregisteredemail['Testmodel']['password'] ;
But now, after I do some things with this data that I retrieved, I want to mark a field, in the same row, in the same model / table as a value of '1' to indicate that an action has taken place (email address has been successfully created, for example)... I just can't figure out how to do this in cakephp despite my efforts of going through the documentation and searching, this should be rather simple, I am tempted, at this point, to just use a regular mysql query as its a simple query.. basically the query is (please excuse my syntax as I haven't used direct mysql queries in a while) "update (database / table) set 'created'='1' where 'emailaddress'=$emailaddress"
Or I could use the row ID, if needed, as cakephp seems to prefer this, but still can't get how to do this.. this is my attempt below that is not working:
// update database to show that email address has been created
$this->Testmodel->read('emailaddress', $this->Testmodel->findBycreated('0'))
$this->Testmodel->id = 1;
$this->Testmodel->set(array(
'created' => '1'
));
$this->Testmodel->save();
There are, as you can see from the previous answers, several ways to achieve the same end. I'd just like to explain a little about why your way didn't work.
In the model, CakePHP has abstracted the database row(s) into an array according its implementation of ORM . This provides us with a handy way of manipulating the data and chucking it around the MVC architecture.
When you say:
$this->Testmodel->set(array(
'created' => '1'
));
You are dealing directly with the model, but the data is actually stored, as an array, in a class variable called $data. To access and manipulate this data, you should instead say:
$this->data['Testmodel']['created'] => '1';
The reason for specifying the model name as the first index is that where associated tables have been retrieved, these can be accessed in the same way, so you might have , for instance:
Array([Testmodel] => Array ([id] => 1,
[created] => [1],
...
)
[Relatedmodel] => Array ([id] => 1,
[data] => asd,
...
)
)
...and so on. Very handy.
Now, when you use $this->MyModelName->save() with no parameters, it uses $this->data by default and uses the part of the array of data appropriate to the model you are calling the save method on. You can also pass an array of data, formatted in the same way if, for some reason, you don't (or can't) use $this->data.
Your use of the method read() is incorrect. The first parameter should be null, a string or an array of strings (representing fieldname(s)). The second parameter should be the id of the record you wish to read. Instead, for param 2, you are passing the result of a find, which will be an array. The result, which you are not capturing, will be empty.
I would write your code like:
$this->data = $this->Testmodel->read('emailaddress',1);
$this->data['Testmodel']['created'] = 1;
$this->Testmodel->save();
or more succinctly:
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created', 1);
In this situation I would let Cake deal with the id's and just focus on changing the row data and resaving it to the database
$row = $this->Model->findBycreated(0);
$row['Model']['emailaddress'] = 1;
$this->Model->save($row);
This way, you don't have to worry about the id's, as the id will be in your dataset anyway, so just change what you want and then tell Cake to save it.
Ninja edit, Be sure that you are returning a full row with an id from your findBycreated() method.
There're many ways to do your work.I suggest you to read the cookbook about saving data in cakephp.And besides david's solution another simple way would be
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created' =>'1');
Ok, I think I finally found the solution, I was able to get this to work:
$this->Test->updateAll(
array(
'Test.field' => 'Test.field+100'
),
array(
'Test.id' => 1
)
);
I think you have to use updateAll as anything else will just create a new row.. basically CakePHP, for whatever reason, neglected to include a function for updating just one field so you have to put it into an array with the updateAll to make it work...
the +100 is where the updated info goes, so in this case "100" would be what the field is updated to.
In cakephp 3.x the syntax seems to be different. This is what worked for me in 3.x:
$this->Tests->updateAll(
[
'Tests.field = Tests.field+100'
],
[
'Tests.id' => 1
]
];
The difference is that the entire expression needs to be in the value of the first array.

How do I join two tables in a third n..n (hasAndBelongsToMany) relationship in CakePHP?

I have a n...n structure for two tables, makes and models. So far no problem.
In a third table (products) like:
id
make_id
model_id
...
My problem is creating a view for products of one specifi make inside my ProductsController containing just that's make models:
I thought this could work:
var $uses = array('Make', 'Model');
$this->Make->id = 5; // My Make
$this->Make->find(); // Returns only the make I want with it's Models (HABTM)
$this->Model->find('list'); // Returns ALL models
$this->Make->Model->find('list'); // Returns ALL models
So, If I want to use the list to pass to my view to create radio buttons I will have to do a foreach() in my make array to find all models titles and create a new array and send to the view via $this->set().
$makeArray = $this->Make->find();
foreach ($makeArray['Model'] as $model) {
$modelList[] = $model['title'];
}
$this->set('models', $models)
Is there any easier way to get that list without stressing the make Array. It will be a commom task to develops such scenarios in my application(s).
Thanks in advance for any hint!
Here's my hint: Try getting your query written in regular SQL before trying to reconstruct using the Cake library. In essence you're doing a lot of extra work that the DB can do for you.
Your approach (just for show - not good SQL):
SELECT * FROM makes, models, products WHERE make_id = 5
You're not taking into consideration the relationships (unless Cake auto-magically understands the relationships of the tables)
You're probably looking for something that joins these things together:
SELECT models.title FROM models
INNER JOIN products
ON products.model_id = models.model_id
AND products.make_id = 5
Hopefully this is a nudge in the right direction?
Judging from your comment, what you're asking for is how to get results from a certain model, where the condition is in a HABTM related model. I.e. something you'd usually do with a JOIN statement in raw SQL.
Currently that's one of the few weak points of Cake. There are different strategies to deal with that.
Have the related model B return all ids of possible candidates for Model A, then do a second query on Model A. I.e.:
$this->ModelB->find('first', array('conditions' => array('field' => $condition)));
array(
['ModelB'] => array( ... ),
['ModelA'] => array(
[0] => array(
'id' => 1
)
)
Now you have an array of all ids of ModelA that belong to ModelB that matches your conditions, which you can easily extract using Set::extract(). Basically the equivalent of SELECT model_a.id FROM model_b JOIN model_a WHERE model_b.field = xxx. Next you look for ModelA:
$this->ModelA->find('all', array('conditions' => array('id' => $model_a_ids)));
That will produce SELECT model_a.* FROM model_a WHERE id IN (1, 2, 3), which is a roundabout way of doing the JOIN statement. If you need conditions on more than one related model, repeat until you have all the ids for ModelA, SQL will use the intersection of all ids (WHERE id IN (1, 2, 3) AND id IN (3, 4, 5)).
If you only need one condition on ModelB but want to retrieve ModelA, just search for ModelB. Cake will automatically retrieve related ModelAs for you (see above). You might need to Set::extract() them again, but that might already be sufficient.
You can use the above method and combine it with the Containable behaviour to get more control over the results.
If all else fails or the above methods simply produce too much overhead, you can still write your own raw SQL with $this->Model->query(). If you stick to the Cake SQL standards (naming tables correctly with FROM model_as AS ModelA) Cake will still post-process your results correctly.
Hope this sends you in the right direction.
All your different Make->find() and Model->find() calls are completely independent of each other. Even Make->Model->find() is the same as Model->find(), Cake does not in any way remember or take into account what you have already found in other models. What you're looking for is something like:
$this->Product->find('all', array('conditions' => array('make_id' => 5)));
Check out the Set::extract() method for getting a list of model titles from the results of $this->Make->find()
The solution can be achieved with the use of the with operation in habtm array on the model.
Using with you can define the "middle" table like:
$habtm = " ...
'with' => 'MakeModel',
... ";
And internally, in the Model or Controller, you can issue conditions to the find method.
See: http://www.cricava.com/blogs/index.php?blog=6&title=modelizing_habtm_join_tables_in_cakephp_&more=1&c=1&tb=1&pb=1

Resources