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
Related
I am making simple pagination which using this code:
$paginate = array(
'limit' => 30,
'fields' => array('DISTINCT Doctor.id','Doctor.*'),
'order' => array('Doctor.id' => 'desc'),
'joins' => array(
array('table' => 'doctors_medical_degrees',
'alias' => 'DoctorsMedicalDegree',
'type' => 'INNER',
'conditions' => array(
'Doctor.id = DoctorsMedicalDegree.doctor_id',
)
),
),
'recursive' => -1,
);
$this->Paginator->settings = $paginate;
$data = $this->Paginator->paginate('Doctor');
Now the problem is I am using Inner join so for Distinct result I am using Distinct Doctor.id, but the cakephp when doing query for pagination the count query not including Distinct Doctor.id
'query' => 'SELECT COUNT(*) AS `count` FROM `pharma`.`doctors` AS `Doctor` INNER JOIN `pharma`.`doctors_medical_degrees` AS `DoctorsMedicalDegree` ON (`Doctor`.`id` = `DoctorsMedicalDegree`.`doctor_id`)'
as you can see No
COUNT(DISTINCT Doctor.id)
so pagination return more number of result which it can actually return for
The problem is that the paginator doesn't pass the fields to the find('count') call, so by default it will always count on *.
But even if it would pass the fields, passing an array would make the find('count') call expect that the field to count is passed as a COUNT() expression, ie something like
'fields' => array('COUNT(DISTINCT Doctor.id) as `count`')
However that won't work with the paginator anyways, so what you need is a customized find('count') call.
Custom query pagination to the rescue
See Cookbook > Pagination > Custom Query Pagination for more information.
Custom query pagination is probably your best bet, that way it's totally up to you how counting is being done.
For example you could make use of the extra values passed by the paginator component, that way you could pass the field to count on to the find('count')` call, something like this (untested example code):
class Doctor extends AppModel {
// ...
public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$parameters = compact('conditions');
if($recursive != $this->recursive) {
$parameters['recursive'] = $recursive;
}
if(!empty($extra['countField'])) {
$parameters['fields'] = $extra['countField'];
unset($extra['countField']);
}
return $this->find('count', array_merge($parameters, $extra));
}
}
$this->Paginator->settings = array(
'limit' => 30,
'fields' => array('DISTINCT Doctor.id','Doctor.*'),
// ...
'countField' => 'DISTINCT Doctor.id'
);
$data = $this->Paginator->paginate('Doctor');
This should then create a COUNT query that looks like
SELECT COUNT(DISTINCT Doctor.id) AS `count` ...
I found the solution on CakePHP - Pagination total count differs from actual count when using DISTINCT
Add the public function paginateCount in your model and use the distinct option in your paginator like:
$this->paginate = array('limit' => $limit, 'order' => array('Item.created' => 'ASC', 'Item.code' => 'ASC'),
'fields' => 'DISTINCT Item.*',
'conditions' => $conditions,
'countField' => array('Item.id'),
'joins' => $joins,
'distinct' => 'Item.id',
'contain' => array(
'Image' => array('limit' => 1),
'ItemsTag'
)
);
I have encountered a weird problem, where in the Controller is passing a single record from a table, but the View ends up displaying the entire table.
I have extensive logging and am pretty sure, that the Controller is passing a single record via the $this->set().
Controller (ContactsController : show_list)
$arr_contacts = $this->Contact->find(
'all',
array(
'conditions' => array(
'Contact.state_id' => $i_state_id,
'Contact.city'=> $str_city
),
'fields' => array(
'Contact.id',
'Contact.name',
'Contact.city'
),
'recursive' => -1
)
);
$contacts = $arr_contacts;
$this->log ($contacts, 'debug');
$this->set('contacts', $this->paginate());
$this->log(__FUNCTION__." : ".__LINE__, 'debug' );
Output in log:
Debug: Array
(
[0] => Array
(
[Contact] => Array
(
[id] => 504
[name] => Michael
[city] => New York
)
)
)
Debug: show_list : 303
In the view file (show_list.ctp), I have
<div class="contacts index">
<?php echo print_r($contacts);?>
The view file, prints a list of all records from the table. The SQL dump that cakephp displays, shows that additional SQL calls are being made. However, it isn't clear, where those calls are coming from.
The rest of the controllers and actions seem to be working fine, ruling out any corruption issues.
Has anyone encountered a similar situation before? Any pointers?
You pass the output of the paginate() function to your view, which is different from your own find() call.
If you want the same conditions as with your find(). pass them to paginate() (Tip: put your conditions in an array first)
If you do not need Paginate as it seems you don't since you are only getting a single line of entries from Db, then you should rewrite your $this->set() as such:
$this->set('contacts',$contacts);
If you need Paginate (???), then you need to set you entire function as such:
$this->paginate = array(
'conditions' => array(
'Contact.state_id' => $i_state_id,
'Contact.city'=> $str_city
),
'fields' => array(
'Contact.id',
'Contact.name',
'Contact.city'
),
'recursive' => -1
);
$this->log ($this->paginate, 'debug');
$this->set('contacts', $this->paginate());
$this->log(__FUNCTION__." : ".__LINE__, 'debug' );
I have the following find function:
$this->MyModel->find('all', array('conditions' => array('id' => $id)));
which returns an array of this form
Array
(
[Model1] => Array
(
[Model1] => Array
(
...
...
)
)
[Model2] => Array
(
[0] => Array
(
...
...
)
[1] => Array
(
...
...
)
[2] => Array
(
...
...
)
...
...
...
)
)
How do I alter the the find all to limit how many elements the second model are fetched (Model2)?
I can add conditions => array( 'limit' => 10 ) but that limits the number of elements of the outer array - not Model2 i.e. the second nested array!
Any ideas? Thank you :).
You should use cake's containable behaviour here to limiting the records of your second model. Here's what you can do using containable to achieve this.
$this->Model->contain = array('Model2' => array('limit' => 10));
$this->Model->find('all', array('conditions' => $conditions));
Now you will get maximum of 10 records from Model2.
If this is something that you want to do each time you do a find('all'), you can use the 'limit' parameters in the associations arrays in the model:
public $hasMany = array(
'OtherModel' => array(
'className' => 'OtherModel',
'foreignKey' => 'model_id',
'limit' => 10
)
Alternatively, you could also modify this value just before calling the find() method:
$this->MyModel->hasMany['OtherModel']['limit'] = 10;
$this->MyModel->find('all', array('conditions' => array('id' => $id)));
Hi I want to be able to generate a list using find so that I can use in select helper. but there is a problem. i want too fetch id,name(first + last). so how can I achieve it. I want first_name and last_name to be joined as name . How can I achieve it.
$this->User->find('all',array('fields' => array('first_name','last_name','id')));
I can't use model filters and callback Please suggest me how can I do it in controllers itself.
I think this can be done using the virtualFields and displayField properties in your model.
In your model, define a virtual field for the full name like this:
public $virtualFields = array(
'full_name' => 'CONCAT(User.first_name, " ", User.last_name)'
);
If you now set displayField to full_name you should be able to get a list of your users with the $this->User->find('list') method which you can use without problems with the Form-helper.
public $displayField = 'full_name';
... or:
public $displayField = 'User.full_name';
The id is fetched automatically.
Another solution is to use Cake's Set::combine to build what you need...
$users = $this->User->find('all',array('fields' => array('first_name','last_name','id')));
$user_list = Set::combine($users, '{n}.User.id', array('{0} {1}', '{n}.User.first_name', '{n}.User.last_name'));
Result will look something like:
array(
[2] => 'First Last',
[5] => 'Bob Jones'
)
Here's the documentation link:
http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::combine
To achieve this first go to the model and add this line
public $virtualFields = array('full_name' => 'CONCAT(first_name, " ", last_name)');
and then go to controller file just use the name "full_name" which you put in virtual fields
$this->User->find('all',array('fields' => array('full_name','id')));
It returns name with combined fields
+1 on Tim's answer, however, if you need an alternative, teknoid wrote a nice article about this a long time ago:
http://nuts-and-bolts-of-cakephp.com/2008/09/04/findlist-with-three-or-combined-fields/
In my case, Set::combine was the way to go, since I had to deal with concatenation of fields in associated models, like:
$bancos_enteros = $this->Financiacion->Banco->find('all', array(
'fields' => array('Empresa.codigo_contable','Empresa.nombre_corto', 'Banco.id'),
'order' => array('Empresa.codigo_contable' => 'asc'),
'recursive' => 1
));
$bancos = Set::combine(
$bancos_enteros,
'{n}.Banco.id',
array(
'{0} {1}',
'{n}.Empresa.codigo_contable',
'{n}.Empresa.nombre_corto'
)
);
returning
array(
(int) 14 => '57200002 Caixa',
(int) 15 => '57200003 Sabadell',
(int) 3 => '57200005 BBVA',
(int) 16 => '57200006 Deutsche Bank',
(int) 17 => '57200007 Popular',
(int) 18 => '57200009 March',
(int) 26 => '57200010 Bankinter',
(int) 4 => '57200011 Santander'
)
While
$this->Financiacion->Banco->Empresa->virtualFields = array(
'codigo_nombre' => 'CONCAT(Empresa.codigo_contable,Empresa.nombre_corto)'
);
$this->Financiacion->Banco->virtualFields['codigo_nombre'] = $this->Financiacion->Banco->Empresa->virtualFields['codigo_nombre'];
$bancos = $this->Financiacion->Banco->find('list', array(
'fields' => array('Banco.id','Banco.codigo_nombre'),
'order' => array('Banco.codigo_nombre' => 'asc'),
'recursive' => 1
)
);
returns a SQL error in a following query if I don't delete the virtual fields first:
unset($this->Financiacion->Banco->Empresa->virtualFields);
unset($this->Financiacion->Banco->virtualFields);
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();