I have been busy with the cakePHP framework for a couple of months now and I really love it. At the moment I'm working on a very new project and it does the job like it should (I think ...) but I feel uncomfortable with some code I wrote. In fact I should optimize my paginate conditions query so I get immediately the right results (right now I manipulate the result set by a bunch of Set::extract method calls.
I'll sketch the relevant aspects of the application. I have a model 'Site' who has a hasMany relationship with the model 'SiteMeta'. This last table looks as follow: id, site_id, key, value, created.
In this last model I record several values of the site at various periods. The name of the key I want to store (e.g. alexarank, google pagerank, ...), and off course also the value. At a given interval I let my app update this database so I can track evolution of this values.
Now my problem is this.
On the overview page of the various websites (controller => Sites, action => index) I'd like to show the CURRENT pagerank of the website. Thus I need one exact SiteMeta record where the 'created' field is the highest and the value in 'key' should be matching the word 'pagerank'. I've tried several things I read on the net but got none of them working (containable, bindmodel, etc.). Probably I'm doing something wrong.
Right now I get results like this when I do a $this->paginate
Array
(
[0] => Array
(
[Site] => Array
(
[id] => 1
[parent_id] => 0
[title] => test
[url] => http://www.test.com
[slug] => www_test_com
[keywords] => cpc,seo
[language_id] => 1
)
[SiteMeta] => Array
(
[0] => Array
(
[id] => 1
[site_id] => 1
[key] => pagerank
[value] => 5
[created] => 2010-08-03 00:00:00
)
[1] => Array
(
[id] => 2
[site_id] => 1
[key] => pagerank
[value] => 2
[created] => 2010-08-17 00:00:00
)
[2] => Array
(
[id] => 5
[site_id] => 1
[key] => alexa
[value] => 1900000
[created] => 2010-08-10 17:39:06
)
)
)
To get the pagerank I just loop through all the sites and manipulate this array I get. Next I filter the results with Set::extract. But this doens't feel quite right :)
$sitesToCheck = $this->paginate($this->_searchConditions($this->params));
foreach($sitesToCheck as $site) {
$pagerank = $this->_getPageRank($site['Site']);
$alexa = $this->_getAlexa($site['Site']);
$site['Site']['pagerank'] = $pagerank;
$sites[] = $site;
}
if (isset($this->params['named']['gpr']) && $this->params['named']['gpr']) {
$rank = explode('-', $this->params['named']['gpr']);
$min = $rank[0];$max = $rank[1];
$sites = Set::extract('/Site[pagerank<=' . $max . '][pagerank>=' . $min .']', $sites);
}
$this->set(compact('sites', 'direction'));
Could you guys please help me to think about a solution for this? Thanks in advance.
Thanks for the contributions. I tried these options (also something with bindmodel but not working also) but still can't get this to work like it should be. If I define this
$this->paginate = array(
'joins'=> array(
array(
'table'=>'site_metas',
'alias'=>'SiteMeta',
'type' =>'inner',
'conditions' =>array('Site.id = SiteMeta.site_id')
)
),
);
I get duplicate results
I have a site with 3 different SiteMeta records and a site with 2 different record.
The paginate method returns me 5 records in total. There's probably an easy solution for this, but I can't figure it out :)
Also I tried to write a sql query myself, but seems I can't use the pagination magic in that case. Query I'd like to imitate with pagination options and conditions is the following. The query returns exactly as I would like to get.
$sites = $this->Site->query('SELECT * FROM sites Site, site_metas SiteMeta WHERE SiteMeta.id = (select SiteMeta.id from site_metas SiteMeta WHERE Site.id = SiteMeta.site_id AND SiteMeta.key = \'pagerank\' order by created desc limit 0,1 )');
As you are trying to retrieve data in a hasMany relationship, cakephp doesn't join the tables by default. If you go for joins you can do something like:
$this->paginate = array(
'joins'=>array(
array(
'table'=>'accounts',
'alias'=>'Account',
'type' =>'inner',
'conditions' =>array('User.id = Account.user_id')
)
),
'conditions'=> array('OR' =>
array(
'Account.name'=>$this->params['named']['nickname'],
'User.id' => 5)
)
);
$users = $this->paginate();
$this->set('users',$users);
debug($users);
$this->render('/users/index');
You have to fit this according to your needs of course. More on joins, like already mentioned in another answer.
Edit 1: This is because you are missing the second 'conditions'. See my code snippet. The first 'conditions' just states where the join happens, whereas the second 'conditions' makes the actual selection.
Edit 2: Here some info on how to write conditions in order to select needed data. You may want to use the max function of your rdbms on column created in your refined condition.
Edit 3: Containable and joins should not be used together. Quoted from the manual: Using joins with Containable behavior could lead to some SQL errors (duplicate tables), so you need to use the joins method as an alternative for Containable if your main goal is to perform searches based on related data. Containable is best suited to restricting the amount of related data brought by a find statement. You have not tried my edit 2 yet, I think.
Edit 4: One possible solution could be to add a field last_updated to the table Sites. This field can then be used in the second conditions statement to compare with the SiteMeta.created value.
Try something like this:
$this->paginate = array(
'fields'=>array(
'Site.*',
'SiteMeta.*',
'MAX(SiteMeta.created) as last_date'
),
'group' => 'SiteMeta.key'
'conditions' => array(
'SiteMeta.key' => 'pagerank'
)
);
$data = $this->paginate('Site');
Or this:
$conditions = array(
'recursive' => 1,
'fields'=>array(
'Site.*',
'SiteMeta.*',
'MAX(SiteMeta.created) as last_date'
),
'group' => 'SiteMeta.key'
'conditions' => array(
'SiteMeta.key' => 'pagerank'
)
);
$data = $this->Site->find('all', $conditions);
If that does not work check this and this. I am 100% sure that it is possible to get the result you want with a single query.
Try something like this (with containable set up on your models):
$this->Site->recursive = -1;
$this->paginate = array(
'conditions' => array(
'Site.title' => 'title') //or whatever conditions you want... if any
'contain' => array(
'SiteMeta' => array(
'conditions' => array(
'SiteMeta.key' => 'pagerank'),
'limit' => 1,
'order' => 'SiteMeta.created DESC')));
I use containable so much that I actually have this in my app_model file so it applies to all models:
var $actsAs = array('Containable');
Many thinks to all who managed to help me through this :)
I got it fixed after all hehe.
Eventually this has been the trick for me
$this->paginate = array(
'joins'=> array(
array(
'table'=>'site_metas',
'alias'=>'SiteMeta',
'type' =>'inner',
'conditions' => array('Site.id = SiteMeta.site_id'))
),
'group' => 'Site.id',
'contain' => array(
'SiteMeta' => array(
'conditions' => array(
'SiteMeta.key' => 'pagerank'),
'limit' => 1,
'order' => SiteMeta.created DESC',
)));
$sites = $this->paginate();
Related
I am trying to get messages with their corresponding users, in order to display a chat list of profiles with their last message in chronological order.
array(
0 => array(
'Recepient' => array(
'id' => ...
'name' => ...
...
),
'Message' => array(
'content' => ...
'created' => ...
...
)
),
1 => ...
)
and in order to retrieve the results, I've written this find() method:
$msgs = $this->Message->find('all', array(
'group' => array('Recepient.id'),
'order'=>'Message.created DESC',
'conditions'=>
array(
'OR'=> array(
array('recepient_id'=>$pid),
array('sender_id' => $pid)
)
)
));
What I have:
message with corresponding "Recepient",
in chronological order
The problem:
the query DOES NOT retrieve the most recent message from $recepient_id/$sender_id combination.
So instead of list of users with the last message, I have a list of users with a message. What's wrong with my query? Thanks for help!
METHOD 2 results
I've created "chat_id" field in the database which is basically recepient_id+sender_id sorted alphabetically (because if user1 send user2 a message user1 is sender, later when user2 responds, he becomes the sender, so sorting will ensure two users will always have the same chat_id).
Than I added DISTINCT to the query:
$this->Message->recursive = 0;
$msgs = $this->Message->find('all', array(
'fields' => array('DISTINCT Message.chat_id','Message.*','Recepient.*'),
'order'=>'Message.created DESC',
'conditions'=>
array(
'OR'=> array(
array('recepient_id'=>$pid),
array('sender_id' => $pid)
)
)
));
and it does NOT work! I am now getting multiple messages for the same conversation.
If I remove Message fields and Recipient fields from the query, I get correct amount of "chats".
'fields' => array('DISTINCT Message.chat_id'),
but that's not the solution.
CakePHP version 2.7.0
MySQL DB
METHOD 3 results
$msgs = $this->Message->find('all', array(
'order'=>'Message.created DESC',
'fields' => 'recepient_id, content, max(Message.created) as max_created',
'group'=>'recepient_id',
// 'contain' => array('Recepient'),
'conditions'=>array( 'chat_id' => $chats )
));
I gave up on single-find method to resolve this, so now 1.I am getting list of chats, 2.I want to find the last message from each chat.
Acording to http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/ my find query should work. What happens is
(int) 0 => array(
'Message' => array(
'recepient_id' => '55e6d764-1444-4aad-a909-9042f07f76da',
'content' => '1st msg',
'created' => '2015-09-20 18:24:17',
'created_nice' => '2 hours ago'
),
(int) 0 => array(
'max_created' => '2015-09-20 18:24:28'
)
),
the field max(Message.created) is indeed showing the latest message from conversation but the 0=>Message array is for different message! As you can see $results[0]['Message']['created'] time and $results[0][0]['max_created'] are different!
Finally! Working solution:
$db = $this->Message->getDataSource();
$chats = $db->fetchAll(
'select id from (select * from messages order by created desc) Message
where recepient_id = "'.$pid.'" or sender_id = "'.$pid.'"
group by `chat_id`'
);
Above code will retrieve all messages, following the requirements. You can
a) SINGLE QUERY add mysql JOIN to include associated model (we keep the code single-query neat)
b) TWO QUERIES BUT SMARTER easier method, to select just "ids" of the messages and create another query that will have cake's build in containable behaviour. This might be better also because Behaviours will be applied.
You will have array tree as a result, to dig out the actual ids for next query use following:
$chats = Set::extract("/Message/id",$chats);
c) translate the solution to CakePHP query builder... :) up for a challenge?
I have few tables which are connected (hasMany) like a tree: 1 -> 2 -> 3.
There are few records in table 1, table 2 and 3.
I'm using CakePHP to fetch all data from table 1 with connected table 2, which is connected with table 3.
However few records in table 1 don't have any connected records in table 2. The same is in table 2, some records don't have connected records in table 3.
For the second situation scripts work fine. I get something like this:
1 -> 2 -> empty. But in the first situation, when data looks similar to: 1-> empty -> empty I get errors that table 3 doesn't exist.
Is there any solution to skip this errors and get pretty formatted association table as return to my query?
$options = array(
'conditions' => array(
'Table1.id' => $table1_ids
),
'contain' => array(
'Table2' => array(
'conditions' => array(
'id' => $table2_ids
),
'Table3' => array(
'conditions' => array(
'date_end >' => date('Y-m-d H:i:s')
),
'fields' => array('id'),
),
'fields' => array()
),
),
'fields' => array('id', 'name')
);
$this->Table1->recursive = -1;
$table1 = $this->Table1->find('all', $options);
It's not really cakephp to blame here but your lack of understanding what contain does.
What you're looking for are left joins, which is actually hinted in the documentation of contain.
In my app I have a Burger model, a Joint model, and a Location model.
Joints have many Burgers, and have many Locations.
What I want to do is filter Burgers two ways: based on a rating, and also bases on whether they belong to a Joint that has a Location that matches a condition, in this case, a city.
Here's what I've tried, but she no workey:
$burgers = $this->Burger->find('all', array(
'conditions' => array('Burger.official_rating !=' => 0),
'contain' => array(
'Joint',
'Joint.Location.conditions' => array(
'Location.city' => 'Toronto'
)
),
'order' => array('Burger.official_rating DESC'),
'limit' => 10
));
Joint.Location.conditions seems to have no effect.
Edit: now using joins, but am unsure if this is the most efficient way (notice, I had to add a Contain clause, to make sure the associated data was returned in the results).
$options['joins'] = array(
array('table' => 'joints',
'alias' => 'Joint1',
'type' => 'inner',
'conditions' => array(
'Joint1.id = Burger.joint_id'
)
),
array('table' => 'locations',
'alias' => 'Location2',
'type' => 'inner',
'conditions' => array(
'Location2.joint_id = Burger.joint_id'
)
)
);
$options['conditions'] = array(
'Location2.city' => 'Toronto',
'Burger.official_rating !=' => 0
);
$options['order'] = array(
'Burger.official_rating DESC',
);
$options['limit'] = 10;
$options['contain'] = array(
'Joint','Joint.Location'
);
$burgers = $this->Burger->find('all', $options);
Problem:
This is a very common issue people have, but is easily remedied as soon as you understand that you cannot limit the main find model based on conditions against it's contained items.
The reason you can't is because doing a contain actually creates separate queries on the tables, as opposed to JOIN, which creates a single query.
Solution:
Your best bet in this case is to use CakePHP JOINs.
In some cases (not yours), the alternate to using JOINs is to change which model you're doing the query on. If you only need one condition, then you could change the find to the model with the condition instead of the one within the contain and contain in the reverse order.
So this is what I am trying to do.
My table say(Courses) has multiple entries with same id.
When I get the data from paginate it shows all the records. So if I have 3 records with Id 5 it will show record number 5 three times.
Now What I want is that it should show the record only once.
I searched online but can't find anything.
If anyone has come across such problem and found a solution to it please let me know.
Thanks,
I came across your problem, as I had a similar problem. David Z's solution did not work for me, but I did find that the group variable in $paginate worked for me.
So using your code sample above, this is how I'd think it should work.
$paginate = array(
'Courses' => array(
'limit' => 20,
'fields' => array('Courses.id'),
'conditions' => $cond,
'group' => array('Courses.id'),
'order' => array('Courses.id' => 'asc')
)
);
To hopefully shed some more light on the solution that worked for me, I have Systems that belong to Companies. I wanted to get a list of the unique companies, for the systems I have. This is the exact code I used, that worked for me
$this->paginate = array ('fields' => array ('Company.*'),
'order' => array('Company.name' => 'ASC'),
'group' => array('Company.id'));
$this->set('companies', $this->paginate($this->Company->System));
Hope this has helped
Looking at the CakePHP cookbook, the documentation for pagination shows that you can override the $paginate member. Behind the scenes, this similar to passing in the parameters for your model's find('all'). Maybe try setting parameter to explicitly return the filds that you are interested with the distinct keyword to narrow down the values you need?
class RecipesController extends AppController {
var $paginate = array(
'fields' => array('Model.field1', 'DISTINCT Model.field2')
);
}
So here is how my paginate variable looks like:
var $paginate = array(
'Courses' => array(
'limit' => 20,
'page' => 1,
'order' => array(
'Courses.id' => 'asc')
),
);
The condition variable looks something like this:
$cond = array("Courses.id LIKE "=>$this->data['id_search'],
"Courses.length LIKE "=>$this->data['length_search'],
"Courses.marks LIKE "=>$this->data['marks']
);
And this is how I am calling paginate.
$data = $this->paginate('CdmaRfReport',$cond);
I tried doing
$paginate = array(
'Courses' => array(
'limit' => 20,
'fields' => array('DISTINCT Courses.id'),
'page' => 1,
'conditions' => $cond,
'group' => array('id'),
'order' => array(
'Courses.id' => 'asc')
)
);
It doesn't seem to help.
I also tried
$cond = array("DISTINCT Courses.id "=>$this->data['id_search'],
"Courses.length LIKE "=>$this->data['length_search'],
"Courses.marks LIKE "=>$this->data['marks']
);
Even this errors out
I might be something wrong. But I am not able to figure it out.
Any suggestions please let me know.
Cake handles pagination of a model with a simple $this->paginate(), but what should I use if I want to paginate a array of values?
The Scenario is like this:
$this->set('sitepages', $this->paginate());
This code in my index() returns an array like
Array
(
[0] => Array
(
[Sitepage] => Array
(
[id] => 13
[name] => Home
[urlslug] => home
[parent_id] => 1
[page_title] => Welcome to KIAMS, Pune
[order] => 1
)
)
[1] => Array
(
[Sitepage] => Array
(
[id] => 26
[name] => About Us
[urlslug] => aboutus
[parent_id] => 1
[page_title] =>
[order] => 2
)
)
[2] => Array
(
[Sitepage] => Array
(
[id] => 27
[name] => Overview of KIAMS
[urlslug] => aboutus/overview
[parent_id] => 26
[page_title] =>
[order] => 2
)
)
I retrieved the same data using $this->Sitepage->find('all') and then performed some manipulations as required and form a array which is very similar to the above one, but the ordering gets changed. I want to paginate this new array and pass it to the view. I tried
$this->set('sitepages',$this->paginate($newarray))
But the data is not getting paginated. Can some one please help with paginating the $newarray in CakePHP?
To paginate in CakePHP you need to pass select conditions to the paginate() call.
Other data manipulation should be done in afterFind(), in your model file.
If you don't need these changes to be done in every single retrieval, you might as well consider creating a new model file pointing to the very same table as the current one, and adding an afterFind() method to that new file.
I've just dealt with this same problem...
I found the only way is to use the paginate() function to handle all the logic, rather than passing it a custom array. However, this isn't as bad as it seems:
$this->paginate = array(
'limit' => 2,
'order' => array(
'Report.name' => 'asc'
),
'conditions' => array(
'Account.id' => $this->foo()
),
);
$reports = $this->paginate();
In this example, I'm paginating Reports - but some Reports will not be included, depending on which Account they belong to (Account has some relationship with Report, hasmany, etc.).
By writing $paginate inside your action, you can use a custom callback for the conditions array. So function foo() can be written in your controller (or shoved to model) and returns an array of valid Account IDs.
I found I could easily rewrite my custom logic in this function - keeping both me and the Cake paginator happy.
Hope that helps!
I'm using cakephp version 1.3 and it seems this one is working:
at controller:
$result = $this->Paginate('Event');
$results = $this->Task->humanizeEvent($result);
$this->set('events', $results);
and it seems to display as a normal paginated array, at the view (setup your pagination in view as normal).
The humanizeEvent function just edits a field on the results array, to make it a sentence based on other fields inside the array, and it seems to work properly.
$this->paginate($newarray) is the wrong way to paginate. The first parameter cannot be an array. It must be a model name. You may want to study pagination setup from the manual. This will order alphabetically:
var $paginate = array(
'limit' => 25,
'order' => array(
'Sitepage.name' => 'asc'
)
);
$this->set('sitepages', $this->paginate('Sitepage'));
I create a component checking the paginator code..
Is not the best thing, but It work for me.....
Controller
$slicedArray = array_slice($fullArray,($page - 1) * $this->PaginatorArray->limit ,$this->PaginatorArray->limit)
$this->params['paging'] = $this->PaginatorArray->getParamsPaging('MyModel', $page, $total,count($slicedArray));
$this->helpers[] = 'Paginator';
Component
<?php
/* SVN FILE: $Id$ */
/**
* Pagination Array Component class file.
* #subpackage cake.cake.libs.view.helpers
*/
class PaginatorArrayComponent {
var $limit = 40;
var $step = 1;
function startup( & $controller){
$this->controller = & $controller;
}
function getParamsPaging($model, $page, $total, $current){
$pageCount = ceil($total / $this->limit);
$prevPage = '';
$nextPage = '';
if($page > 1)
$prevPage = $page - 1;
if($page + 1 <= $pageCount)
$nextPage = $page + 1;
return array(
$model => array(
'page' => $page,
'current' => $current,
'count' => $total,
'prevPage' => $prevPage,
'nextPage' => $nextPage,
'pageCount' => $pageCount,
'defaults' => array(
'limit' => $this->limit,
'step' => $this->step,
'order' => array(),
'conditions' => array(),
),
'options' => array(
'page' => $page,
'limit' => $this->limit,
'order' => array(),
'conditions' => array(),
)
)
);
}
}
?>
There was a bug in find("count") and it returned incorrect count if the query resulted in records for only in group. This has been fixed click here