I've been quite some time trying to use the Containable Behavior in CakePHP but I can't get to make it work as I expected.
My application is different, but to simplify I'll put this example. Let's say I have a forum with threads and activities, and the activities can be rated. The general relations would be:
Forum: hasMany [Thread]
Thread: belongsTo [Forum], hasMany [Activity]
Activity: belongsTo [Thread], hasMany [Rating]
Rating: belongsTo [Activity]
What I want to achieve is, using the find method, get all the ratings performed on a certain forum. What I suppose should be done is the following:
$this->Rating->find('count', array(
'contain' => array(
'Activity' => array(
'Thread'
)
),
'conditions' => array(
'Thread.forum_id' => 1
)
));
But the result query is:
SELECT COUNT(*) AS `count` FROM `ratings` AS `Rating` LEFT JOIN `activities` AS `Activity` ON (`Rating`.`activity_id` = `Activity`.`id`) WHERE `Thread`.`forum_id` = 1;
I've accomplished this using the 'joins' option, but it's more complex and I have to use this kinda action in many situations.
All the files related with the example can be found here: http://dl.dropbox.com/u/3285746/StackOverflow-ContainableBehavior.rar
Thanks
Update 23/11/2011
After investigating the framework and thanks to the answers of Moz Morris and api55 I found the source of the problem.
The basic problem was that, as I understood CakePHP, I thought it was querying using joins each time. The thing it that it doesn't do that, the real operation it would perform to obtain the result I was looking for would be something like this:
SELECT * FROM Rating JOIN Activity...
SELECT * FROM Activity JOIN Thread...
SELECT * FROM Activity JOIN Thread...
...
Meaning that it would do a query to get all the activities and then, for each activity, perform a query to get the Threads... My approach was failing not because of the Containable Behaviour being used wrong, but because the 'conditions' option was applied to all queries and, on the first one, it crashed because of the absence of the Thread table. After finding this out, there are two possible solutions:
As api55 said, using the conditions inside the 'contain' array it would apply them only to the queries using the Thread table. But doing this the problem persists, because we have way too many queries.
As Moz Morris said, binding the Thread model to Rating would also work, and it would perform a single query, which is what we want. The problem is that I see that as a patch that skips the relations betweem models and doesn't follow CakePHP philosophy.
I marked api55 solution as the correct because It solves the concrete problem I had, but both give a solution to the problem.
First of all, have you put the actAs containable variable in the appModel?? without it this beahaviour won't work at all (i see it is not working correctly since it didn't join with Thread table)
I would do it from the top, i mean from forum, so you choose your forum (im not sure you want forum or thread) and get all its rating, if theres no rating you will end up with the rating key empty.
something like this
appModel
public $actsAs = array('Containable');
rating controller
$this->Rating->Activity->Thread->Forum->find('count', array(
'contain' => array(
'Thread' => array(
'Activity' => array(
'Rating' => array (
'fields' => array ( 'Rating.*' )
)
)
)
),
'conditions' => array(
'Forum.id' => 1
)
));
Then if you need only a value in rating table just use Set:extract to get an array of this value.
As you did it IT SHOULD work anyways but i sugest not to use forum_id there, but in conditions inside contain like this
'contain' => array(
'Activity' => array(
'Thread' => array(
'conditions' => array('Thread.forum_id' => 1)
)
)
),
Also, never forget the actsAs variable in the model using the containable behaviuor (or in app model)
Whist I like api55's solution, I think the results are a little messy - depends on what you intend to do with the data I guess.
I assume that when you said using the 'joins' method you were talking about using this method:
$this->Rating->bindModel(array(
'belongsTo' => array(
'Thread' => array(
'foreignKey' => false,
'conditions' => 'Thread.id = Activity.thread_id',
),
'Forum' => array(
'foreignKey' => false,
'conditions' => 'Forum.id = Thread.forum_id'
)
)
));
$ratings = $this->Rating->find('all', array(
'conditions' => array(
'Forum.id' => 1 // insert forum id here
)
));
This just seems a little cleaner to me, and you don't have to worry about using the containable behaviour in your AppModel. Worth considering.
Related
can anyone tell me, how to retrieve joined result from multiple tables in cakePHP ( using cakePHP mvc architecture). For example, I have three tables to join (tbl_topics, tbl_items, tbl_votes. Their relationship is defined as following: a topic can have many items and an item can have many votes. Now I want to retrieve a list of topics with the count of all votes on all items for each topic. The SQL query for this is written below:
SELECT Topic.*, count(Vote.id) voteCount
FROM
tbl_topics AS Topic
LEFT OUTER JOIN tbl_items AS Item
ON (Topic.id = Item.topic_id)
LEFT OUTER JOIN tbl_votes AS Vote
ON (Item.id = Vote.item_id);
My problem is I can do it easily using $this-><Model Name>->query function, but this requires sql code to be written in the controller which I don't want. I'm trying to find out any other way to do this (like find()).
$markers = $this->Marker->find('all', array('joins' => array(
array(
'table' => 'markers_tags',
'alias' => 'MarkersTag',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('MarkersTag.marker_id = Marker.id')
),
array(
'table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array(
'Tag.id = MarkersTag.tag_id',
'Tag.tag' => explode(' ', $this->params['url']['q'])
)
)
)));
as referred to in nate abele's article: link text
I'll be honest here and say that you'll probably be a lot happier if you just create a function in your model, something like getTopicVotes() and calling query() there. Every other solution I can think of will only make it more complicated and therefore uglier.
Edit:
Depending on the size of your data, and assuming you've set up your model relations properly (Topic hasMany Items hasMany Votes), you could do a simple find('all') containing all the items and votes, and then do something like this:
foreach ($this->data as &$topic)
{
$votes = Set::extract('/Topic/Item/Vote', $topic);
$topic['Topic']['vote_count'] = count($votes);
}
Two things are important here:
If you have a lot of data, you should probably forget about this approach, it will be slow as hell.
I've written this from my memory and it might not look like this in real life and/or it may not work at all :-)
You can easily set the "recursive" property on a find() query.
$result = $this->Topic->find('all', array('recursive' => 2));
Alternatively, you can use the Containable behavior in your model. Then you can use:
$this->Topic->contain(array(
'Item',
'Item.Vote',
));
$result = $this->Topic->find('all');
or
$result = $this->Topic->find('all', array(
'contain' => array(
'Item',
'Item.Vote',
),
));
What you need is recursive associations support, which is not possible with stock CakePHP currently.
Although it could be achieved using some bindModel trickery
or an experimental RecursiveAssociationBehavior.
Both of these solutions will either require you to use extra code or rely on a behaviour in your application but if you resist the temptation to write pure SQL code, you'll be rewarded with being able to use Cake`s pagination, auto conditions, model magic etc..
I think this answer is already submitted, but I am posting here for someone who seeks still for this.
The joins can be done with find() method can be like below
$result = $this->ModelName1->find("all",array(
'fields' => array('ModelName1.field_name','Table2.field_names'), // retrieving fileds
'joins' => array( // join array
array(
'table' => 'table_name',
'alias' => 'Table2',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('ModelName1.id = Table2.id') // joins conditions array
),
array(
'table' => 'table_name3',
'alias' => 'Table3',
'type' => 'inner',
'foreignKey' => false,
'conditions'=> array('Table3.id = Table2.id')
)
)));
You should study HaBTM (Has and Belongs to Many)
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html
I wrote 2 days ago to ask about andConditions and it appeared that I didn't understand the idea but the fact is that for two days now I am stuck with the next step using CakeDC:
How do I implement complex HABTM conditions in "query" methods for CakeDC search plugin?
I have Offer HABTM Feature (tables: offers, features, features_offers) and the below works just fine when used in controller:
debug($this->Offer->find('all', array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id in (8, 10)',
)
)
)
)
)
);
The problem comes when I want to use the same conditions in the search:
public $filterArgs = array(
array('name' => 'feature_id', 'type' => 'query', 'method' => 'findByFeatures'),
);
........
public function findByFeatures($data = array()) {
$conditions = '';
$featureID = $data['feature_id'];
if (isset($data['feature_id'])) {
$conditions = array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id' => $data['feature_id'],
)
)
)
);
}
return $conditions;
}
I get an error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'contain' in 'where clause'
which makes me think that I cannot perform this search and/or use containable behavior in searches at all.
Can someone with more experience in the field please let me know if I am missing something or point me to where exactly to find a solution for that - perhaps a section in the cookbook?
EDIT: Also tried the joins. This works perfectly fine in the controller, returning all the data I need:
$options['joins'] = array(
array('table' => 'features_offers',
'alias' => 'FeaturesOffers',
'type' => 'inner',
'conditions' => array(
'Offer.id = FeaturesOffers.offer_id'
),
array('table' => 'features',
'alias' => 'F',
'type' => 'inner',
'conditions' => array(
'F.id = FeaturesOffers.feature_id'
),
)
),
);
$options['conditions'] = array(
'feature_id in (13)' //. $data['feature_id']
);
debug($this->Offer->find('all', $options));
... and when I try to put in the search method I get the returned conditions only in the where clause of the SQL
WHERE ((joins = (Array)) AND (conditions = ('feature_id in Array')))
...resulting in error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'joins' in 'where clause'
EDIT: Maybe I am stupid and sorry to say that but the documentation of the plugin sucks a ton.
I double, triple and quadruple checked (btw, have lost already 30 hours at least on 1 filed of the search form facepalm) and the stupid findByTags from the documentation still doesn't make any sense to me.
public function findByTags($data = array()) {
$this->Tagged->Behaviors->attach('Containable', array('autoFields' => false));
$this->Tagged->Behaviors->attach('Search.Searchable');
$query = $this->Tagged->getQuery('all', array(
'conditions' => array('Tag.name' => $data['tags']),
'fields' => array('foreign_key'),
'contain' => array('Tag')
));
return $query;
}
As I understand it
$this->Tagged
is supposed to be the name of the model of the HABTM association.
This is quite far from the standards of cakePHP though: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
The way it is described here, says that you don't need another model but rather you associate Recipe with Ingredient as shown below:
class Recipe extends AppModel {
public $hasAndBelongsToMany = array(
'Ingredient' =>
array(
'className' => 'Ingredient',
'joinTable' => 'ingredients_recipes',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'ingredient_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
meaning that you can access the HABTM assoc table data from Recipe without needing to define model "IngredientRecipe".
And according to cakeDC documentation the model you need is IngredientRecipe and that is not indicated as something obligatory in the cakePHP documentation. Even if this model is created the HABTM assoc doesn't work properly with it - I tried this as well.
And now I need to re-write the search functionality in my way, using only cakePHP even though I spent already 30 hours on it... so unhappy. :(
Every time I come to do this in a project I always spend hours figuring out how to do it using CakeDC search behavior so I wrote this to try and remind myself with simple language what I need to do. I've also noticed that although Using the CakeDC search plugin with associated models this is generally correct there is no explanation which makes it more difficult to modify it to one's own project.
When you have a "has and belongs to many" relationship and you are wanting to search the joining table i.e. the table that has the two fields in it that joins the tables on either side of it together in a many-to-many relationship you want to create a subquery with a list of IDs from one of the tables in the relationship. The IDs from the table on the other side of the relationship are going to be checked to see if they are in that record and if they are then the record in the main table is going to be selected.
In this following example
SELECT Handover.id, Handover.title, Handover.description
FROM handovers AS Handover
WHERE Handover.id in
(SELECT ArosHandover.handover_id
FROM aros_handovers AS ArosHandover
WHERE ArosHandover.aro_id IN (3) AND ArosHandover.deleted != '1')
LIMIT 20
all the records from ArosHandover will be selected if they have an aro_id of 3 then the Handover.id is used to decide which Handover records to select.
On to how to do this with the CakeDC search behaviour.
Firstly, place the field into the search form:
echo $this->Form->create('Handover', array('class' => 'form-horizontal'));?>
echo $this->Form->input('aro_id', array('options' => $roles, 'multiple' => true, 'label' => __('For', true), 'div' => false, true));
etc...
notice that I have not placed the form element in the ArosHandover data space; another way of saying this is that when the form request is sent the field aro_id will be placed under the array called Handover.
In the model under the variable $filterArgs:
'aro_id' => array('name' => 'aro_id', 'type' => 'subquery', 'method' => 'findByAros', 'field' => 'Handover.id')
notice that the type is 'subquery' as I mentioned above you need to create a subquery in order to be able to find the appropriate records and by setting the type to subquery you are telling CakeDC to create a subquery snippet of SQL. The method is the function name that are going to write the code under. The field element is the name of the field which is going to appear in this part of the example query above
WHERE Handover.id in
Then you write the function that will return the subquery:
function findByAros($data = array())
{
$ids = ''; //you need to make a comma separated list of the aro_ids that are going to be checked
foreach($data['aro_id'] as $k => $v)
{
$ids .= $v . ', ';
}
if($ids != '')
{
$ids = rtrim($ids, ', ');
}
//you only need to have these two lines in if you have not already attached the behaviours in the ArosHandover model file
$this->ArosHandover->Behaviors->attach('Containable', array('autoFields' => false));
$this->ArosHandover->Behaviors->attach('Search.Searchable');
$query = $this->ArosHandover->getQuery('all',
array(
'conditions' => array('ArosHandover.aro_id IN (' . $ids . ')'),
'fields' => array('handover_id'), //the other field that you need to check against, it's the other side of the many-to-many relationship
'contain' => false //place this in if you just want to have the ArosHandover table data included
)
);
return $query;
}
In the Handovers controller:
public $components = array('Search.Prg', 'Paginator'); //you can also place this into AppController
public $presetVars = true; //using $filterArgs in the model configuration
public $paginate = array(); //declare this so that you can change it
// this is the snippet of the search form processing
public function admin_find()
{
$this->set('title_for_layout','Find handovers');
$this->Prg->commonProcess();
if(isset($this->passedArgs) && !empty($this->passedArgs))
{//the following line passes the conditions into the Paginator component
$this->Paginator->settings = array('conditions' => $this->Handover->parseCriteria($this->passedArgs));
$handovers = $this->Paginator->paginate(); // this gets the data
$this->set('handovers', $handovers); // this passes it to the template
If you want any further explanation as to why I have done something, ask and if I get an email to tell me that you have asked I will give an answer if I am able to.
This is not an issue of the plugin but how you build the associations. You need to properly join them for a search across these three tables. Check how CakePHP is fetching the data from HABTM assocs by default.
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#joining-tables
Suppose a Book hasAndBelongsToMany Tag association. This relation uses
a books_tags table as join table, so you need to join the books table
to the books_tags table, and this with the tags table:
$options['joins'] = array(
array('table' => 'books_tags',
'alias' => 'BooksTag',
'type' => 'inner',
'conditions' => array(
'Books.id = BooksTag.books_id'
)
),
array('table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'conditions' => array(
'BooksTag.tag_id = Tag.id'
)
)
);
$options['conditions'] = array(
'Tag.tag' => 'Novel'
);
$books = $Book->find('all', $options); Using joins allows you to have
a maximum flexibility in how CakePHP handles associations and fetch
the data, however in most cases you can use other tools to achieve the
same results such as correctly defining associations, binding models
on the fly and using the Containable behavior. This feature should be
used with care because it could lead, in a few cases, into bad formed
SQL queries if combined with any of the former techniques described
for associating models.
Also your code is wrong somewhere.
Column not found: 1054 Unknown column 'contain' in 'where clause'
This means that $Model->contain() is somehow called. I don't see such a call in your code pasted here so it must be somewhere else. If a model method can not be found this error usually happens with the field name as column.
I want to share with everyone that the solution to working with HABTM searches with the plugin lies here: Using the CakeDC search plugin with associated models
#burzum, the documentation is far from ok man. Do you notice the use of 'type' => 'checkbox' and that it is not mentioned anywhere that it is a type?
Not to mention the total lack of grammar and the lots of typos and missing prepositions. I lost 2 days only to get a grasp of what the author had in mind and bind the words in there. No comment on that.
I am glad that after 5 days on the uphill work I made it. Thanks anyway for being helpful.
I'm very new to CakePHP (started last night) and I'm having an issue with queries.
The way my model works is this:
Classgroups << hasAndBelongsTo >> Users << hasAndBelongsTo >> PermissionsGroups
Basically a Classgroup is made up of a load of users and each user belongs to a permission group. The group defines what type of user they are as well as their permissions.
Now I need to make a query where I get all Users that belong to a certain PermissionGroup but I'm doing it from the ClassgroupsController.
However whenever I try to do this:
$this->Classgroup->User->find('list', array(
'fields' => array('User.surname_firstname'),
'order' => array(
'User.surname_firstname'
),
'conditions' => array(
'PermissionGroup.permissions' => '10'
)
)
I get this error:
Column not found: 1054 Unknown column 'PermissionGroup.permissions' in 'where clause'
Because I'm in the ClassgroupController it's as if the Group fields can't be seen but I would have assumed because User is associated with PermissionGroup it should be ok. I have set up the associations between the 3 models but the problem seems to be that the query isn't looking "deep" enough into the model.
I've also tried setting the recursive value on the find to 2,3 and 4. From what I've read I think this probably isn't a great idea and Containable is better but surely it should at least work?
I should also point out that these associations are one way. I've only added them to the Models I needed. So Classgroups has an association to User and User has an association to PermissionGroup
Am I doing something obviously wrong? Or have I misunderstood a CakePHP concept?
Thanks
UPDATE: For the second time I've seen someone say I should use joins, like so:
'joins'=>array(
array(
'table'=>'rooms',
'alias'=>'Room',
'type'=>'inner',
'conditions'=>array(
'Room.hotel_id'=>'Hotel.id'
)
)
But I have the feeling that's an outdated way to do it... I've also tried using contain like so:
'contain' => array(
'Group' => array(
'conditions' => array(
'Group.name' => '10'
)
)
)
But it didn't work, I still got all of the Users.
You were on the right track with joins:
$joins = array(
array(
'table'=>'permission_groups',
'alias'=>'PemissionGroup',
'type'=>'inner',
'conditions'=>array(
'User.id = PermissionGroup.user_id',
'PermissionGroup.permissions = 10'
)
)
$this->Classgroup->User->find('list', array(
'fields' => array('User.surname_firstname'),
'joins' => $joins,
'order' => array(
'User.surname_firstname'
)
)
Try to work with that!
I am aiming to create a summary page of activity on an application I am currently working on. I have identified that I must do the following:
Get all stories from people I am subscribed to and format them like the following:
[Username] has posted [StoryName] - View story
Get all stories that users I am connected to have posted comments on
[Username] has posted a comment on [StoryName] - View story
I am unsure how I can get both arrays, display them the format I want but order them by the posted date (in the same way people like Facebook do)
What is the best way to go about this?
Please Note: The answer must be something which is easily extendible. I am considering following wordpress' model and creating a Posts table which has a Post Type field.
What you're trying to do is pretty much built-in with CakePHP. The big thing is to make sure that your models are properly associated. Once this is accomplished, Cake will do most of the heavy lifting for you.
For your situation, use 3 models associated like this:
class Story extends AppModel{
var $belongsTo = 'Author';
var $hasMany = 'Comment';
}
class Author extends AppModel{
var $hasMany = array( 'Story', 'Comment' );
var $hasAndBelongsToMany = 'User';
}
class Comment extends AppModel{
var $belongsTo = array( 'Author', 'Story' );
}
Set up your tables according to the Cake conventions. Then a little CakePHP magic in your controller:
$this->Story->Author->bindModel( array( 'hasOne' => array( 'AuthorsUsers' ) ) );
$myAuthors = $this->Story->Author->find( 'list', array(
'fields' => array( 'id' ),
'conditions' => array( 'AuthorsUsers.user_id' => $userId ),
'recursive' => false
) );
$stories = $this->Story->find( 'all', array(
'fields' => array( 'Story.id', 'Story.title', 'Author.id', 'Author.name' ),
'order' => 'published_date DESC',
'conditions' => array( 'Author.id' => $myAuthors ),
'recursive' => 2
) );
Quick explanation of what's going on:
bindModel() lets Cake know that you want to use the HABTM association to find Authors by the associated User id. (Cake convention is to have a table called 'authors_users' to join the Author->User HABTM association.)
If you debug the $myAuthors variable, you'll see that it gets a simple array of ids.
'conditions' => array( 'field' => array() ) will get parsed as "WHERE field IN (...)". In this example, we get all models WHERE 'Author.id' IN $myAuthors.
The short $this->Story->find() call is the beauty of Cake. It will automatically find all Story models matching the specified conditions, and it will find the other models associated with each found Story. (You can tell it not to find associated models by turning off Recursive or using Containable behavior.)
Debugging the $stories variable will show you a structure like the following:
Array
(
[0] => Array
(
[Story] => Array
(
[id] => 1
[title] => 'Common Sense'
[published_date] => 1776-01-10
)
[Author] => Array
(
[id] => 1
[name] => 'Thomas Paine'
)
[Comment] => Array
(
[0] => Array
(
[id] => 1
[text] => 'Revolutionary!'
[Author] => Array
(
[id] => 3
[name] => 'Silence Dogood'
)
)
[1...n] => ...
)
)
[1...n] => ...
)
You can then use that structure in your View to display data as you desire.
I have a feeling there should be a way to do it with just 1 query, but this way works and doesn't require you to do any subqueries or insert custom SQL into your Cake find() call.
Gary, it is difficult to answer this without knowing your models. Assuming you have got correctly defined schema and models it should be relatively easy.
For your find queries order by date DESC and retrieve the last 10 rows. (this would give you last 20 results for example)
Merge the result sets into one and sort by date DESC again. It's then some trivial rendering logic to render the strings as you need.
Well, I would do something like this
Create the following Relationships
User hasMany StoryName
User hasMany Comments
StoryName hasMany Comments
Comments belongsTo User
Comments belongsTo StoryName
That way would be really easy to retrieve all the information needed, since you could fetch all the needed data though the User model with something like
$this->User->recursive = 2;
$this->User->find('all', $params) *// on $params you could use the conditions that the user retrieved should be friends to the current user*
Hopefully that would give me an Array populated with the following structure
[USER]
[POSTS]
[COMMENTS]
And was just a matter of passing it to the view and using a foreach in the view to create the Html
I don't know if you you already have other models setup (User, Story, etc.). But to keep the answer to the point, I'll just focus on Activity model (activities table): You can use this table like a log table.
A simple table: id, user_id, story_id, created (datetime), action_type(0 for post story, 1 for comment on a story, for example). Then it's simply a matter of querying this table to find the activities.
Or a more generic, extensible one: id, subject_id, subject_type (User or whatever is acting), verb_type, object_id, object_type, created. You don't really have to bind this model to any other model (Although you can with the 'conditions' when you specify relationship) or bindModel on the fly.
I'm using the Containable behavior to get a list of Comments (belongsTo Post, which belongs to Question; Question hasMany Post, and Post hasMany Comments; all of these belong to Users).
$data = $this->Question->find ( 'first',
array ('contain' =>
array ('User',
'Post' => array ('User', /* 'order' => 'User.created DESC'*/ )
)
)
);
It works, when I comment out the section in comments above. I suppose this is to be expected, but what I want is all of the Posts that are found, should be sorted in order of the 'created' field of the 'User' they belong to. How do I accomplish this deeper level sorting in CakePHP? I always get, "Warning (512): SQL Error: 1054: Unknown column 'User.created' in 'order clause'"
Thanks for your help!
Also, you might be trying to group on a related table from a find call that doesn't use joins.
Set your debug level to something greater than 1 so you can see the query log and make sure that Cake isn't doing two queries to fetch your data. If that is the case then the first query is not actually referencing the second table.
If you want to manually force a join in these situations you can use the Ad-Hoc joins method outlined by Nate at the following link.
http://bakery.cakephp.org/articles/view/quick-tip-doing-ad-hoc-joins-in-model-find
I have found two ways to get around this.
The first is to define the second level associacion directly in the model.
Now you will have access to this data everywhere.
It should look something like this.....
var $belongsTo = array(
'Foo' => array(
'className' => 'Foo', //unique name of 1st level join ( Model Name )
'foreignKey' => 'foo_id', //key to use for join
'conditions' => '',
'fields' => '',
'order' => ''
),
'Bar' => array(
'className' => 'Bar', //name of 2nd level join ( Model Name )
'foreignKey' => false,
'conditions' => array(
'Bar.id = Foo.bar_id' //id of 2nd lvl table = associated column in 1st level join
),
'fields' => '',
'order' => ''
)
);
The problem with this method is that it could make general queries more complex than they need be.
You can thus also add the second level queries directly into te find or paginate statement as follows: (Note: I found that for some reason you can't use the $belongsTo associations in the second level joins and will need to redefine them if they are already defined. eg if 'Foo' is already defined in $belongsTo, you need to create a duplicate 'Foo1' to make the association work, like the example below.)
$options['joins'] = array(
array('table' => 'foos',
'alias' => 'Foo1',
'type' => 'inner',
'conditions' => array(
'CurrentModel.foo_id = Foo1.id'
)
),
array('table' => 'bars',
'alias' => 'Bar',
'type' => 'inner',
'foreignKey' => false,
'conditions' => array(
'Bar.id = Foo1.bar_id'
)
)
);
$options['conditions'] = array('Bar.column' => "value");
$this->paginate = $options;
$[modelname] = $this->paginate();
$this->set(compact('[modelname]'));
I hope this is clear enough to understand and that it helps someone.
Check your recursive value. If it's too limiting, it will ignore the containable links, IIRC. I remember bumping into this a few times. I'd try containing multiple models, but my recursive option was set to 0 and nothing would get pulled. For your example, I'd think that a value of 1 (the default) would suffice, but maybe you've explicitly set it to 0 somewhere?
You can add before your call to find() the following:
$this->Question->order = 'Question.created DESC';
Yeah, I couldn't work out how to sort based on the related/associated model, so ended up using the Set::sort() method. Checkout this article for a good explanation.
// This finds all FAQ articles sorted by:
// Category.sortorder, then Category.id, then Faq.displaying_order
$faqs = $this->Faq->find('all', array('order' => 'displaying_order'));
$faqs = Set::sort($faqs, '{n}.Category.id', 'ASC');
$faqs = Set::sort($faqs, '{n}.Category.sortorder', 'ASC');
...And yes, it should probably be a Category->find() but unfortunately the original developer didn't code it that way, and I didn't wanna rework the views.