Can't use paginate on a query result - cakephp

According to the doc http://book.cakephp.org/3.0/en/controllers/components/pagination.html, I'd like to paginate a result of a query like below:
$unlocked_sitesQuery = $this->Deviceconnections
->find()
->contain([
'Agthemes.Sites',
'Agthemes.Agpois',
'Agthemes.Agthemelanguages'
])
->where(['request' => 'unlock'])
->groupBy('agtheme.site.id');
$unlocked_sites = $this->paginate($unlocked_sitesQuery);
But I get the following error:
Error: Call to undefined method ArrayIterator::alias()
File /home/mywebsite/vendor/cakephp/cakephp/src/Controller/Component/PaginatorComponent.php
Line: 154
What does it mean?
EDIT
It seems that #ndm is right but the doc says:
By default the paginate() method will use the default model for a controller. You can also pass the resulting query of a find method:
public function index()
{
$query = $this->Articles->find('popular')->where(['author_id' => 1]);
$this->set('articles', $this->paginate($query));
}
So it should work on a result set. Or I didn't understand what the doc explains. Possible.

It means that you are passing the wrong type of object. Pagination on result sets is not supported, only tables (either objects or names) and queries are.
groupBy is not a method of the query class, it's one of the magic methods that are causing the query to be executed, and forward the method call to the resulting result set. So you end up calling Cake\ORM\ResultSet::groupBy(), which returns another collection.
Cookbook > ... ORM > Query Builder > Queries Are Collection Objects
Cookbook > ... ORM > Retrieving Data & Results Sets > Working with Result Sets
So if you need such grouped results in pagination, then you have to solve this (at least partially) on SQL level, for example by fetching the results the other way around, ie fetch Sites and their associations, and filter by Deviceconnections.request, something like this (no guarantee that this will give you the desired result, but the example should give you a hint!):
$query = $Sites
->find()
->contain([
'Agthemes.Deviceconnections',
'Agthemes.Agpois',
'Agthemes.Agthemelanguages'
])
->matching('Agthemes.Deviceconnections', function(\Cake\ORM\Query $query) {
return $query
->where([
'Deviceconnections.request' => 'unlock'
]);
});
You'd of course have to adapt your view code accordingly.

Related

Sorting on nested relationship

I have the following relationship:
Rounds hasMany Results
Results belongsTo Drivers
Results are shown on Rounds's view page: rounds/view/{id}. I want to sort Results based on Drivers.name.
This is my RoundsController::view method:
public function view($id = null)
{
$this->paginate = [
'sortableFields' => [
'Results.Drivers.name',
]
];
$round = $this->Rounds->get($id, [
'contain' => ['Championships' => ['Teams'], 'Tracks', 'Results' => ['Drivers', 'Constructors']],
]);
$this->set(compact('round'));
}
And in my rounds/view.ctp file, I have this:
<?= $this->Paginator->sort('Results.Drivers.name', __('Driver')); ?>
However, no matter what I change, it doesn't sort by driver name. Also, when I click it multiple times, the direction stays asc.
Why is it not working?
Like #ndm mentioned, your code is showing only one specific Round. View method/function is receiving an $id arg, this $id is related to the round id you want to show in view page.
You can not order this (you can do it but it doesnt make sense) because you are receiving only one Round, and it doesnt make sense to order only one row.
This view method is usually used with this goal: show individual/personal information about an Entity (Round entity in your case). Please, note I use the word USUALLY.
I think you could use the index method for this purpose (this method/function is usually used to list entities from a model and its association data). You could create a new method/function for your porpuse as well if you already are using index method for another goal.
I suggest something like this, assuming you will use the index function:
public function index()
{
$rounds = $this->Rounds->find('all')
->contain([
'Championships' => ['Teams'],
'Tracks',
'Results' => [
'Constructors'
[
'Drivers' => function (Query $q) {
return $q->order('Drivers.name');;
}
]
]
]);
$rounds= $this->paginate($rounds);
$this->set(compact('rounds'));
}
You could pass conditions into the contain or do the same into paginate method. I always use the first (just preferences).
NOTE: this code may not work, it is an idea of what you could do.
Finders documentation: https://book.cakephp.org/4/en/orm/retrieving-data-and-resultsets.html#using-finders-to-load-data
Passing conditions to contains: https://book.cakephp.org/4/en/orm/query-builder.html#passing-conditions-to-contain

How to remove an item from a resultset in CakePHP 3?

I'd like to remove some specified items from a ResultSet
which means I need a ResultSet object at end.
Is there any way to remove an item from a ResultSet or create another ResultSet from existing ResultSet?
I've tried using filter() or reject().
But, they don't return a ResultSet object?
$ids = [123, 234, 456];
$results = $this->find()
->where(['xxx' => 'xxxx'])
->all();
$newResults = $results->filter(function ($result) {
return in_array($result->id, $ids);
});
I would question whether you actually need a result set, that is an object that implements \Cake\Datasource\ResultSetInterface.
The result set interface is just a combination of \Cake\Collection\CollectionInterface, \Countable, and \Serializable, which is almost satisfied by CakePHPs regular collection class, ie \Cake\Collection\Collection, it's just lacking \Countable::count(). So the collection returned by your filter() call should satisfy your needs in most cases.
If for some reason you'd be forced to pass a \Cake\Datasource\ResultSetInterface implementation, and you absolutely cannot change that contract for whatever reason, then you could always pass your data to a new result set decorator:
$resultSet = new \Cake\Datasource\ResultSetDecorator($collection->toArray());
Also you can manipulate your data on query level, either by simply not retrieving the data that you don't want in the first place, ie exclude them in your where() conditions, or by filtering in a result formatter, then you'll directly receive a reduced \Cake\Datasource\ResultSetInterface object from your query:
$results = $this
->find()
->where(['xxx' => 'xxxx'])
->formatResults(function (\Cake\Collection\CollectionInterface $results) {
return $results->filter(/* ... */);
})
// ...

Cakephp 3: Countercache with conditions on the target model

I am trying to count the number of Spots associated with a Plan, but limited to Spots downloaded after the plans renewal date. Hope that makes sense. I would image something like this, but it doesn't work:
class SpotsTable extends Table
{
public function initialize(array $config)
{
$this->addBehavior('CounterCache', [
'Plan' => [
'creditsUsed' => [
'conditions' => [
'downloaded >' => 'Plan.renewed'
]
]
]
]);
...
}
...
}
Basically right now it acts as though Plan.renewed means NULL.
Is this possible, or am I on the wrong track?
Two problems
1. Identifiers cannot be passed as string values
When using the key => value format, the value side will always be subject to binding/escaping/casting unless it's an expression object, so since the downloaded column is probably a date/time type, you'll end up with Plan.renewed being bound as a string, thus the final SQL will be something like:
downloaded > 'Plan.renewed'
which probably always results in false. Long story short, use for example an identifier expression:
'Spots.downloaded >' => new \Cake\Database\Expression\IdentifierExpression('Plan.renewed')
2. The counter query doesn't have access to the associated table
Plan.renewed will not be accessible in the query generated by the counter cache behavior, it will not automatically contain/join associations, it will create a simple query with a condition based on the foreign key value in the currently processed Spot entity.
So you have to use a custom/modified query, for example using a custom finder, something like this:
'creditsUsed' => [
'finder' => 'downloadedAfterPlanRenewal'
]
// in SpotsTable
public function findDownloadedAfterPlanRenewal(\Cake\ORM\Query $query, array $options)
{
return $query
->innerJoinWith('Plan')
->where([
'Spots.downloaded >' => $query->identifier('Plan.renewed')
]);
}
This will properly join in the association, so that you can compare with a field from Plan. The original primary key conditions generated by the behavior will already be applied on the given query object.
See also
Cookbook > Database Access & ORM > Behaviors > CounterCache > Advanced Usage
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods

Cakephp 3.0: Multiple matching conditions

I'm trying to daisy-chain matching() calls on a query, or find an equivalent way to achieve the same result. Specifically, here's what I'd like to do:
$matchedItemVersion = $this->ItemVersions->find()
->where(['version' => $verion])
->matching('Items', function($q) use ($ipnCore) {
return $q->where(['ipn_core' => $ipnCore]);
})
->matching('Items.ItemTypes.ItemSuperGroups', function($q) use ($itemSuperGroupName) {
return $q->where(['name' => $itemSuperGroupName]);
})
->first()
->toArray();
This would let me filter my ItemVersions results by two conditions on associated models. However, right now, the second matching() call basically overrides the first one, so the first matching() call has no effect on the query.
We know that you cannot chain a matching() and a contain()(https://github.com/cakephp/cakephp/issues/5109), but is there a way to do it with matching(), or a way to get the same result?
Appreciate the help.

Cakephp Paginate Find

I want to list the posts of a given user. It work but paginate is not accurate.
My code is the following
public function index($userid = null) {
if ($this->Post->exists($userid)) {
$this->set('posts',$this->Post->find('all',array('conditions'=>array('user_id'=>$userid))),
$this->paginate());
} else
{
$this->Post->recursive = 0;
$this->set('posts', $this->paginate());
}
The result give the correct list --> 3 posts, but the paginator display page number 1 and 2
Can you help me?
Thank you
Refer to the documentation
The code in the question is quite confused.
find
The find method only has two parameters:
find(string $type = 'first', array $params = array())
The third parameter (the result of calling paginate) isn't used and will be ignored - but it will setup the view variables for the pagination helper, based on the conditions used in the paginate call - there are no conditions being used.
It is not possible to paginate the result of a find call - to do so restructure the code to call paginate instead of find.
paginate
The paginate method is just a proxy for the paginator component - it can be used in several ways, this one (controller code example):
$this->paginate($conditions)
Is the most appropriate usage for the case in the question i.e. the complete action code should be similar to:
public function index($userId = null) {
$conditions = array();
if ($userId) {
$conditions['Post.user_id'] = $userId;
}
$this->set('posts',$this->paginate($conditions));
}
Note that logically, if a user id is requested that doesn't exist the response should be nothing - not everything.
I'm quite sure that conditions for paginate do now work that way.
If you want to set conditions for paginations you should do it as follows:
$this->paginate = array('conditions' => array('Post.user_id' => $userid)));
$this->set('posts', $this->paginate());
And yes, the result stored in $posts ( in view ) will be proper as you assigned proper find result to it, meanwhile you've paginated post model without any conditions whatsoever.
First off, you're checking to see if the post exists but using the $userid. Are you trying to see "if the user exists, get the posts for that user, or else get posts for ALL users"? As you have it right now, say you have the $userid = 159, but the max Post.id in your database is 28, then the condition is not being met because it is checking to see whether or not there is a Post with the id = 159 that exists, which it doesn't.
Second, your conditions are wrong. You are performing a find and then a paginate which are two separate queries. The conditions are being implemented on the find query but not the paginate but you are only displaying the find results.
public function index($userid = null) {
// setting recursive outside of if statement makes it applicable either way
$this->Post->recursive = 0;
// check if user exists
if ($this->Post->User->exists($userid)) {
// get posts for user
$this->set('posts', $this->paginate('Post', array('Post.user_id' => $userid));
}
else{
// get all posts
$this->set('posts', $this->paginate('Post'));
}
} // end index function

Resources