CakePHP containable behaviour not firing - cakephp

First time using Cake and its containable behaviour, but it's not working as expected ... or at all.
I'm trying to obtain a list of accessories for a product. Product model HABTM products (alias 'ProductRelation'). Join table is products_products which has two product ids - product_id and related_id. It's against this I want to pull the list of accessories (products driven from the related_id column) for a given product_id
In my Product model, I've added $actsAs = array('Containable');
And in my controller, a quick test of containable using reference from the cookbook fails to contain products at all, even without conditions.
debug($this->Product->find('all', array('contain' => 'ProductRelation')));
.. returns an array of every product in the db, with ALL related models - images, content tabs, ratings, reviews, pricing, etc I haven't tried applying any 'conditions' against this because the call as written should limit data to the product and it's ProductRelation data, according to the cookbook ...
Any tips?

It seems like you have recursive on. Try using the following:
debug($this->Product->find('all', array(
'contain' => 'ProductRelation',
'recursive' => -1
)));
If that works for you, you should start adding containable to the AppModel class and setting the recursive property to -1. This will ensure you only ever get the results you request.
NB: Cake does not join for HABTM, so you can not use ProductRelation in any conditions.

Related

Making CakePHP associations smarter

I have three models associated with each other in my CakePHP application. I have an Article which belongs to both a Category and an Author. I query for an category’s latest articles like this:
$category = $this->Category->find('first', array(
'conditions' => array(
'Category.slug' => $slug
),
'contain' => array(
'Article' => array(
'Author'
)
)
));
However, I’ve noticed this is very inefficient, as for every Article record it finds it queries my authors database table, even if I’ve already fetched that particular author’s details. So if I fetch 10 articles and they’re all by the same author, CakePHP will still issue 10 separate SELECT statements.
Can I make CakePHP “smarter” when using the Containable behavior so that it only issues the necessary queries? In my case, issue the first SELECT statement to find the author, and then use that result in subsequent Article rows if the ID matches.
There are several ways of doing this through cake's api - the simplest would be to either set the Model's $recursive attribute to either -1 or 0, or use the Model's unbind() function

Possible to have association or not based on main model's data?

I have a nodes table (Node model). I'd like it to be associated to different data types, but only if one if it's field is set to 1.
Example:
My nodes table has a data_article field (tinyint 1). I only want the Node to $hasMany Article IF that field is a 1.
I tried this:
public $hasMany = array(
'Article' => array(
'conditions' => array('Node.data_articles' => '1')
),
);
But I get an error:
Column not found: 1054 Unknown column 'Node.data_articles' in 'where
clause'
Because the association is doing the Article find in it's own query:
SELECT `Article`.`id`, `Article`.`title`, `Article`.`node_id`, ...more fields...
FROM `mydatabase`.`articles` AS `Article`
WHERE `Node`.`data_artiles` = '1'
AND `Article`.`node_id` = ('501991c2-ae30-404a-ae03-2ca44314735d')
Obviously that doesn't work, since the Node table isn't being Joined at all in this query.
TLDR:
Is it possible to have associations or not based on a field in the main model? If not, how else can I keep different data types in multiple tables, and not have to query them all every time?
I don't think that there's a standard way to do this with CakePHP (at least I can't imagine a way). What definitely is possible would be binding associated models dynamically.
So you might query your model without associations by passing the recursive parameter as -1 to the find() method and based on the result unbind the associated models dynamically. Afterwards you would have to query again, for sure. You might build a behavior out of this to make it reusable.
A quite elegant solution would be possible, if Cake could make a two-step query with a callback after the main model was queried, but before associated models are queried, but this isn't possible at the moment.
Edit: There might be a non-Cake way to archieve this more performantly with a custom Query including IF-statements, but I'm not the SQL expert here.
You should do it from the other side:
public $belongsTo = array(
'Node' => array(
'conditions' => array('Node.data_articles' => '1')
),
);

CakePHP Recursive Find Method

In my CakePHP app, I have the following model association:
Offer -> Order -> Coupon
Then, at OffersController.php, i need to write a find method to output
the number of coupons sold on each offer, assuming that some orders can have multiple coupons.
The problem is that I'm in the OffersController, so when I try to use find('count'), I just get the number of offer entries. I want to count the number of coupon entries.
I tried to do something like $this->Offer->Order->Coupon->find('count'), but it does not work.
I tried to use the ContainableHelper as well, but got into the same problem of counting the wrong entries.
How can I proceed?
You need the resulting SQL query to do some magic with COUNT and GROUP BY. Try to add a field like this (untested):
$this->Offer->Order->Coupon->find(
'all',
array(
// Generate a field that counts the coupons per Order
'fields' => array(
'COUNT(Coupon.id) AS coupon_count'
),
// Only look at Orders for the given Offer
'conditions' => array(
'Order.offer_id' => $id
),
// This is important: group by Order.
'group' => ('Coupon.order_id')
)
);
If your Order -> Coupon have a simple hasMany relationship, a more convenient approach would be to keep track of the existing Coupon count in every Order record using an extra coupon_count field and the counterCache function. CakePHP can automatically update that counter field for you.

Containable unexpected behavior in related models

We have the Ebooks HABTM Tags. And we try to select the Ebooks that have belong to the tag with id=160.
Both use the containable behavior, so in the Ebooks Controller I have written the following:
$this->Ebook->contain('Tag.id = "160"');
$ebooks = $this->Ebook->find('all');
According the book this should return the needed result. Instead of that, a list of all ebooks is given back.
Note also that two queries are run, the first returns the list of all Ebooks and the second the ebooks that should be returned. Does anyone have any idea?
Thanks in advance
Yes, this query says "Find all Ebooks and include with them all Tags with the id 160".
contain does not limit the primary result, only the related results.
You need to make an SQL JOIN to the HATBM table and filter your primary results on it, like so:
$this->Ebook->bindModel(array('hasOne' => array('EbooksTag')));
$this->Ebook->find('all', array(
'conditions' => array('EbooksTag.tag_id' => 160)
));

How do I join two tables in a third n..n (hasAndBelongsToMany) relationship in CakePHP?

I have a n...n structure for two tables, makes and models. So far no problem.
In a third table (products) like:
id
make_id
model_id
...
My problem is creating a view for products of one specifi make inside my ProductsController containing just that's make models:
I thought this could work:
var $uses = array('Make', 'Model');
$this->Make->id = 5; // My Make
$this->Make->find(); // Returns only the make I want with it's Models (HABTM)
$this->Model->find('list'); // Returns ALL models
$this->Make->Model->find('list'); // Returns ALL models
So, If I want to use the list to pass to my view to create radio buttons I will have to do a foreach() in my make array to find all models titles and create a new array and send to the view via $this->set().
$makeArray = $this->Make->find();
foreach ($makeArray['Model'] as $model) {
$modelList[] = $model['title'];
}
$this->set('models', $models)
Is there any easier way to get that list without stressing the make Array. It will be a commom task to develops such scenarios in my application(s).
Thanks in advance for any hint!
Here's my hint: Try getting your query written in regular SQL before trying to reconstruct using the Cake library. In essence you're doing a lot of extra work that the DB can do for you.
Your approach (just for show - not good SQL):
SELECT * FROM makes, models, products WHERE make_id = 5
You're not taking into consideration the relationships (unless Cake auto-magically understands the relationships of the tables)
You're probably looking for something that joins these things together:
SELECT models.title FROM models
INNER JOIN products
ON products.model_id = models.model_id
AND products.make_id = 5
Hopefully this is a nudge in the right direction?
Judging from your comment, what you're asking for is how to get results from a certain model, where the condition is in a HABTM related model. I.e. something you'd usually do with a JOIN statement in raw SQL.
Currently that's one of the few weak points of Cake. There are different strategies to deal with that.
Have the related model B return all ids of possible candidates for Model A, then do a second query on Model A. I.e.:
$this->ModelB->find('first', array('conditions' => array('field' => $condition)));
array(
['ModelB'] => array( ... ),
['ModelA'] => array(
[0] => array(
'id' => 1
)
)
Now you have an array of all ids of ModelA that belong to ModelB that matches your conditions, which you can easily extract using Set::extract(). Basically the equivalent of SELECT model_a.id FROM model_b JOIN model_a WHERE model_b.field = xxx. Next you look for ModelA:
$this->ModelA->find('all', array('conditions' => array('id' => $model_a_ids)));
That will produce SELECT model_a.* FROM model_a WHERE id IN (1, 2, 3), which is a roundabout way of doing the JOIN statement. If you need conditions on more than one related model, repeat until you have all the ids for ModelA, SQL will use the intersection of all ids (WHERE id IN (1, 2, 3) AND id IN (3, 4, 5)).
If you only need one condition on ModelB but want to retrieve ModelA, just search for ModelB. Cake will automatically retrieve related ModelAs for you (see above). You might need to Set::extract() them again, but that might already be sufficient.
You can use the above method and combine it with the Containable behaviour to get more control over the results.
If all else fails or the above methods simply produce too much overhead, you can still write your own raw SQL with $this->Model->query(). If you stick to the Cake SQL standards (naming tables correctly with FROM model_as AS ModelA) Cake will still post-process your results correctly.
Hope this sends you in the right direction.
All your different Make->find() and Model->find() calls are completely independent of each other. Even Make->Model->find() is the same as Model->find(), Cake does not in any way remember or take into account what you have already found in other models. What you're looking for is something like:
$this->Product->find('all', array('conditions' => array('make_id' => 5)));
Check out the Set::extract() method for getting a list of model titles from the results of $this->Make->find()
The solution can be achieved with the use of the with operation in habtm array on the model.
Using with you can define the "middle" table like:
$habtm = " ...
'with' => 'MakeModel',
... ";
And internally, in the Model or Controller, you can issue conditions to the find method.
See: http://www.cricava.com/blogs/index.php?blog=6&title=modelizing_habtm_join_tables_in_cakephp_&more=1&c=1&tb=1&pb=1

Resources