What's the correct way to limit the number of contained associated records when paginating?
The docs don't appear to address how to set options for 'contain' while paginating, but $paginate['contain']['AssociatedModel']['limit'] = 1; seemed to make sense. However, that line is resulting the following error for me in CakePHP 3.1.3:
Fatal error: Unsupported operand types in ...\vendor\cakephp\cakephp\src\ORM\EagerLoader.php on line 312
The error being generated because, in the line $pointer[$table] = $options + $pointer[$table];, $options is 1 and $pointer[$table] is an array.
Confusingly, setting $paginate['contain']['AssociatedModel']['fields'] works as expected, but setting 'limit' or 'order' results in that same error.
Despite the fact that setting $paginate['contain']['AssociatedModel']['fields'] = [...] works, other options need to be set using functions. The following code fixes my problem:
$paginate['contain']['AssociatedModel'] = function($q) {
return $q
->select([...])
->limit(1)
->order([...]);
};
Related
I am following Using SQL Functions to build my query. I tend to get the first due instalment. My query was working (on cakephp3.1) before updating the cakephp to version 3.3 (by composer).
In my Controller
$this->loadModel('Orders');
$order = $this->Orders->find()
->where(['order_id' => $orderId])
->contain([
'PaymentInstalments' => function($q) {
return $q->find('firstDue');
},
'Users' => function($q) {
return $q->select(['email']);
}
])
->first();
On my paymentInstalments Table
public function findFirstDue(Query $query, array $options)
{
$alias = $this->alias();
$query
->select([
// "id" => $query->func()->min("$alias.id"), ## This use to work before but now is not working
'id' => $query->func()->min(function($row){
return $row->id;
}),
'order_id', 'amount', 'date_due', 'transaction_id'
])
->contain([
'Transactions' => function($q) {
return $q
->select([
'id', 'transaction_date'
])
->where(function ($exp, $q) {
return $exp->isNull('transaction_date');
});
}
]);
debug($query->__debugInfo()['sql']);
die;
return $query;
}
Here is print of my query.
'SELECT PaymentInstalments.id AS `PaymentInstalments__id`, PaymentInstalments.order_id AS `PaymentInstalments__order_id`, PaymentInstalments.instalment_num AS `PaymentInstalments__instalment_num`, PaymentInstalments.amount AS `PaymentInstalments__amount`, PaymentInstalments.date_due AS `PaymentInstalments__date_due`, PaymentInstalments.payment_email_sent AS `PaymentInstalments__payment_email_sent`, PaymentInstalments.transaction_id AS `PaymentInstalments__transaction_id`, {
"id": 41408,
"order_id": "10000",
"instalment_num": 1,
"amount": 100,
"date_due": "2016-08-25T12:15:00+01:00",
"payment_email_sent": false,
"transaction_id": null
} AS `PaymentInstalments`.`id`, Transactions.id AS `Transactions__id`, Transactions.transaction_date AS `Transactions__transaction_date` FROM payment_instalments PaymentInstalments LEFT JOIN transactions Transactions ON ((Transactions.transaction_date) IS NULL AND Transactions.id = (PaymentInstalments.transaction_id)) WHERE PaymentInstalments.order_id in (:c0)'
The problem is if I use "id" => $query->func()->min("$alias.id"), I get this error:
You are required to select the "PaymentInstalments.order_id" field(s)
And if I use this
'id' => $query->func()->min(function($row){
return $row->id;
}),`
I get this error:
Error: SQLSTATE[42000]: Syntax error or access violation: 1064 You
have an error in your SQL syntax; check the manual that corresponds to
your MySQL server version for the right syntax to use near '"id":
41408, "order_id": "10000", "instalment_num": 1, "amount": ' at
line 2
Any help please
You cannot use collection methods
$query->min() is a collection method, respectively will execute the query and call the method on the resulting result set, ie it has nothing to do with SQL functions. Just look at your debug output, there's result set data in your SQL query.
See Cookbook > Database Access & ORM > Query Builder > Queries Are Collection Objects
$query->func()->min() is the way to go, that's what generates an SQL function expression object.
There's a bug in the core regarding function expressions
That being said, what you are experiencing is a bug that is present in the foreign key presence existence check, and is being triggered by having expressions in the select list. You should see even more errors, respectively warnings like
Object of class Cake\Database\Expression\FunctionExpression could not be converted to string
Please make sure to always include stuff like that in your questions! If you don't see it, try cranking up your error reporting level.
The cause of that warning, combined with a PHP bug, is the source of the whole problem, the select list ist being passed to array_diff(), which uses string comparison, ie (string)$elementA === (string)$elementB, which will fail since the expression object cannot be converted to a string.
PHP is weird
And now comes an interesting quirk, without that you'd only see a warning, but the query would run fine.
Before PHP 7, if you put the key that is being searched for, ie order_id, directly after the function expression in the select list, then array_diff() won't find it, and say that it is missing. However if you have at least one additional element between it and the function expression, ie something like
'id' => $query->func()->min("$alias.id"),
// instead of here
'amount',
'order_id', // put it here
'date_due',
'transaction_id'
then array_diff() will find it, and the query will execute fine despite the string conversion error being thrown. But that's not all, no no, it wouldn't be PHP if there weren't really weird things going on.
The whole "place the key differently and behavior changes" only happens when array functions like asort() are being invoked inside of the error handler callback. It doesn't matter on what data they are being invoked, it doesn't need to be connected to the error in any way, it could just be something like $foo = []; asort($foo);, that would already cause that weird behavior.
https://3v4l.org/2nT5M
You gotta love PHP :)
Use a hardcoded SQL fragment as a workaround
As a workaround until the bug is fixed, you could pass an SQL fragment as a string instead, like
'id' => 'MIN(PaymentInstalments.id)'
Last but not least
Please report the string conversion problem as a bug over at GitHub.
You could temporarily get it to work by hard-coding it:
$alias = $this->alias();
$query->select(['id' => "MIN($alias.id)"]);
How can I dynamically build the contain in the new cakephp 3 query builder. This is what I have now:
$query = $dbTable->find()
->select($contain['select']['fields'])
->contain(function($q) use($array,$contain){
$new = [];
foreach($array as $v){
if(isset($contain['contains'][$v])){
$fields = $contain['contains'][$v];
$new[$v] = $q->select($fields);
}
}
return $new;
});
But I am getting several errors with this:
Warning (2): Illegal offset type in isset or empty [CORE\src\ORM\EagerLoader.php, line 198]
Warning (2): strpos() expects parameter 1 to be string, object given [CORE\src\ORM\EagerLoader.php, line 203]
Warning (2): Illegal offset type [CORE\src\ORM\EagerLoader.php, line 223]
Warning (2): Illegal offset type [CORE\src\ORM\EagerLoader.php, line 224]
As already mentioned by Lorenzo, that's not how it works, contain() doesn't accept callables, just look at the docs:
http://api.cakephp.org/3.0/class-Cake.ORM.Query.html#_contain
Also your code would invoke select() multiple times on one and the same query, that wouldn't work anyways.
However, looking at your code it seems that you could simply make use of the fields option, ie build a simple array to pass to contain(). This is shown in the docs, the example however will trigger an error as it seems to be necessary to explicitly set the foreign key field too:
$query->contain([
'Articles' => [
'fields' => ['foreign_key_column_name', 'title']
]
]);
You cannot use contain with a closure inside. If you believe this is a good idea (I think it could be) then open a enhancement request on github.
To get all fields in the table Articles do this in your controller.
$query = $this->modelName->find('all')
->contain('Articles');
If you want a expecify field use this:
$query = $this->modelName->find('list', [
'keyField' => 'id',
'valueField' => 'author.name'
])->contain(['Articles']);
To more informations about: https://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html
I manually set the CakePHP Pagination values in my Usergals Controller like so, so as to Paginate a related Model (TreasuresUsergal) on the view of Usergal. Here is a simplified snippet from the UsergalController:
public function view($id = null) {
$this->loadModel('TreasuresUsergal');
$options['joins'] = array(
array('table' => 'treasures',
'alias' => 'Treasure2',
'type' => 'LEFT',
'conditions' => array(
'Treasure2.id = TreasuresUsergal.treasure_id',
)
)
);
$options['conditions']=array('TreasuresUsergal.usergal_id'=>$id);
$options['fields']=array('Treasure2.*','TreasuresUsergal.ord','TreasuresUsergal.comments');
$options['order']=array('TreasuresUsergal.ord'=>'asc');
$options['limit']=$limit;
$this->Paginator->settings = $options;
$treasures=$this->Paginator->paginate('TreasuresUsergal');
$this->set('treasures',$treasures);
}
So in the above example, $id is the value passed to the view function from the URL. There is a live example of this here:
http://collections.centerofthewest.org/usergals/view/20
As you can see, it works just fine for a single page. However, today I tested the Paginator in the view and discovered the "next" button does not work. The Counter, sorting, and Page numbers all load correctly - but anytime the actual named parameter "page:n" is passed (when n is greater than 1) I get a Not Found page with the following error:
Not Found
Error: The requested address '/usergals/view/20/page:2?url=%2Fusergals%2Fview%2F20' was not found on this server.
I must be missing something simple - I have experimented with the routes a little, but haven't been able to figure it out. Or perhaps I am missing some Paginator options? Or does it think its OutOfBounds when its not?
UPDATE / WORKAROUND
After some messing around, I have devised this workaround. Not as nice as I'd like, but here is the basic idea (error handling, etc can be added)
First, I added a check in beforeFilter to see if page paramter was set. If so, I change it to 'p' parameter and redirect.
I did this here because otherwise I had problems with the Not Found exception (see notes at bottom). So, in beforeFilter:
if (isset($this->params['named']['page'])){
$newurl=$this->params['named'];
$pg=$newurl['page'];
unset($newurl['page']);
$newurl['p']=$pg;
$this->redirect(array('action' => 'view/'.$this->params['pass'][0])+$newurl);
}
Then, in the 'view' function of the same controller, I added this along with the other Paginator options:
if (isset($this->params['named']['p'])) $options['page']=$this->params['named']['p'];
With this, the standard Paginator behavior seems to work fine in the view. Prev, next, etc.
If anyone has a better suggestion, I would love to hear it. I don't like the idea of having to redirect, but it works for now.
It's worth noting that adding this code (even just to experiment) - caused all of my pagination counts to stop working. The query in debug was correct, but the displayed counts were wrong:
try {
$this->Paginator->paginate();
} catch (NotFoundException $e) {
}
See the following example code:
$conditions = array("Post.title" => 'This is a "Book"');
// Example usage with a model:
$this->Post->find('first', array('conditions' => $conditions));
Because find() actually looks for title = 'This is a \"Book\"', no result returned. I am wondering how to prevent find() from adding backslashes. Or is there any other solution?
==fixed==
*Actually the error occurred when I used updateAll($field, $conditions), not find(). I did not put the quote around literal values. For example, $field = array('title' => $some_title) should be $field = array('title' => "'" . Sanitize::escape($some_title) . "'") . Don't like the way CakePHP handles this though.*
You must be mistaken. The error must be somewhere else.
The resulting SQL query does contain
LIKE 'foo \"bar\"'
But that escaping is actually intentional.
I will still turn up the DB entry with foo "bar" - I just tried it myself with cake2.3/2.4.
So CakePHP is working correctly.
Just checked with cakePhp version 2.3.5, The double quotes are working fine, Please check below code for a Profile controller.
$data = $this->Profile->find('all',array('conditions'=>array('Profile.type'=>'user "one"')));
pr($data);
Using CakePHP 1.3 I created a small application which has a FooController. In this FooController I am using the Paginator to deliver the content of Foo to its views.
In the head of the FooController class I customized the preparation of the data from Foo like this:
var $paginate = array(
'limit' => 100,
'order' => array('Order.created' => 'desc'));
Now I would like to put the value for 'limit' into a configuration file.
So I created a file $config['list_length'] = '500'; in the config directory which I load in bootstrap.php. That works fine, I can echo the value of list_length, for example in the beforeFilter() of the class.
But I fail to make the Paginator use that value.
First I tried:
var $paginate = array(
'limit' => Configure::read('list_length'),
'order' => array('Order.created' => 'desc'));
But that fails with the following PHP error:
Parse error: syntax error, unexpected '(', expecting ')' in xyz/app/controllers/foo_controller.php on line 9
Then I tried:
public function beforeFilter() {
parent::beforeFilter();
$paginate['limit'] = Configure::read('list_length');
}
But it would still use the value '100' instead of '500'.
I guess this is a rather trivial problem that really comes from the fact that I am not very proficient with basic PHP concepts. I don't have much opportunity to get to program a little bit in my current job, so I'm really out of practice.
I would very much appreciate any hint on how to do this and even more on what concept I am missing here, so I could read a little bit about it.
You almost got it right;
The reason PHP is giving you the error in your first example is that class properties cannot be set using 'dynamic' values; from the documentation:
This declaration may include an initialization, but this initialization must be a constant value--that is, it must be able to be evaluated at compile time and must not depend on run-time information in order to be evaluated.
http://www.php.net/manual/en/language.oop5.properties.php
If you need to set a property to a value that is 'dynamic' (e.g. The result of a function), you will need to assign if from within a class 'method'
You tried to do this in your second example, however, properties of a class/object should be accessed using $this->nameOfTheProperty to indicate it is not a 'variable', but a 'property' of the object;
public function beforeFilter() {
parent::beforeFilter();
$this->paginate['limit'] = Configure::read('list_length');
}
Hope this helps