Efficient pagination with CakePHP - cakephp

I have an "architectural" problem with CakePHP :p .
I have to paginate queries, that seems easy, I use the $paginate array and the paginate method, but I have many restrictions.
In many methods of my controller I must return different fields of the same Model, and in both cases I have to paginate. This fact forces me to mention all the fields in the $paginate array, and this could cause poor performance when I don't need those fields.
How can I set different paginate rules for different methods in a clean way?
(I thought in using different arrays and assign to $paginate the specific array in runtime, but I want to know if there is an "oficial way" to do it)

if your table doesn't have a massive number of fields, I think it's ok to let cake query all of them. The performance shouldn't be much different.
You can specify different pagination sets:
var $paginate = array(
'Recipe' => array (...),
'Author' => array (...)
);
then $data = $this->paginate('Recipe');
http://book.cakephp.org/view/1232/Controller-Setup

I don't know if this is what you are already doing, but I think this is already clean enough:
function foo() {
$this->paginate['fields'] = array('field_1', 'field_2');
/* rest of the method */
}
function bar() {
$this->paginate['fields'] = array('field_3', 'field_4');
/* rest of the method */
}
If there are fields that you are going to use in all methods you could also do it like this:
var $paginate = array (
'fields' => array('always_need_this', 'also_need_this_always',)
);
function foo() {
array_push($this->paginate['fields'], 'only_in_foo', 'also_only_in_foo');
/* rest of the method */
}
function bar() {
array_push($this->paginate['fields'], 'only_in_bar', 'also_only_in_bar');
/* rest of the method */
}

Related

Pagination in requestAction

I'm building a dynamic view (Page) that consists of multiple elements (widgets) called via $this->element('messages_unread'). Some of these elements need data that is not related to the Page model.
In real life words: my users will be able to construct their own Page by choosing from a multitude of elements ("top 5 posts", "10 unread messages", etc...)
I get the data by calling $this->requestAction(array('controller'=>'events','action'=>'archive') from within the element, the url-variables differ per element .
I'm aware of the fact that requestAction() is expensive and I plan on limiting the costs by proper caching.
The actual question:
My problem is Pagination. When I'm in the Page view and call requestAction('/events/archive') the PaginatorHelper in the Page view will be unaware of the Event model and its paginator variables and $this->Paginator->next() etc... will not work.
How can I implement proper Pagination? I've tried to set the model by calling $this->Paginator->options(array('model'=>'Event')) but that doesn't work.
Do I maybe need to return custom defined Pagination variables in the requestAction and thus construct my own?
Or is there another approach that maybe even avoids requestAction()? And keep in mind here that the requested data is unrelated to the Page.
Kind regards,
Bart
[Edit] My temporary solution but still open for comments/solutions:
In the requestedAction Event/archive, return paginator variables along with the data like this:
return array('data'=>$this->paginate(), 'paging' => $this->params['paging']);
I've tinkered a bit more and the following works for me, and the PaginationHelper works:
In the element:
// requestAction returns an array('data'=>... , 'paging'=>...)
$data = $this->requestAction(array('controller'=>'events','action'=>'archive'));
// if the 'paging' variable is populated, merge it with the already present paging variable in $this->params. This will make sure the PaginatorHelper works
if(!isset($this->params['paging'])) $this->params['paging'] = array();
$this->params['paging'] = array_merge( $this->params['paging'] , $data['paging'] );
foreach($data['events'] as $event) {
// loop through data...
}
In the Controller:
public function archive() {
$this->paginate = array(
'limit' => 10
);
if ($this->params['requested'])
return array('events'=>$this->paginate('Event'), 'paging' => $this->params['paging']);
$this->set('events', $this->paginate('Event') );
}

Cakephp Paginate Find

I want to list the posts of a given user. It work but paginate is not accurate.
My code is the following
public function index($userid = null) {
if ($this->Post->exists($userid)) {
$this->set('posts',$this->Post->find('all',array('conditions'=>array('user_id'=>$userid))),
$this->paginate());
} else
{
$this->Post->recursive = 0;
$this->set('posts', $this->paginate());
}
The result give the correct list --> 3 posts, but the paginator display page number 1 and 2
Can you help me?
Thank you
Refer to the documentation
The code in the question is quite confused.
find
The find method only has two parameters:
find(string $type = 'first', array $params = array())
The third parameter (the result of calling paginate) isn't used and will be ignored - but it will setup the view variables for the pagination helper, based on the conditions used in the paginate call - there are no conditions being used.
It is not possible to paginate the result of a find call - to do so restructure the code to call paginate instead of find.
paginate
The paginate method is just a proxy for the paginator component - it can be used in several ways, this one (controller code example):
$this->paginate($conditions)
Is the most appropriate usage for the case in the question i.e. the complete action code should be similar to:
public function index($userId = null) {
$conditions = array();
if ($userId) {
$conditions['Post.user_id'] = $userId;
}
$this->set('posts',$this->paginate($conditions));
}
Note that logically, if a user id is requested that doesn't exist the response should be nothing - not everything.
I'm quite sure that conditions for paginate do now work that way.
If you want to set conditions for paginations you should do it as follows:
$this->paginate = array('conditions' => array('Post.user_id' => $userid)));
$this->set('posts', $this->paginate());
And yes, the result stored in $posts ( in view ) will be proper as you assigned proper find result to it, meanwhile you've paginated post model without any conditions whatsoever.
First off, you're checking to see if the post exists but using the $userid. Are you trying to see "if the user exists, get the posts for that user, or else get posts for ALL users"? As you have it right now, say you have the $userid = 159, but the max Post.id in your database is 28, then the condition is not being met because it is checking to see whether or not there is a Post with the id = 159 that exists, which it doesn't.
Second, your conditions are wrong. You are performing a find and then a paginate which are two separate queries. The conditions are being implemented on the find query but not the paginate but you are only displaying the find results.
public function index($userid = null) {
// setting recursive outside of if statement makes it applicable either way
$this->Post->recursive = 0;
// check if user exists
if ($this->Post->User->exists($userid)) {
// get posts for user
$this->set('posts', $this->paginate('Post', array('Post.user_id' => $userid));
}
else{
// get all posts
$this->set('posts', $this->paginate('Post'));
}
} // end index function

CakePHP append to Containable/Contain

How to append to contain when it's already declared?
I call a regular contain/find:
// A controller
$this->Site->contain(array('User'));
$site = $this->Site->find();
I want to automatically add something to contain. I was thinking of doing this in the Model by adding to the find function... something like this:
// Site.php Model
function find($conditions = null, $fields = array(), $order = null, $recursive = null) {
if(!isset($this->containVariable) || !in_array('Box', $this->containVariable)) {
$this->containVariable[] = 'Box';
}
parent::find($conditions, $fields, $order, $recursive);
}
To make this work (automatically adding model Box to contain) I only need to change $this->containVariable by a function or variable that has an array of what's already in contain. In this case this would return array('User'). How to append to contain when it's already declared? Is there a variable that contains contain?
Unless someone can find another solution I've came to the conclusion that:
Unfortunately what I was trying to do seems impossible how containable is designed. I had to manually add my model to every ->contain() calls.
I fixed perhaps a similar issue with persisting contain for multiple find calls (using cakephp 1.3). After declaring contain for a certain model, this will be set for that model:
$this->Behaviors->Containable->runtime
-where $this is some model object. 'runtime' is an array that essentially holds the contain information. So you are close I believe, but instead of:
$this->containVariable[] = 'Box'
You would have:
$this->Behaviors->Containable->runtime['someModel']['contain'][] = 'Box'
To be able to specify more than just the model though, you would have to set up to handle like the following (perhaps build a method accordingly):
$this->Behaviors->Containable->runtime['someModel']['contain'][] = $model_contain_array
$model_contain_array is just an arbitrary name I just chose for an array which holds the contain information you would like to add for a particular model. An example of $model_contain_array might be:
array('modelName' => array(
'fields' => array('id', 'other'),
'otherModelName' => array(
'fields' => array('id', 'otherfield', 'etc')
)
)

Paginate from within a model in CakePHP

I have a function in my Event model called getEvents - you can pass limit, start and end dates, fields, event types, and event subtypes.
I read that paginate can accept all the parameters I'm using like joins, conditions, limit...etc just like a normal find can.
It returns data just fine when I don't try to paginate. But - I'd like to be able to pass it a paginate variable to tell it instead of doing this:
$this->recursive = -1;
$data = $this->find('all', $qOptions);
to do this:
$this->recursive = -1;
$data = $this->paginate($qOptions);
When I try that, though, it gives me lots of errors. I can specify the errors later if needed - for now, I guess I'm looking for - is this something that can be done? If so, how?
Is there another better way to do something like this? I spent enough time making this function do just what I want, and allowing all the options passed...etc - it just seems like a waste if I can't also use it for pagination. But - if it's not ideal, I'm ok hearing that too. Thanks in advance.
EDIT:
I'm reading other things online that say you shouldn't use paginate in your model, because it draws from URL variables, which defeats the MVC structure purpose. This makes sense, but does that mean I have to write the same joins/queries in both model and controller? And in every action that it's needed?
The way I figured out how I can keep my complex find in my model without having to rewrite it a second time in the controller is by passing a $paginate boolean variable.
If $paginate is true, it returns just the options created, which can then be used in the controller's pagination. If it's false (meaning we don't want to paginate), it returns the actual event results. So far this seems to be working.
In my getEvents() function (this method is in the Events model)
if($paginate) {
return $qOpts; // Just return the options for the paginate in the controller to use
} else {
$data = $this->find('all', $qOpts); // Return the actual events
return $data;
}
Then, in my Events/Index (events controller, index action - where I know I want pagination):
$this->Event->recursive = -1; // (or set recursive = -1 in the appModel)
$opts['paginate'] = true;
$paginateOptions = $this->Event->getEvents($opts);
$this->paginate = $paginateOptions; // Set paginate options to just-returned options
$data = $this->paginate('Event'); // Get paginate results
$this->set('data', $data); // Set variable to hold paginated results in view
The paginate() model method does not accept the same parameters as a find(). Specifically, find() wants an array of options, but paginate() wants every option passed individually. See Custom Query Pagination in the CakePHP book.
So, instead of:
$data = $this->paginate($qOptions);
You want something like:
$data = $this->paginate($qOptions['conditions'], $qOptions['fields'], ...);
EDIT
Custom model pagination isn't a function that you call. It's a function that you need to implement and will be called by the CakePHP framework. In the example in your question you are trying to manually call $this->paginate(...) from somewhere in your model. That doesn't work. Instead, do this.
In your model, implement the paginate and paginateCount methods.
function paginate($conditions, $fields, ...)
{
// return some data here based on the parameters passed
}
function paginateCount($conditions, ...)
{
// return some rowcount here based off the passed parameters
}
Then, in your controller you can use the standard pagination functions.
function index()
{
$this->paginate = array('MyModel' => array(
'conditions' => array(...),
'fields' => array(...),
));
$this->set('myobjects', $this->paginate('MyModel'));
}
Now, the Controller::paginate() function will grab the conditions and other data from the Controller::paginate parameter and, instead of passing it to your Model::find it will pass it to your custom Model::paginate() and Model::paginateCount() functions. So, the data that is returned is based on whatever you do in those two methods and not based on a standard find().
}
you can use this one which is working fine for me.
$condition="your where condition";
$this->paginate = array(
'fields' => array('AsinsBookhistory.id', 'AsinsBookhistory.reffer_id', 'AsinsBookhistory.ISBN','AsinsBookhistory.image','AsinsBookhistory.title','AsinsBookhistory.last_updatedtime'),
'conditions' => $condition,
'group' => array('AsinsBookhistory.ISBN'),
'order' => array('AsinsBookhistory.last_updatedtime' => 'desc')
);
$this->set('lastvisitedbooks', $this->paginate('AsinsBookhistory'));
$paginate array are similar to the parameters of the Model->find('all') method, that is: conditions, fields, order, limit, page, contain, joins, and recursive.
So you can define your conditions like this :
var $paginate = array(
'Event' => array (...)
);
Or you can also set conditions and other keys in the $paginate array inside your action.
$this->paginate = array(
'conditions' => array(' ... '),
'limit' => 10
);
$data = $this->paginate('Event');
http://book.cakephp.org/2.0/en/controllers.html
http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html
R u using $name = 'Event' in your controller ?
If we wont mention model name in $this->paginate() , it will use model as mentioned in $name otherwise look in var $uses array and in that will get Model name (first one )
for e.g var $uses = array('Model1','Model2'); // $name != mentioned
n you want pagination with respect to Model2 then you have to specify ModelName in paginate array like $this->paginate('Model2') otherwise Model1 will be considered in pagination.

CakePHP: Can I ignore a field when reading the Model from the DB?

In one of my models, I have a "LONGTEXT" field that has a big dump of a bunch of stuff that I never care to read, and it slows things down, since I'm moving much more data between the DB and the web app.
Is there a way to specify in the model that I want CakePHP to simply ignore that field, and never read it or do anything with it?
I really want to avoid the hassle of creating a separate table and a separate model, only for this field.
Thanks!
Daniel
As #SpawnCxy said, you'll need to use the 'fields' => array(...) option in a find to limit the data you want to retrieve. If you don't want to do this every time you write a find, you can add something like this to your models beforeFind() callback, which will automatically populate the fields options with all fields except the longtext field:
function beforeFind($query) {
if (!isset($query['fields'])) {
foreach ($this->_schema as $field => $foo) {
if ($field == 'longtextfield') {
continue;
}
$query['fields'][] = $this->alias . '.' . $field;
}
}
return $query;
}
Regarding comment:
That's true… The easiest way in this case is probably to unset the field from the schema.
unset($this->Model->_schema['longtextfield']);
I haven't tested it, but this should prevent the field from being included in the query. If you want to make this switchable for each query, you could move it to another variable like $Model->_schemaInactiveFields and move it back when needed. You could even make a Behavior for this.
The parameter fields may help you.It doesn't ignore fields but specifies fields you want:
array(
'conditions' => array('Model.field' => $thisValue), //array of conditions
'fields' => array('Model.field1', 'Model.field2'), //list columns you want
)
You can get more information of retrieving data in the cookbook .
Another idea:
Define your special query in the model:
function myfind($type,$params)
{
$params['fields'] = array('Model.field1','Model.field2',...);
return $this->find($type,$params);
}
Then use it in the controller
$this->Model->myfind($type,$params);
Also try containable behaviour will strip out all unwanted fields and works on model associations as well.
Containable
class Post extends AppModel { <br>
var $actsAs = array('Containable'); <br>
}
where Post is your model?
You can add a beforeFilter function in your Table and add a select to the query
Excample:
public function beforeFind(Event $event, Query $query){
$protected = $this->newEntity()->hidden;
$tableSchema = $event->subject()->schema();
$fields = $tableSchema->columns();
foreach($fields as $key => $name){
if(in_array($name,$protected)){
unset($fields[$key]);
}
}
$query->select($fields);
return $event;
}
In this excample I took the hidden fields from the ModelClass to exclude from result.
Took it from my answer to a simular question here : Hidden fields are still listed from database in cakephp 3

Resources