Retrieving a belongsTo relationship data in a hasOne relationship (deep associations) - cakephp

I'm building this CakePHP application with a Teaser model, each Teaser has one Location.
Each Location belongs to a Country and a State.
On the index side of Teaser (and view) I want to display the Country.abbr and State.title of the Location associated with this Teaser
Associations
Teaser
public $hasOne = array('Location');
Location
public $belongsTo = array('Teaser','Country','State');
Country
public $hasMany = array('Location');
State
public $hasMany = array('Location');
What have I tried
Default
If I do the following
$teasers = $this->Teaser->find('all');
in my TeasersController then it won't fetch the correct Country and State. I have seen it working and I had no problem to fetch a country title in my view through the Teaser model as follows:
$teaser['Location']['Country']['title'];
$teaser['Location']['State']['title'];
So first this worked, after working on some other stuff I noticed this stopped working (not sure if other stuff had influenced this, but I didn't touched the model. As a good programmer I started looking here.
Containable behaviour
I've read about the Containable behaviour being amazing so I looked up on how to use it and came up with the following.
$this->Teaser->Behaviors->attach('Containable');
$teasers = $this->Teaser->find('all', array(
'contain' => array(
'Location' => array(
'Country',
'State'
),
'User',
),
));
$this->set('teasers', $teasers);
This does not work, it gives the following error:
Model "Location" is not associated with model "Country"
Recursive parameter
I've tried setting recursive to 2 (or -1 in association with the Containable Behaviour) as it was a solution before for others, but I've seen comments on how you better stick with the Containable Behaviour.
Actual question
So, Does anyone has an idea what I'm doing wrong here? Is the syntax wrong or am I getting these associations all mixed up?
Related question
Furthermore a related question on the side I've left out in the main part, but is an association between Country and State a wise idea? Our would this just complicate things even more?

Rather than adding the containable behavior on the fly, I would add the following properties to your AppModel, since using containable should be the default state, rather than the exception.
public $actsAs = array('Containable');
public $recursive = -1;
In case you weren't already aware, the reason people say use containable instead of recursive is that recursive will get everything all the time, even when you don't need it. So recursive is easy to use, but it encourages bad habits.
As for your query, I don't see anything that jumps out at me as being incorrect. I would try, as a test only, doing
$this->Teaser->recursive = 2;
$teasers = $this->Teaser->find('all');
Then see what it comes back with. If it doesn't contain your associated data, you'll know there's something wrong with the associations themselves. If it does get the associated data, then it's something with the query.
As for your related question, that's more of design question which doesn't necessarily have a right answer.

Related

Removing a belongsTo from a model shows warning Model 'x' is not associated with model 'y'

I am new to CakePHP and I am having to maintain/develop a CakePHP application and struggling with removing a BelongsTo in one of the models.
The code has been copied from another project that used a lot more related tables. In the new project I do not need the relationships because it is creating LEFT JOINS in the queries that I really want to get rid of in order to speed things up. So I went in to the model and removed the $belongsTo property..
When I go to the home page I now get a warniing:-
Warning (512): Model "Product" is not associated with model "ParentProduct"
[CORE/Cake/Model/Behavior/ContainableBehavior.php, line 343]
If I turn off debugging it is not shown but I would like to know why this message is being generated. I am unable to find any information on how I should go about removing the belongsTo relationships.
Any help appreciated.
So I went in to the model and removed the $belongsTo property
Because you're asking for it
This kind of error is displayed when using the containable behavior incorrectly.
E.g. Consider User hasMany Article, and the following code:
$User->find('all', array(
'contain' => array(
'Article'
)
));
This would return all users and their articles (only), irrespective of any other associations that exist.
If an association is requested that isn't defined:
$User->find('all', array(
'contain' => array(
'Article',
'Comment'
)
));
Containable will generate a warning so that you, the developer, can know that there will be no Comments in your results which either indicates a typo or another kind of development error (Ah... it's User<-Article<-Comment).
This is in-keeping with what you describe since the model associations have been modified but (evidently) not the application code.
The message is been generated (most probably) because in your controller (check the ParentProductsController first) is something like
$this->ParentProductsController->Product->find();
That association of models no longer exists because you wanted to get rid of it, so there's no way for ParentProducts to call Product. You need to change calls like that to
ClassRegistry::init('the_name_of_the_model');
(in this case ClassRegistry::init('Product');) or load the model with
$this->loadModel('Product');
Now, this only is necessary when you call models not related to the current controller. If you are calling Product->find from the ProductsController, there will be no error.
Ok, that's the cause of the error.
But now: I don't recommend you deleting the association. There's really no need and your removing a logical association in the code even though it's still there in the database. There are other ways to avoid left joins and useless queries to the database without cutting someone a limb.
If you want to speed things up, read about Containable Behavior, and set all models to $recursive = -1 (recursive doc). That way you won't get LEFT JOINS unless you explicitly say so.

elements or view extensions or something else? reference app?

In cakephp [2.2] I have baked everything and my "people" view is quite busy with relations and phones and addresses and other related data. I do want all of that information visible in the people view, though not quite in the baked layout.
How should I handle those portions of the related data? I'm not sure if I should use elements or extended views or plugins or what, I'm kinda new to this and the documentation wasn't clear to me (at my level) which should be used when. The baked code seemed to be a monolithic approach, so I didn't get much help looking there.
Once the user chooses to edit a phone number (for instance) from the listing on the person view, it takes them to the phone edit view and then returns them to the phone listing (index view) and not the person view that they were on. How do I get them back to the person view instead?
The blog example they provide is nice, but is there a "reference" application somewhere for cakephp that demonstrates best practices on a wide variety of their features? I couldn't find one, or anything more than just a simple app example.
Thanks, I appreciate the guidance.
This is a rather broad question, but I'm going to try and answer it. I'm not sure how advanced you're programming knowledge is, so forgive me if I'm rehashing things you already know. First, this article was a great help when I started to use the framework for the first time as it explains what code should go where and why. It's the closest I've seen to a "reference application", which would actually be a great learning tool. You could try and have a look at some of the higher profile Cake applications, like Croogo (a Cake-based CMS). But the codebase is bound to be a little bit complex.
Personally I would use elements when you want to actually reuse them in different views. The problem however, is feeding the element its data. There's a method called requestAction, but even the manual states that this should be used with moderation and in combination with caching. The problem is that using a lot of requestAction calls in different elements litters your Controllers with methods and doesn't adhere to the "Skinny Controllers, Fat Models" mantra.
I would put most of the related data calls in their respective Models and call those Model methods from the Controller and feed them to the View. So let's say you want the 10 latest PhoneNumbers and related Users.
You would have a method in your PhoneNumber model which returns an array of users and their phonenumbers. Use the Containable behaviour to limit the number of related models which are returned. The code below is an example, so the practical implementation might vary:
public function getRecentPhoneNumbers($limit=10) {
$phoneNumbers = array();
$phoneNumbers = $this->find('all', array(
'limit' => $limit,
'contain' => array('User'),
'order' => 'PhoneNumber.id DESC'
));
return $phoneNumbers;
}
If the PhoneNumber and User model are properly related you would be able to call getRecentPhoneNumbers() from the User model:
$this->PhoneNumber->getRecentPhoneNumbers(10)
Or from the Users Controller:
$this->User->PhoneNumber->getRecentPhoneNumbers(10)
Say you have an element which shows a list of those 10 numbers and it accepts a variable called $recentPhonenumbers, you then set the variable in the relevant UsersController method with the returned array from the getRecentPhoneNumbers call:
$this->set('recentPhonenumbers', $this->User->PhoneNumber->getRecentPhoneNumbers(10));
This will make it available to the View that contains the element.
The extended views are relatively new (from Cake 2.1 and onwards) and I haven't used them, but seem a great way to create conditional markup.
As for the second question, redirecting the user to the person view, rather than the index view. This is a matter of adjusting the redirect (see the manual for more details) in the edit() method of the Controller. Standard baked edit() methods accept an $id parameter you can use this to redirect to the view() (which probably also accepts an $id paramater).
So the redirect probably looks something like this:
$this->redirect(array('controller' => 'users', 'action' => 'index'));
Change it to:
$this->redirect(array('controller' => 'users', 'action' => 'view', $id));

How can I minimize the 'contain' queries in CakePHP?

I have three models, Users, Comments and Pages.
Users has many Comments, and Comments belong to Pages.
All models use the containable behavior, and default to recursive -1.
If I call a find() query on Comments, with the contain request including the Page model's field, this correctly returns the results using a single query, automagically joining the Page table to the user.
If I call a similar query from the User model (containing Comment and Comment.Page), the result is a query to source the Comments, followed by a query per comment to source the relevant Page.
Is there a way to configure the models to maintain the JOIN optimisation? I assumed the belongsTo declaration on the related model (Comments) would follow through to the host model (Users).
UPDATE
I should clarify, my question used a simplified version of my actual case study. Although the minimal solution I require would include this initial Model hasMany Model belongsTo Model structure, I am also looking for the solution at one or more additional belongsTo Models down the chain (which, I though, would automagically use LEFT JOINs, as this would be feasible).
Hmm that's interesting. That's a sort of optimization that should be implemented in the core :)
At any rate, I think you could get the same results (perhaps formatted differently) by building the query a little differently:
$this->User->Comment->find('all', array(
'conditions' => array(
'Comment.user_id' => $userId
),
'contain' => array(
'User',
'Page'
)
));
By searching from the Comment model, it should use two left joins to join the data since they are both 1:1 relationships. Note: The results array may look a little different than from when you search from the User model.
So are you asking if there is an easier way to just contain all your queries? If you want to contain everything within the current controller. You could do the contain in the beforeFilter() callback and it would apply to all your queries within that controller.
I am not quite sure if I understand your question, but I think you have a problem with the many sql-calls for the Comment -> Page linkage? If that is correct, then
try linkable behaviour which reduces sql calls and works almost as contain does
or if its pretty much the same data you want, then create a function in a specific model from where you are happy with hte sql calls (for example the Comment-Model) and call it from the user model by $this->Comment->myFindFct($params);
hope that helps
EDIT: one thing that comes to my mind. You were able to change the join type in the association array to inner, which made cake to single call the associated model as well
I find a good way to do this is to create a custom find method.
As a for instance I'd create a method inside your User model say called _findUserComments(). You'd then do all the joins, contains, etc.. inside this method. Then in your controllers, wherever you need to get all of your user's comments you would call it thusly:
$this->User->find('UserComments', array(
"conditions" => array(
'User.id' => $userId
)
));
I hope this helps.
If model definition like bellow:
Comment model belongs to Page and User.
Page belongs to User and has many Comment.
User has many Page and Comment
code bellow will return one joined query:
$this->loadModel('Comment');
$this->Comment->Behaviors->attach('Containable');
$queryResult = $this->Comment->find('all', array(
'contain' => array(
'User',
'Page'
)
));
The code bellow will return two query. Page and User joined into one query and all comment in another query
$this->loadModel('Page');
$this->Page->Behaviors->attach('Containable');
$queryResult = $this->Page->find('all', array(
'contain' => array(
'User',
'Comment'
)
));
and also bellow code will return three query, one for each model:
$this->loadModel('User');
$this->User->Behaviors->attach('Containable');
$queryResult = $this->User->find('all', array(
'contain' => array(
'Page',
'Comment'
)
));

fetch only 'enabled' records from the database in cakephp v1.3

Is there any way of fetching only those records of a model which have 'status = 1' in cakephp v1.3 ?
I have created a field named 'status' in every table of my web application.
I have a model named 'Message'. What I want is that only those messages are displayed, included in search results which have 'status = 1'. Messages with a 'status = 0' won't be displayed and won't be included in search results too.
Please help me if I can do it the cakePHP way, for now as a temporary solution I am using the 'status = 1' in the conditions array of every find of messages.
Thanks
I'd include this in the beforeFind() callback method. You can include it in the callback for every model (but only once for each model--not for each query) or you can apply it to your AppModel so that all of your other models inherit it.
In the beforeFind() callback, you can modify the structure of the query. In this case, you'd apply the condition array( 'status' => 1 ) to your incoming query. Dump the argument data structure to see what's coming in and I think it will make sense to you.
Note that if you chose to do this in your AppModel and have to create a model-specific beforeFind callback in the future, you'll need to be sure that you call parent::beforeFind() in your subclass' beforeFind() method.
Found the answer over here The beforeFind() callback comes handy for this.
like this
public function beforeFind($conditions) {
if(!isset($conditions['conditions']['Message.status']) && !isset($conditions['conditions']['status'])) {
$conditions['conditions']['Message.status'] = 1;
}
return $conditions;
}
Basically you are speaking for Soft deletable behavior. There is one here For me it's not perfect, but for simple app it could do the job. In the comments you can see the improvements too :)
But extending the beforeFind() in the AppModel should be ok too.

cakePHP, Model Recursion

I have a few models, all with appropriately named model files.
$this->Property->PropertyImage->Image->read();
All linked accordingly. Problem is, the recursive model is not able to attach to all the relationships and for some reason is returning AppModel data type when i do a var_dump on $this->PropertyImage. When i do a var_dump($this->Property); i get data type: Property.
What is going on here, what would cause this to happen? Also how can I fix this problem?
Do you hava a PropertyImage model in your application or is it a HABTM association? If you're having a Property which hasAndBelongsToMany Image, you need a pivot table (properties_images) in the database, but to access Image model from the PropertiesController, you'd do $this->Property->Image without anything in between.
Building on what Marko said, if you have a HABTM relationship your best bet is to use a join table, properties_images.
Then rather than doing a Property->PropertyImage->Image, you would just do a Property->Image->read/find().
What I failed to understand about the HABTM relationship is how to filter based on criteria in the related model. E.g., you cannot do this:
$this->Property->Image->find( 'all', array( 'conditions' => array( 'Image.id' => 7 ) ) );
Instead, you have to add the Containable behavior to the Property model, as described in the manual at http://book.cakephp.org/view/474/Containable.

Resources