CackePHP 3 formatResults() ORDER by field created dynamically - cakephp-3.0

I wish to sort on a field that I dynamically creates in formatResults()
$users = $this->Users
->find('all')
->formatResults(function ($users) use ($lat, $lng) {
return $users->map(function ($user) use ($lat, $lng) {
$user->distance = getDist($lat, $lng);
return $user;
});
})
->order([
'distance' => 'ASC'
]);
[...]
$this->set('users', $this->paginate($users));
The field $user->distance is not in the database.
$user->distance contains a float (getDist($lat, $lng);) that is variable depending on the position of the user at the time of his request.
->order([
'distance' => 'ASC'
]);
return an error : Column not found: 1054 Unknown column 'Users.distance' in 'order clause'
My question is this: Can I sort a field I have dynamically created when receiving information from the user?

You can sort by a field calculated in php, but it will not work for pagination, since for doing the sorting you need all the records from the database. You need to create an equivalent logic that runs in SQL so you can sort.
Check this plugin that can help you calculate the distances and sort by them https://github.com/dereuromark/cakephp-geo/
The is the find('distance') custom finder that you can dynamically add to your table when using the provided behavior. https://github.com/dereuromark/cakephp-geo/blob/master/src/Model/Behavior/GeocoderBehavior.php#L223

Related

Multiple Fields with a GroupBy Statement in Laravel

Already received a great answer at this post
Laravel Query using GroupBy with distinct traits
But how can I modify it to include more than just one field. The example uses pluck which can only grab one field.
I have tried to do something like this to add multiple fields to the view as such...
$hats = $hatData->groupBy('style')
->map(function ($item){
return ['colors' => $item->color, 'price' => $item->price,'itemNumber'=>$item->itemNumber];
});
In my initial query for "hatData" I can see the fields are all there but yet I get an error saying that 'colors', (etc.) is not available on this collection instance. I can see the collection looks different than what is obtained from pluck, so it looks like when I need more fields and cant use pluck I have to format the map differently but cant see how. Can anyone explain how I can request multiple fields as well as output them on the view rather than just one field as in the original question? Thanks!
When you use groupBy() of Laravel Illuminate\Support\Collection it gives you a deeper nested arrays/objects, so that you need to do more than one map on the result in order to unveil the real models (or arrays).
I will demo this with an example of a nested collection:
$collect = collect([
collect([
'name' => 'abc',
'age' => 1
]),collect([
'name' => 'cde',
'age' => 5
]),collect([
'name' => 'abcde',
'age' => 2
]),collect([
'name' => 'cde',
'age' => 7
]),
]);
$group = $collect->groupBy('name')->values();
$result = $group->map(function($items, $key){
// here we have uncovered the first level of the group
// $key is the group names which is the key to each group
return $items->map(function ($item){
//This second level opens EACH group (or array) in my case:
return $item['age'];
});
});
The summary is that, you need another loop map(), each() over the main grouped collection.

Cakephp 3: Modifying Results from the database

In my database there is a content table and when fetching data from this table I would like to append field url to the results, which is based on slug field which is contained in the table. Anyway, I have seen a way to do this in the previous versions of cakephp using behavior for the model of this table and then modifying results in afterFind callback in the behavior class. But in version 3 there is no afterFind callback, and they recommend using mapReduce() method instead in the manual, but this method is poorly explained in the manual and I cant figure out how to achieve this using mapReduce().
After little bit of research I realized that the best way to append the url field field to find results is using formatResults method, So this is what I did in my finders:
$query->formatResults(function (\Cake\Datasource\ResultSetInterface $results) {
return $results->map(function ($row) {
$row['url'] = array(
'controller' => 'content',
'action' => 'view',
$row['slug'],
$row['content_type']['alias']
);
return $row;
});
});

Custom finder selecting fields in associated model Cakephp3

I have an Event model hasMany Attendances with event_id in the attendances. I want to select some fields in attendance table in my custom find method but the contain() method doesn't join the two tables.
public function findAttendanceDetails(Query $query)
{
return $query->contain('Attendances')
->select(['Gender' => 'Attendances.gender',
'Name' => 'Attendances.full_name'
]);
}
I am getting an error of Error: SQLSTATE[42S22]: Column not found: 1054 Champ 'Attendances.gender' inconnu dans field list and wondering what is missing.
When I use the ->Join() method instead of ->contain(), I get the results.
Contain doesn't quite work that way. You would be looking for something like:
return $query->contain(['Attendances']);
Which will just grab and return the full associated table Attendances. Contain looks for an array, not a string value.
If you want specific fields from the associated table, you could try:
return $query->contain(['Attendances' => function ($q) {
return $q
->select(['gender', 'full_name'])
}
]);
You can read more about using contain here.

Codeigniter/Mysql: Column count doesn't match value count with insert_batch()?

Alright, so i have a huge list (like 500+) of entries in an array that i need to insert into a MySQL database.
I have a loop that populates an array, like this:
$sms_to_insert[] = array(
'text' => $text,
'contact_id' => $contact_id,
'pending' => $status,
'date' => $date,
'user_id' => $this->userId,
'sent' => "1"
);
And then i send it to the database using the built insert_batch() function:
public function add_sms_for_user($id, $sms) {
//$this->db->delete('sms', array("user_id" => $id)); Irrelevant
$this->db->insert_batch('sms', $sms); // <- This!
}
The error message i get is as follows:
Column count doesn't match value count at row 1.
Now, that doesn't make sense at all. The columns are the same as the keys in the array, and the values are the keys value. So, why is it not working?
Any ideas?
user_id turned out to be null in some situations, that's what caused the error.
EDIT: If you replace insert_batch() with a loop that runs insert() on the array keys you will get more clear error messages.

CakePHP Model with "Between dates"

I have a large data set (over a billion rows). The data is partitioned in the database by date. As such, my query tool MUST specify an SQL between clause on every query, or it will have to scan every partition.. and well, it'll timeout before it ever comes back.
So.. my question is, the field in the database thats partitioned is a date..
Using CakePHP, how can I specify "between" dates in my form?
I was thinking about doing "start_date" and "end_date" in the form itself, but this may bring me two a second question.. how do I validate that in a model which is linked to a table?
If I am following you correctly:
The user must specify start/end dates for find queries generated from a form
You need to validate these dates so that, for example:
end date after start date
end date not centuries away from start date
You want validation errors appearing inline within the form (even though this isn't a save)
Since you want to validate these dates they will be harder to grab when they are tucked away inside your conditions array. I suggest trying to pass these in separately and then dealing with them later:
$this->Model->find('all', array(
'conditions' => array(/* normal conditions here */),
'dateRange' => array(
'start' => /* start_date value */,
'end' => /* end_date value */,
),
));
You should hopefully be able to handle everything else in the beforeFind filter:
public function beforeFind() {
// perform query validation
if ($queryData['dateRange']['end'] < $queryData['dateRange']['start']) {
$this->invalidate(
/* end_date field name */,
"End date must be after start date"
);
return false;
}
/* repeat for other validation */
// add between condition to query
$queryData['conditions'][] = array(
'Model.dateField BETWEEN ? AND ?' => array(
$queryData['dateRange']['start'],
$queryData['dateRange']['end'],
),
);
unset($queryData['dateRange']);
// proceed with find
return true;
}
I have not tried using Model::invalidate() during a find operation, so this might not even work. The idea is that if the form is created using FormHelper these messages should make it back next to the form fields.
Failing that, you might need to perform this validation in the controller and use Session::setFlash(). if so, you can also get rid of the beforeFind and put the BETWEEN condition array in with your other conditions.
if you want to find last 20 days data .
$this->loadModel('User');
//$this->User->recursive=-1;
$data=$this->User->find('all', array('recursive' => 0,
'fields' => array('Profile.last_name','Profile.first_name'),'limit' => 20,'order' => array('User.created DESC')));
other wise between two dates
$start = date('Y-m-d') ;
$end = date('Y-m-d', strtotime('-20 day'));
$conditions = array('User.created' =>array('Between',$start,$end));
$this->User->find("all",$conditions)
You could write a custom method in your model to search between the dates:
function findByDateRange($start,$end){
return $this->find('all',array('date >= '.$start,'data >= .'$end));
}
As far as validating, you could use the model's beforeValidate() callback to validate the two dates. More info on this here.
function beforeValidate(){
if(Validation::date($this->data['Model']['start_date'])){
return false;
}
if(Validation::date($this->data['Model']['end_date'])){
return false;
}
return parent::beforeValidate();
}
Does that answer your question?