cakephp - sorting by a second level association in paginate - php

I am playing around with a quotes database relating to a ski trip I run. I am trying to list the quotes, but sort by the person who said the quote, and am struggling to get the paginate helper to let me do this.
I have four relevant tables.
quotes, trips, people and attendances. Attendances is essentially a join table for people and trips.
Relationships are as follows;
Attendance belongsTo Person hasMany Attendance
Attendance belongsTo Trip hasMany Attendance
Attendance hasMany Quote belongs to Attendance
In the QuotesController I use containable to retrieve the fields from Quote, along with the associated Attendance, and the fields from the Trip and Person associated with that Attendance.
function index() {
$this->Quote->recursive = 0;
$this->paginate['Quote'] = array(
'contain' => array('Attendance.Person', 'Attendance.Trip'));
$this->set('quotes', $this->paginate());
}
This seems to work fine, and in the view, I can echo out
foreach ($quotes as $quote) {
echo $quote['Attendance']['Person']['first_name'];
}
without any problem.
What I cannot get to work is accessing/using the same variable as a sort field in paginate
echo $this->Paginator->sort('Name', 'Attendance.Person.first_name');
or
echo $this->Paginator->sort('Location', 'Attendance.Trip.location');
Does not work. It appears to sort by something, but I'm not sure what.
The $quotes array I am passing looks like this;
Array
(
[0] => Array
(
[Quote] => Array
(
[id] => 1
[attendance_id] => 15
[quote_text] => Hello
)
[Attendance] => Array
(
[id] => 15
[person_id] => 2
[trip_id] => 7
[Person] => Array
(
[id] => 2
[first_name] => John
[last_name] => Smith
)
[Trip] => Array
(
[id] => 7
[location] => La Plagne
[year] => 2000
[modified] =>
)
)
)
I would be immensely grateful if someone could suggest how I might be able to sort by the the first_name of the Person associated with the Quote. I suspect my syntax is wrong, but I have not been able to find the answer. Is it not possible to sort by a second level association in this way?
I am pretty much brand new with cakephp so please be gentle.
Thanks very much in advance.

I've had the similar problem awhile back. Not with sort though. Try putting the associated table in another array.
echo $this->Paginator->sort('Name', 'Attendance.Person.first_name');
change to:
echo $this->Paginator->sort('Name', array('Attendance' => 'Person.first_name'));
Hope this helps

i'm also looking for help with this.
so far i've found that you can sort multi level associations in controller's pagination options after using the linkable plugin https://github.com/Terr/linkable.
but it breaks down when you try to sort form the paginator in the view. i'm using a controller for magazine clippings. each clipping belongs to an issue and each issue belongs to a publication.
$this->paginate = array(
"recursive"=>0,
"link"=>array("Issue"=>array("Publication")),
"order"=>array("Publication.name"=>"ASC",
"limit"=>10);
after debugging $this->Paginator->params->paging->Clipping in the view, you can see that the sort is described in two separate places, "defaults" and "options". the sort info needs to be present in both for it to work in the view.
here is after setting order in controller:
[Clipping] => Array
(
[page] => 1
[current] => 10
[count] => 6685
[prevPage] =>
[nextPage] => 1
[pageCount] => 669
[defaults] => Array
(
[limit] => 10
[step] => 1
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
[Publication.name] => ASC
)
[conditions] => Array
(
)
)
[options] => Array
(
[page] => 1
[limit] => 10
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
[Publication.name] => ASC
)
[conditions] => Array
(
)
)
)
and here is after using $this->Paginator->sort("Publication","Publication.name");.
notice the options array is empty.
[Clipping] => Array
(
[page] => 1
[current] => 10
[count] => 6685
[prevPage] =>
[nextPage] => 1
[pageCount] => 669
[defaults] => Array
(
[limit] => 10
[step] => 1
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
[Publication.name] => DESC
)
[conditions] => Array
(
)
)
[options] => Array
(
[page] => 1
[limit] => 10
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
)
[conditions] => Array
(
)
)
does one really need to modify the paginator class to make this work?
UPDATE:
i found out the problem:
in the core cake controller paginator merges default and options to create the find query.
but the options array is empty when using linkable to sort. because options is listed after default it overrides default and the empty array replaces the default array of options.
solution to this is extending the paginate function inside of app_controller.php and unsetting the options array order value if it is empty:
(line 1172 in cake/libs/controller/controller.php)
if(empty($options["order"])){
unset($options["order"]);
}
then the options will not be overwritten by thte blank array.
of course this should not be changed inside of controller.php, but put it in app_controller.php and move it to your app folder.

On CakePHP 3 this problem can be solved by adding 'sortWhitelist' params to $this->paginate on your controller.
$this->paginate = [
// ...
'sortWhitelist' => ['id', 'status', 'Attendance.Person.first_name']
];
And then in your view:
echo $this->Paginator->sort('Name', 'Attendance.Person.first_name');
This is noted in the docs:
This option is required when you want to sort on any associated data, or computed fields that may be part of your pagination query:
However that could be easily missed by tired eyes, so hope this helps someone out there!

Related

php push an array into an existing array by it's index

I'm having trouble with putting this question to words so ill just use a simple example, hope the title sortof got my problem across.
I'm creating a blog site where I can create blogposts and people can post comments. This is all saved in JSON except for login details which are saved in MySQL.
Now saving the blogposts go fine but I'm now trying to save comments.
Lets say the blogpost array looks like this:
Array
(
[0] => Array
(
[id] => 0
[title] => first blogpost
[content] => blogpost text
)
[1] => Array
(
[id] => 1
[title] => second blogpost
[content] => blogpost 2 text
)
)
Now someone writes a comment on 'second blogpost', I save it into an array like this(user taken from MySQL):
Array
(
[user] => myusername
[comment] => first post was better!
)
Now I want to merge them like this:
Array
(
[0] => Array
(
[id] => 0
[title] => first blogpost
[content] => blogpost text
)
[1] => Array
(
[id] => 1
[title] => second blogpost
[content] => blogpost 2 text
[comments] => Array
(
[user] => myusername
[comment] => first post was better!
)
)
)
I tried searching for a while and I'd expect this to be somewhere on the site already but I can't find it. I tried a couple variations of array_push and array_merge but it always ended up replacing the relevant blogpost instead of adding onto it.
EDIT: Someone noted the new array can't just float around, I think it's better now.
If you had any related key between posts and comments ( like having post_id in comment array ) that would make more sense to merge/put them.
I assume that's your blogpost
Array
(
[0] => Array
(
[id] => 0
[title] => first blogpost
[content] => blogpost text
)
[1] => Array
(
[id] => 1
[title] => second blogpost
[content] => blogpost 2 text
)
)
And your comments should be like:
Array
(
[user] => myusername
[comment] => first post was better!
[post_id] => 1
)
That way, you would be able to find the matched blogpost.
But, outside of your data structure, here is an example to merge an item into an element of an array of array.
A nested loop example.
foreach($posts as &$post){
foreach($comments as $comment){
if($post['id'] == $comment['post_id']){
$post['comments'][] = $comment;
}
}
}
the key here is sending each reference of the element into loop by &$post and then just manipulate them in loop.
Working with indexed arrays. (Like you already have index names as post_id and a comments index as an empty array)
foreach($comments as $comment){
$posts[$comment['post_id']]['comments'][] = $comment;
}
When the blogpost is updated, I assume you can get the id of that blogpost.
Then you can check if your data structure already has a key "comments". If it does not, add the key and create an array containing the comment and the user as the first array.
If it already exists, add a new array with the user and the comment so that there can be multiple comments for each blogpost.
For example using array_map:
$blogPosts = array_map(function ($blogPost) use ($blogPostId, $comment) {
if ($blogPost["id"] === $blogPostId) {
isset($blogPost["comments"]) ? $blogPost["comments"][] = $comment : $blogPost["comments"] = [$comment];
return $blogPost;
}
return $blogPost;
}, $blogPosts);
Php demo
So I fixed it after a bit of thinking
This is the final structure:
Array
(
[0] => Array
(
[id] => 0
[title] => 1st post
[content] => 1st post works!
[date] => 21-01-2019
[comments] => Array
(
[0] => Array
(
[user] => Me
[comment] => hey 1
[date] => 12:02 21-01-2019
)
[1] => Array
(
[user] => Me
[comment] => hey 2
[date] => 12:03 21-01-2019
)
)
)
)
I added a timestamp because of a suggestion here. It's also a simplified version of what I actually use, I tried adding many more comments and on multiple posts which both work.
This is the code, I should mention the ID is in the URL and it's saved as JSON:
$filename = file.json;
$currentArray = json_decode(file_get_contents($filename), true);
$comment = $_POST['comment'];
$username = $_SESSION['username'];
$date = date("H:i d-m-Y");
$id = $_GET['id'];
Pretty straightforward so far, here is how the array is created:
$currentArray[$id]["comments"][] = array (
'user' => $username,
'comment' => $comment,
'date' => $date
);
[$id] saves it to the correct post, ["comments"] saves it to the comments key(or creates it) and the last [] gives every comment a different index inside the ["comments"].
$newJSON = json_encode($currentArray, JSON_PRETTY_PRINT);
file_put_contents($filename, $newJSON);
And lastly encoding it and saving it to JSON.
Hope this helps someone.

Turning associative JSON array into nested SQL WHERE clause

I'm new to backend programming and CodeIgniter is my first framework. This is new to me. Right now, the thing that I'm working is that I need to create a query based on the JSON encoded data saved at my table.
I have here a segmentation form that should build a query to look/search for the customers profiles based on the condition set
Segmentation form
$segment = array(
...
'filters' => json_encode($post['filters']), //to insert the filters in json
...
);
if ($this->model->addSegments($segment))
The filters saved in the database are like this :
{"data":[{"data":{"condition":"0","attribute":"gender","sign":"=","value":"Male"},"type":"condition","operator":"0","filter":{"operator":"0"}}],"type":"match","operator":"0"}
Looking for the answers, I found this forum and I relate it to the problem that I had: http://www.codingforums.com/php/202207-turning-json-object-into-nested-sql-where-clause.html
Calling the same like function (solution at the link) to the controller using this lines:
$filter = $this->model->getSegmentsFilters($id_segment);
$filters = $this->model->parseFilterToQuery(json_decode($filter['filters'], true));
With the parameter TRUE from the json_decode, It throws the filters in array and using print_r shown like this:
Array (
[0] => Array (
[data] => Array (
[condition] => 0
[attribute] => firstname
[sign] => =
[value] => John )
[type] => condition
[operator] => 0
[filter] => Array (
[operator] => 0 )
)
[1] => Array (
[data] => Array (
[condition] => 0
[attribute] => gender
[sign] => =
[value] => Male )
[type] => condition
[operator] => condition
[filter] => Array (
[operator] => 0 )
)
)
the $sql inside if(is_array) doesn't show any value to concatenate with. I want it to concatenate with another query as a WHERE clause like this idea.
$sql = "SELECT * FROM customers WHERE" . parseFilterToQuery($filters);
If you think that the solution/procedure that I came up is correct. My question is that inside the if(is_array) condition,
how should I work on it with associative JSON array?

PHP search/filter array with given conditions, recursively

I am getting data from an api (that I cannot query agains, just get lump of data), and then I need to query against those data like I would do using database. Only It would be great if I could do it recursively.
Data example
[0] => Array
(
[id] => 1
[url] => https://domain.com/api/1.0/item/1/
[name] => some_item
[category] => some category
[created_by] => Array
(
[id] => 1
[screen_name] => tomino
)
[current_user_domain_access] => Array
(
[is_active] => 1
[is_administrator] => 1
)
[alerts_enabled] => 0
)
(much shortened version)
I receive an array of objects like that and then I need to select/filter/search by values.
Something like this
SomeModel::find(['category'=>'some category','current_user_domain_access' => ['is_administrator' => 1]]);
Is that something that would be possible in PHP? I was thinking about flattening the array, but then there might be key conflicts
1) select data : You can select data by (array_name->id),(array_name->url) and so on...
2)Filter : add conditions according to requirement
3)search : in_array(),array_search

How to associate models correctly cakephp

I have a scenario where i have templates which has many themes.
Clear enough that my relation will be template hasmany themes
I wanna show the template with number of themes and i am applying this code:
$this->Template->recursive = 2;
$this->Template->bindModel(
array(
'hasMany' =>array(
'TemplateTheme'=>array(
'className'=>'TemplateTheme',
'fields' => 'count(TemplateTheme.id) AS themes'
)
)
),false
);
$this->paginate = array(
'order' => array('Template.modified DESC'),
'limit' =>$limit
);
$template = $this->paginate('Template');
pr($template);die();
but i am getting is
Array
(
[0] => Array
(
[Template] => Array
(
[id] => 1
[name] => churchDesign
[status] => Active
[created] => 2011-10-24 10:37:23
[modified] => 2011-10-25 15:16:46
)
[TemplateTheme] => Array
(
[0] => Array
(
[template_id] => 1
[TemplateTheme] => Array
(
[0] => Array
(
[themes] => 3
)
)
)
)
)
[1] => Array
(
[Template] => Array
(
[id] => 2
[name] => blossoms
[status] => Active
[created] => 2011-10-19 00:00:00
[modified] => 2011-10-24 14:05:27
)
[TemplateTheme] => Array
(
)
)
)
The problem here is it is counting all the themes in first array(1st template).see [TemplateTheme] => Array
(
[0] => Array
(
[themes] => 3
)
the third theme actually belongs to the 2nd template.The relation is
template_id is in themes table to represent each theme for a template.
Template 1 has 2 themes and template 2nd has one theme and currently its showing all the three themes in the first template.
i want it like this way
templatename1
count of themes 1st template
template name 2
count of themes of 2nd template
Please tell me the right way to do this.
If you wanted to get the count of the Themes per each Template, the most elegant way is to use counterCache.
Another way is to use Containable behavior, which fetches only the related data which you wish, and then count the fetched data array in PHP.
After you attach the containable behavior to the Template model, some code like this should work:
$allTemplates = $this->Templates->find('all', array(
'contain' => 'TemplateTheme.name'
));
foreach($allTemplates as $template){
$currentThemeCount = count($template['TemplateTheme']);
foreach($template['TemplateTheme'] as $theme){
echo $theme['name'];
}
}
Hope this helps someone along the way.
you can count the themes array like
foreach($template as $t){
count($t['TemplateTheme'])
}
hope this helps
You should be using the find('count') method instead of using a virtual 'count' field like you're doing now. For example:
$allTemplates = $this->Template->find('all');
foreach($allTemplates as $data) {
$templateCount = $this->Template->TemplateTheme->find('count', array(
'conditions' => array(
'TemplateTheme.template_id' => $data['Template']['id']
)
));
// $templateCount should now contain the count of this specific template. Foreach will continue recursion for all others.
}

Cakephp: question about saveall() with multiselect

I'm wondering what the cleanest way is to implement a cakephp form where 1 control is a multi-select and the rest are text fields or single-selects, and then the data is inserted as multiple rows with a saveall(). So for example a form is selected with these values:
textfield A
value=Foo
mulit-select B
values=US,Mexico,Canada
single=select C
value=10
and so I want to insert these rows into the database with a saveall():
Foo,US,10
Foo,Mexico,10
Foo,Canada,10
Now I know in the add view I can use this format for the input statement:
input('Model.0.field1',...)
but I'm wondering if I can mix that in that same form with inputs formatted like
input('Model.field2',....).
Update:
When I mix and match the single-select and multiple-select controls, the form data gets submitted like this:
Array
(
[Alert] => Array
(
[schedule_id] => 75
[user_id] => 6
[0] => Array
(
[frequency] => Array
(
[0] => WEEKLY
[1] => MONTHLY
)
)
[limit_value] => .03
[limit_adjustment] => 0
[type] => LIMIT
[disabled] => 0
)
)
I tried passing that data into saveall() but it treats it like a single record.
Update2: I think saveAll() requires that the multiple rows of data be formatted like this:
Array
(
[Article] => Array(
[0] => Array
(
[title] => title 1
)
[1] => Array
(
[title] => title 2
)
)
)
So it looks like after the submit I'm going to need some javascript code that will restructure the array.
I have something that works... I'm not sure if it takes full advantage of all of cake's "automagic" capabilities, but I don't think it's too convoluted.
So I just added the following code to my controller's add function:
if (!empty($this->data)) {
//debug($this->data, true);
/* begin custom code */
$multiselect = $this->data['Alert']['entity_id'];
$tmp2 = array();
foreach ($multiselect as $item)
{
$tmp = $this->data['Alert'];
$tmp['entity_id'] = $item;
array_push($tmp2,$tmp);
}
$this->data['Alert'] = $tmp2;
debug($this->data,true);
/* end custom code */
$this->Alert->create();
//restructure data
if ($this->Alert->saveAll($this->data['Alert'])) {
$this->Session->setFlash(__('The alert has been saved', true));
//$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The alert could not be saved. Please, try again.', true));
}
and that converts my data to this:
Array
(
[Alert] => Array
(
[0] => Array
(
[schedule_id] => 74
[entity_id] => 1
[user_id] => 6
[frequency] => HOURLY
[limit_value] => .02
[limit_adjustment] => 0
[type] => LIMIT
[disabled] => 1
)
[1] => Array
(
[schedule_id] => 74
[entity_id] => 2
[user_id] => 6
[frequency] => HOURLY
[limit_value] => .02
[limit_adjustment] => 0
[type] => LIMIT
[disabled] => 1
)
)
)

Categories