Multiple paginations in one controller - cakephp

Is there a possibility to use more than one $paginate variable in the same controller?
My problem is that I have an hasAndBelongsToMany-Relation between Links and Tags.
I have customized the pagination options so that it works with the habtm:
public $paginate = array(
'limit' => 25,
'joins' => array(
array(
'table' => 'links_tags',
'type' => 'inner',
'alias' => 'LinkTag',
'conditions' => array(
'Link.id = LinkTag.link_id'
)
)
));
It works fine, but there is another action which should be also paginated. In this action there is no tag available so every links is shown as often as there is an entry in the links_tags table for that link.
The easiest way to solve this problem is to use a second $paginate variable, but I did not found a solution how can I do that.

You can customise your paginate methods in various ways after the $paginate variable has been set, as per examples such as this one in the Cake Cookbook:
function list_recipes() {
$this->paginate = array(
'conditions' => array('Recipe.title LIKE' => 'a%'),
'limit' => 10
);
$data = $this->paginate('Recipe');
$this->set(compact('data'));
);
I haven't tested it out, but I would have thought you could put stuff like your join, which needs to vary from one action to another, in the $this->paginate call, rather than the $paginate variable. Hmm, I'll have to see if this works - could be very useful for me to know how to do this kind of thing, for later use!

According to the Cake Book pagination page (HERE), you can set different paging variables for different models (all within the same controller):
In fact, you can define more than one
set of pagination defaults in the
controller, you just name the pieces
of the array after the model you wish
to configure:
class RecipesController extends AppController {
var $paginate = array(
'Recipe' => array (...),
'Author' => array (...)
);
}

Related

Get original associations after using Containable behavior in CakePHP

Background: CakePHP 2.6.3. A pretty stable app. New behavior (MyCustomBehavior) created to output some extra info.
I have a Model MyModel acting as Containable (defined in AppModel) and then MyCustom (defined in MyModel). MyCustomBehavior is written in a way that it needs to work with the Model's associations with other Models in my app.
Problem: Whenever I contain related models in my find() call of MyModel, I cannot get a complete list of MyModel associations because Containable behavior unbinds the models that are not contained. However, if I don't set contain in my find() options or set 'contain' => false everything works as expected.
Sample MyModel->belongsTo
public $belongsTo = array(
'MyAnotherModel' => array(
'className' => 'MyAnotherModel',
'foreignKey' => 'my_another_model_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Creator' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Approver' => array(
'className' => 'User',
'foreignKey' => 'approver_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Status' => array(
'className' => 'Status',
'foreignKey' => 'status_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
);
Sample find()
$this->MyModel->find('all', array(
'fields' => array(...),
'conditions' => array(...),
'contain' => array('Approver', 'Status')
));
Result of MyModel->belongsTo in MyCustomBehavior::beforeFind()
$belongsTo = array(
'Approver' => array(
...
),
'Status' => array(
...
),
);
Expected MyModel->belongsTo in MyCustomBehavior::beforeFind()
$belongsTo = array(
'MyAnotherModel' => array(
...
),
'Creator' => array(
...
),
'Approver' => array(
...
),
'Status' => array(
...
),
);
Obvious solution: One dumb way to solve the problem is to simply set Containable behavior in MyModel instead of AppModel to control the order of loading the behaviors, i.e., public $actsAs = ['MyCustom', 'Containable']. This solution is not the best because there may be other behaviors in other models that depend on Containable, so the order of Containable needs to set in each model in app explicitly and hope that I didn't break the app somewhere.
A similar(related) question was asked on SO here but has no answers.
Need a more robust solution that can address the needs of MyCustomBehavior without having to make changes in rest of the app and looking out for any unexpected behavior.
Attempt 1 (Imperfect, error prone):
One way to recover all the original associations is to call
$MyModel->resetBindings($MyModel->alias);
$belongsToAssoc = $MyModel->belongsTo; // will now have original belongsTo assoc
However, this approach it may fail (SQL error 1066 Not unique table/alias) to work correctly if I had used joins in my find call (using default alias) to explicitly join to an already associated model. This is because Containable will also attempt to join all these tables restored by resetBindings() call resulting in join being performed twice with same alias.
Attempt 2 (perfect#, no known side effects##):
Further digging through the core Containable behavior and docs led me to object $MyModel->__backOriginalAssociation and $MyModel->__backAssociation (weird enough that ContainableBehavior never used $__backContainableAssociation as the variable name suggests) that was created and used by this behavior to perform resetBindings(). So, my final solution was to simply check if Containable is enabled on my modal (redundant in my case because it is attached in AppModel and is never disabled or detached throughout the app) and check if the object is set on the model.
// somewhere in MyCustomBehavior
private function __getOriginalAssociations(Model $Model, $type = 'belongsTo') {
if(isset($Model->__backAssociation['belongsTo']) && !empty($Model->__backAssociation['belongsTo'])) { // do an additional test for $Model->Behaviors->enabled('Containable') if you need
return $Model->__backAssociation[$type];
}
return $Model->$type;
}
public function beforeFind(Model $Model, $query) {
// somewhere in MyCustomBehavior::beforeFind()
...
$belongsToAssoc = $this->__getOriginalAssociations($MyModel, 'belongsTo'); // will now have original belongsTo assoc
...
return $query;
}
$__backAssociation holds model associations temporarily to allow for dynamic (un)binding. This solution can definitely be further improved by merging results of $Model->belongsTo and $Model->__backAssociation['belongsTo'] (or hasMany, hasOne, hasAndBelongsToMany) to include any models that were bound on the fly. I don't need it, so I will skip the code for merging.
# Perfect for my own use case and my app setup.
## No side effects were found in my testing that is limited by my level of expertise/skill.
Disclaimer: My work in this post is licensed under WTF Public License(WTFPL). So, do what the f**k you want with the material. Additionally, I claim no responsibility for any financial, physical or mental loss of any kind whatsoever due to the use of above material. Use at your own risk and do your own f**king research before attempting a copy/paste. Don't forget to take a look at cc by-sa 3.0 because SO says "user contributions licensed under cc by-sa 3.0 with attribution required." (check the footer on this page. I know you never noticed it before today! :p)

cakePHP virtualField within condition of another model

I have 2 models, User and Entity. I need to on my entities page, have pagination, and a simple search function. The search function will filter the entities, but the data it filters is a virtualField within the User model.
The Entity model:
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
};
The virtual field in the User model:
public $virtualFields = array("searchFor" => "CONCAT(User.first_name, ' ',User.last_name, ' ',User.email)");
And the condition within the entity controller:
$conditions["User.searchFor LIKE"] = "%".str_replace(" ","%",$this->passedArgs["searchFor"])."%";
$this->paginate = array(
'conditions' => $conditions,
'order' => array('Re.created' => 'DESC'),
'limit' => 20
);
From what I can read this is not possible because I cannot use virtualFields within a query of an associative model, and it says I must use it in "run time", but I really do not understand how to do that, or how to do that in this instance. Any ideas or a way I can get this to work?
You could try attaching the virtualField to the Entity model like so
$this->Entity->virtualFields['searchFor'] = $this->Entity->User->virtualFields['searchFor'];
But you have to make sure that a join is done and not 2 queries.
I believe its discussed in the book.
Edit: Book page
For this purpose I needed to do a search on a concatinated string from an associated model. Normally this can be done using Virtualfields in cake, but cake does not support using a virtualField from an associated model in the search.
Seeing that the 2 models were already linked with the belongsTo, I merely changed the condition to:
"conditions" => "CONCAT(User.first_name, ' ',User.last_name, ' ',User.email) LIKE" => "%".str_replace(" ","%",$this->passedArgs["searchFor"])."%"
Probably not the most elegant solution, but it works.

CakePHP bindModel HABTM Save

I want to create a binding from one model to my User model on the fly so that the JOIN is not called every time that I perform a find on that model. I am using the binding to perform a HABTM save. However, when I use the bindModel function, the HABTM data is not saved in the database.
What makes this odd is that if I move my binding to the User model, then the save works perfectly. I don't see any indication in the documentation that the save behavior would be different when the association is made in the model versus the bindModel function (though, I may have missed it if there is any).
Here is my bindModel code in my controller:
$this->User->bindModel(
array(
'hasAndBelongsToMany' => array(
'Othermodel' => array(
'className' => 'Othermodel',
'joinTable' => 'othermodels_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'othermodel_id',
'unique' => true,
)
)
)
);
if($res = $this->User->save($data)){
return true;
}
And this is my user model.
class User extends AppModel {
public $name = 'User';
public $belongsTo = array();
public $hasOne = array();
public $hasMany = array();
public $hasAndBelongsToMany = array(
'Othermodel' => array(
'className' => 'Othermodel',
'joinTable' => 'othermodels_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'othermodel_id',
'unique' => true
)
);
Again, I only have the relationship active in one place at one time, so I know the problem is not with the binding itself. It seems the problem is solely related to the fact that I have tried to use bindModel. Is this the intended behavior?
Based on the responses in this article, it looks like the bindModel function only exists for finds.
CakePHP: Bind Model not working
Though there's no information in the CakePHP 1.3 documentation that states that the bind is only for finds, though that would explain the behavior...
http://book.cakephp.org/1.3/en/The-Manual/Developing-with-CakePHP/Models.html
So, I am marking this as answered.
I'm not sure if this is relevant, but are you sure you are not doing any other find operations between the binding and the save. bindModel only binds the model for the next find operation so you may need to pass true through to bindModel to tell it to keep the binding around.
Relevant links: CakePHP: Bind Model not working and http://groups.google.com/group/cake-php/browse_thread/thread/316c9796603eac57?pli=1.
I hope this helps.
Try this,
$this->Message->bindModel(
array(
'belongsTo'=>array(
'User'=>array(
'foreignKey'=>false,
'conditions'=>array('Npl.to=User.id '),
'fields'=>array('recive')
),
'User_e'=>array(
'className'=>'User',
'foreignKey'=>false,
'alias'=>'User_e',
'conditions'=>array('Npl.from=User_e.id '),
'fields'=>array('recive')
)
)
)
);

Is there a way to define Model relationships with conditions in CakePHP?

I am using CakePHP 1.3.
I have a few questions regarding CakePHP Models:
1) Is there a way to define a Model with conditions, such that when invoked using the Containable, behavior, I do not need to define 'conditions' for it again. For example: I have two models - "Store" and "Deal" where "Store" that hasMany "Deal". I want to accomplish the following without having to define these conditions for "Deal" every time:
$this->Store('all', array('contain'=>array('Deal'=>array('isactive'=>1,'now() < expirydate', 'qty > 0')));
2) Also, is there a way to define relationships between Models differently based on conditions? In other words, how can I define "Store" hasMany "Deals", "ActiveDeal", "ExpiredDeal", etc.., all on the 'deals' table but differing based on conditions I set for each.
Much appreciate any help.
Thanks/Regards..
If you look at the first couple of code examples on the Associations: Linking Models Together page of the CakePHP Cookbook, you'll see that you can add conditions to your model joins that will work whenever you do a basic find.
Therefore you should be able to do something like the following:
class Store extends AppModel {
var $name = 'Store';
var $hasMany = array(
'Deal' => array(
'className' => 'Deal',
'foreignKey' => 'store_id',
'conditions' => array(
'Deal.isactive' => '1',
'now() < Deal.expirydate',
'Deal.qty > 0'
),
'order' => ''
),
'ExpiredDeal' => array(
'className' => 'Deal',
'foreignKey' => 'store_id',
'conditions' => array('now() >= ExpiredDeal.expirydate'),
'order' => ''
)
);
}

Is it possible to use bindModel to bind 3 different nested tables in CakePHP

I have a segment which can have many comments and each comment can have many tags. I can bind the comments to the segments using code like the below which is a function in the segment model class.
function prepareForGettingSegmentsWithComments() {
$this->bindModel(
array('hasMany' => array(
'Comment' => array(
'className' => 'Comment',
'foreignKey' => 'segmentID'
)
)
)
);
}
However how can I bind in the Tags as well?
Yes, in this situation, I bind the Tags to the Comments with a belongsTo. Then filter the results with some query conditions.
Let me see if I can find an example snippet somewhere,
if(isset($this->params['named']['category'])){
$this->Link->bindModel(
array('belongsTo' => array(
'CategoriesLink' => array(
'className' => 'CategoriesLink',
'foreignKey' => 'id',
)
)),
array('belongsTo' => array(
'Category' => array(
'className' => 'Category',
'foreignKey' => 'categories_link_id',
)
))
);
$data = $this->paginate('Link', array('CategoriesLink.category_id'=>$this->params['named']['category']));
} else {
$data = $this->paginate('Link', array('Link.status_id'=>'1'));
}
$this->set('links', $data);
This is how I did it when I was trying to paginate my Link model by a related field. This was with Cake1.2 though, but I think the principle is the same.
I would also recommend installing DebugKit, http://www.ohloh.net/p/cakephp-debugkit , then tinker with the links and conditions until you get a query which works for you.
Sorry, not very technical ;) I'm sure someone can give you a more accurate answer.
PS, Having just reread the question, do you not have these Models linked already? Surely hooking them up in the Models through CakePHP relationships, you'd not need to bind the models and could just use Containable or unbindModel()

Resources