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

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(/* ... */);
})
// ...

Related

CakePHP3: How to get the query result count if the result is a collection?

I have the following code:
$sites = $this->Sites->find()
->contain([
'Sitecategories',
'Sitedescriptions.Languages',
'Countries',
'Users'
])
->where([
'postcode LIKE' => $this->request->data['numero'] . '%'
])
->sortBy(function($row) { return substr($row->postcode, 0, 2); }, SORT_ASC);
debug($sites); displays:
object(Cake\Collection\Iterator\SortIterator) {
'count' => (int) 428
}
But I don't understand how to access to that count var.
I tried to access $sites->count() but I get the following error message:
Error: You cannot issue a count on a Collection.
Counting collections is kinda unreliable, given than they can mutate when being iterated, ie the count and the content can change, and with every iteration, changes may be reapplied for certain operations, causing the collection to possibly mutate in an unwated way (filtering, reducing, modifying contained objects, etc). Another unwanted effect may be unrewindable collections, ie they could not be iterated anymore. So that's basically why no count method is implemented.
The debug info shown there is the return value of iterator_count(), it accepts traversable objects and returns the number of elements in the iterator. Calling this will have the aforementioned side effects, the collection will be iterated and possibly mutated.
If you really need to know the count, and want to keep the collection, then you could for example compile it in beforehand, that would create a new collection based on the source data with all modifications applied, like:
$sites = $sites->compile();
$count = iterator_count($sites);
$sites can safely be reused after that point.
If you don't need the collection anymore, you could always simply convert it into an array and count that:
$sites = $sites->toArray();
$count = count($sites);
See also
PHP Manual > iterator_count()
Cookbook > Collections > Collection::compile()
Another possible solution is to use buffered iterator.
$collection = new Collection([1,2,3]);
$collection->count(); //throws exception
$collection->buffered()->count(); //gives 3
You have to use count($sites->toArray()).

Can't use paginate on a query result

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.

specify return fields for all find calls on model in cakephp

I can't believe I don't remember how to do this, but how do I specify in my model the default fields that are returned with the find() methods? I can't find on google how to do this, or at least I don't know the wording to search for.
What you will most likely need to do, is to check if the fields key exists in a beforeFind() method in your model.
If the fields key is not set, you can set it to $this->fields in your native models, and create the beforeFind() in your AppModel, then you can instruct that method to use the $this->fields array from your models.
UPDATE
// AppModel.php
parent function beforeFind($queryData = array()) {
if (empty($queryData['fields']) && !empty($this->fields)) {
$queryData['fields'] = $this->fields;
}
return $queryData;
}
// And in your Model:
public $fields = array(
'Alert.id'
);
This will check for existence of a fields array, and will then check for existence of a $this->fields property. If it does exist, it will apply it to the query data and return that modified query data to the beforeFind() - this will change your find.
Adjust it to fit your needs, and good luck!

Is it safe to do query with Model while in Behavior's BeforeSave callback?

I don't see this documented anywhere, so I ask you, my dear Cake-eaters.
Inside a CakePHP's Behavior::BeforeSave(&$Model) method, I read and write changes to $Model->data array. Before I am finished, I need to read some other records from the database. I am worried that, if I use $Model->find(), it will overwrite the current data within the model, which is about to be saved.
Viewing the source code, the Model::find() function clearly resets the Model::$id variable. This is the same variable I later use to check if a field is being updated.
Here's an example:
<?php
class UniqueBehavior extends ModelBehavior {
function beforeSave(&$Model){
$value = $Model->data[$Model->alias]['unique_field'];
$query = array('conditions' => array('unique_field' => $value));
if ($Model->find('first', $query){
// Does $Model::find() reset the internal $Model->data array?
$Model->data[$Model->alias]['unique_field'] = "..."
//... some other code here
}
//ALSO...
if ($Model->exists()) // Returns true if a record with the currently set ID exists.
$slug = $Model->field('slug');
// this should fetch the slug of the currently updated Model::id from the database
// if I do find()'s, can I trust that the record I'm getting is the right one?
}
}
?>
you can always store the current id in $tmp and assign this stored id back to the model after you are finished
$tmp = $Model->id;
// ...
$Model->id = $tmp;
This way you don't run into problems using the Model-id.
If it is save or not depends on how you work in your model.
I - for example - never rely on this id. I always assign the id to the model manually prior to any update or delete call etc. But this is not necessary, of course. You have to be more careful then, though.

how to display the data returned by get_by_user_id() in DataMapper Code Igniter?

I am new to code igniter data mapper. I have a table called user, and I am trying to retrieve data from the database table and show them to the user.
Here is what I have in the model:
$u=new User();
$results=$u->get_by_user_id($id);
//$results here will be set to huge bunch of none sense data( which also includes the row that I am looking for as well)
if ($u->exists())
{
foreach ($results->all as $row){
$data['user']['first_name']=($row->user_first); //this where I am stuck ..
$data['user']['last_name']=($row->user_last);//this is also where I am stuck..
}
I don't know how to treat results to get a required fields I am looking for and store them in the $data I am passing to the user to view.
Thanks!
When you call get_by_x() on the model, the fields will be populated with data and you can access them like this:
$u = new User();
$u->get_by_user_id($id);
if($u->exists())
{
// you can access the table columns as object fields
$data['user']['first'] = $u->first;
$data['user']['last'] = $u->last;
}
else
{
$data['error'] = 'No such user!';
}
Have a look at the documentation which is really helpful: see Get and Get By.
Also, DataMapper expects all tables to have an id column: see Table Naming Rules. If your column is named id you should then call $u->get_by_id($id) instead of $u->get_by_user_id($id).

Resources