CakePHP 2.4.4 How can I structure this find() with Containable? - cakephp

Tables
User(id)
Profile(id, *user_id*, type)
Attribute(id, *attribute_name_id*, value)
AttributeName(id, name)
ProfileAttribute(id, *profile_id*, *attribute_id*)
Relationships
The relationships are set up correctly (and go both ways, hasMany/belongsTo).
User hasMany Profile
Profile hasMany ProfileAttribute
Attribute hasMany ProfileAttribute
(could be written Profile hasMany Attribute through ProfileAttribute)
AttributeName hasMany Attribute
Goal
For a specified User id, with a find() in the User model, I only want the following fields, laid out as such:
$results[Profile.type][AttributeName.name][Attribute.value]
Is it even possible to retrieve results arranged like this? I've been playing around with Find and Containable for hours, but, first time trying to do anything complicated like this with Cake, I can't get the hang of it.
Thanks!
EDIT
I'm getting these results now, all that I need, but nowhere near the desired format above -can it be done as part of the find, or does it need to be sorted after?

Yep, it's possible. You just have to specify fields on containable:
$this->User->find('all', array(
'conditions' => array('User.id' => $id),
'fields' => array('id'),
'contain' => array(
'Profile' => array(
'fields' => array('id','type'),
'ProfileAttribute' => array(
'fields' => array('id'),
'AttributeName' => array(
'fields' => array('id','name'),
'Attribute' => array(
'fields' => array('id','value')
)
)
)
)
)
);
Be wary that when you use contain and fields options, you have to specify the id so it can make the association (check the docs)
EDIT: I don't know if you can group contained data as the docs didn't say anything about that, but probably you can, as they accept some parameters as in the main query. You can try it, adding group to any contained data that you want to group

Related

how to use cakedc/search plugin for searching across 3 different tables with 1 search bar?

I am using CakePHP2.4 and the search plugin https://github.com/CakeDC/search
I have the following
Employee hasOne EmployeeProfile
Employee hasMany Qualification
So I have a single search bar.
the search bar will search using LIKE through the following fields
Employee.name
EmployeeProfile.email
Qualification.title
how do I configure the model Employee->filterArgs for this search?
This is a cross-posting of the original issue here
The documentation includes an example.
'username' => array('type' => 'like', 'field' => array('User.username', 'UserInfo.first_name')),
You just have to make sure the models you're calling are available in your find() call. In the example the find will do a like on the users table username and on the user_infos first_name field at the same time.
I'd like to expand on this as I've been trying to setup a search on a hasMany relationship for a few hours and couldn't find anything. Mark mentionned "custom bindModel (as hasOne) for hasMany relationship (Qualification)". Here's how it's done :
$this->Employee->bindModel(array(
'hasOne' => array(
'Qualification' => array(
'foreignKey' => false,
'conditions' => array('Employee.id = Qualification.employee_id')
)
)
), false);
Just bind it before your paginate and add Qualification.title in your field list in your filterArgs

HABTM with self requires 2x the rows in join table?

I'm trying to build a CMS with Nodes as the main model. Each Node belongsTo a NodeType, and every Node can be related to any/every other Node.
So - thought this called for HABTM:
//Node model
public $hasAndBelongsToMany = array(
'AssociatedNode' => array(
'className' => 'Node',
'foreignKey' => 'node_id',
'associationForeignKey' => 'associated_node_id',
'joinTable' => 'node_associations'
)
);
The problem is, it seems like the only way it works is if I have TWO rows for each association.
Example with just one association row:
Nodes
ER (id=1)
George Clooney (id=2)
A single row in the join-table describing the relationship between those two nodes:
'node_id' = 1
'associated_node_id' = 2
Now - If I query for TV Shows and Contain it's Actor nodes:
$nodes = $this->Node->find('all', array(
'conditions' => array(
'Node.node_type_id' => '645' //tv shows
),
'contain' => array(
'AssociatedNode' => array(
'conditions' => array(
'AssociatedNode.node_type_id' => '239' //actors
),
)
)
));
This works, and I get ER -> George Clooney.
But - what if I want to pull all shows that George Clooney is in?
$nodes = $this->Node->find('all', array(
'conditions' => array(
'Node.node_type_id' => '239' //actors
),
'contain' => array(
'AssociatedNode' => array(
'conditions' => array(
'AssociatedNode.node_type_id' => '645' //tv shows
),
)
)
));
This doesn't work because it's looking for George Clooney's ID to be in the 'node_id' field, and ER's ID to be in the 'associated_node_id' field - when in reality they're reversed.
The only solution I've thought of is to keep two rows for EVERY association. But this seems overkill. But then I have to come up with some kind of custom something that makes sure to keep each duplicate in sync w/ the other every time an association is saved or deleted...etc - and this seems like a large can of worms.
Is there something I'm missing?
You could probably do it with a custom query, but to keep with standard Cake functions, one thing I can think of would be to declare two relationships between the Nodes:
public $hasAndBelongsToMany = array(
'AssociatedNode1' => array(
'className' => 'Node',
'foreignKey' => 'node_id',
'associationForeignKey' => 'associated_node_id',
'joinTable' => 'node_associations'
),
'AssociatedNode2' => array(
'className' => 'Node',
'foreignKey' => 'associated_node_id',
'associationForeignKey' => 'node_id',
'joinTable' => 'node_associations'
)
);
and then you could merge both arrays in an afterFind callback.
function afterFind($results)
{
foreach($results as &$result)
{
if(isset($result['AssociatedNode1']) || isset($result['AssociatedNode2']))
{
$associated_nodes = array();
if(isset($result['AssociatedNode1']))
{
foreach($result['AssociatedNode1'] as $associated_node)
{
$associated_nodes[] = $associated_node;
}
}
if(isset($result['AssociatedNode2']))
{
foreach($result['AssociatedNode2'] as $associated_node)
{
$associated_nodes[] = $associated_node;
}
}
$result['AssociatedNode'] = $associated_nodes;
}
}
unset($result);
return $results;
}
But this would force you to declare both AssociatedNode1 and AssociatedNode2 in the call to contain();
I'm not sure what the details of your use case are, but I've got a couple of alternate options for you:
You might look into using the Tree Behavior - this is built for storing things in trees, which it sounds like might be what you're doing. I haven't used it myself, so I'm not sure how applicable it is to your use.
On the other hand, if you store the relationships in a consistent direction (i.e. always TV Show->Actor) and know which direction your queries run (looking up the tree for TV Shows an Actor is in vs looking down for finding Actors in a TV Show), you should be able query AssociatedNode when you're going the reverse direction, e.g.
$nodes = $this->AssociatedNode->find('all', array(
'conditions' => array(
'AssociatedNode.node_type_id' => '239' //actors
),
'contain' => array(
'Node' => array(
'conditions' => array(
'Node.node_type_id' => '645' //tv shows
),
)
)
));
In this case, it might be better to use "ChildNode" instead of "AssociatedNode" for clarity.
But again, both of these answers depend on the particulars of your use case - nIcO's answer is a good general solution. It is (necessarily) awkward and possibly slower, but it abstracts away the awkwardness nicely.
One thing I've done in the past that might help is to make a model for the join table. I was able to store extra data in there and do whatever I wanted with my queries. Then on both sides of that join model just define a hasMany association (maybe a belongsTo as well). Then you can do a find using the join model and write something like (from a controller):
$this->Node->NodesNode->find('all', array('conditions'=>array("or"=>array('node_id'=>$id,'sub_node_id'=>$id))));
IMHO: there is nothing really forcing you to use cake conventions. I love cake, but sometimes both it and the ORMs complicate really easy things. You may just want to write your own query and parse the results yourself. It'd probably be faster than dealing with the overhead of another behavior or model plus you could probably write a way better query than the defaults given.
Oh and lastly, I'd watch out when you are using 1 model for multiple purposes. Really think if that one model should really be supporting everything. I've found that whenever I've done that I've just rewritten the entire thing within a year or two. You will quickly hit a bottle neck where some nodes need extra behavior in this way, others need something else and you have if statements (or maybe something more clever) scattered everywhere. Plus it really slows thing down to do crazy tree based queries in a db.

CakePHP: bi-directional self-referential hasMany Through associations

I'm trying to get my head around bi-directional self-referential hasMany through relationships in CakePHP (what a mouthful!).
I'm working on a picture matching website.
Pictures are associated to other pictures via a 'match' (the join model).
Each match has two pictures and stores the current rating and the total number of votes.
When viewing a picture, all of its related images from either direction should be available (via its matches).
I've started by defining a hasMany through relationship with a join model.
The pictures_matches join table has this structure:
id | picture_id | partner_id | rating | total_votes
My match join model association looks like this:
class PictureMatch extends AppModel {
...
public $belongsTo = array(
'Picture' => array(
'className' => 'Picture',
'foreignKey' => 'picture_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Partner' => array(
'className' => 'Picture',
'foreignKey' => 'partner_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
Each picture needs to be able to access its related pictures from either direction, but this is where my grasp is slipping.
It looks like I need to save both sides of the relationship but this destroys the extra data stored in the join model - with two db entries, voting could vary depending on the direction.
Can anyone shed any light on the best way to do this in CakePHP? I'm rather confused.
Is it possible to create the inverse relationships on the fly?
You can create realtions on the fly vie Model::bindModel(), very usefull stuff this would alow you to bind reverse relations or rather any direction you would like on the fly.
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html
Also using Containable behaviour you can create infinite chain of retriving your associated date ex.
contain('Picture.PictureMatch.Partner.PictureMatch.Picture.....')
Basically you can loop through all of your models as long as each chain is somehow related to the next one to explain it better simple example ( please disregard logic in it )
Circle belongsTo Square
Square belongsTo Triangle
So Triangle is not related to Circle ( directly ), but Square is kinda in between
Circle->find('all', array('...', contain => array('Square.Triangle'));
or to have more fun lets get circle by circle with loop around
Circle->find('all', array('...', contain => array('Square.Trinagle.Square.Circle'));
and so on, of course those example are useless and without any programming logic, but I hope you understand the point that you can loop trough infinite number of relations going back and forth.
I am not sure if this is the best solution but if you did this:
public $belongsTo = array(
'Picture1' => array(
'className' => 'Picture',
'foreignKey' => 'picture_id',
),
'Picture2' => array(
'className' => 'Picture',
'foreignKey' => 'picture_id',
),
'Partner' => array(
'className' => 'Partner',
'foreignKey' => 'partner_id',
),
);
then when you do a search you just search for ($this->data['Picture1'] == $var || $this->data['Picture2'] == $var) and so long as you have recursive set to 1 or 2 you should get back all the related data for that Picture.
I assume this is abandoned, but it's easily resolvable -- it has to do with the phase
destroys the extra data stored in the join model
That means your saves are running a deleteAll and inserting the record on matching... instead you need to find and update that record...
This can be done in a few ways, but the easiest is before your save call, look it up, and include the primary key in the match record data. Basically don't save it as a HABTM, and only save it as a hasMany if you've already tried to find an existing match record primary key (id) and updated the data to save with it.

Containable behavior issue with non-standard keys?

I'm working with a legacy database whose keys follow a convention, but not the cake convention, unfortunately, and I've bumped into something odd with the containable behavior -- it's pulling the wrong data. Here's my setup:
TechnologyIncentive belongsTo...
array(
'Incentive' => array(
'className' => 'Incentive',
'type' => 'inner',
'foreignKey' => false, # actually 'incentive_id', but we need to fool Cake
'conditions' => array( 'TechnologyIncentive.incentive_id = Incentive.incentive_id' ),
),
'Technology' => array(
'className' => 'Technology',
'type' => 'inner',
'foreignKey' => false, # actually 'incentive_tech_id', but we need to fool Cake
'conditions' => array( 'TechnologyIncentive.incentive_tech_id = Technology.incentive_tech_id' )
),
);
You can see that I've had to fool Cake into working with my non-standard keys by setting the foreignKey to false and defining the link in my where clause. So far so good.
Now, when I try to run a query from the TechnologyIncentive model:
$this->find( 'all', array(
'contain' => array( 'Incentive', 'Technology' ),
'fields' => array( 'Incentive.name', 'Technology.name', 'TechnologyIncentive.amount' ),
'conditions' => array( /** conditions... */ )
);
Everything works great. Stuff is nicely contained and I get exactly what I'd expect. Then I need to include a TechnologyGroup, which hasMany Technology and things breakdown for some reason.
Now my contain option looks like this:
'contain' => array( 'Incentive', 'Technology' => array( 'TechnologyGroup' ) )
And what I get back is an Incentive record contains an incentive record. That's not entirely surprising since I'm specifying a few fields in one place (the main fields option) and implicitly all fields in the contain option, but what's really weird to me is that they're different incentives. The "contained" incentive is just wrong.
Inspecting the SQL, it looks like the query was run with no effective where clause at all, so everything is being pulled and then artificially limited to a single record. Note the difference between $result['Incentive']['incentive_id'] and $result['Incentive']['Incentive']['incentive_id'].
Array
(
[Incentive] => Array
(
[incentive_id] => MD046
[name] => Incentive name
[category] =>
[Incentive] => Array
(
[incentive_id] => AK004
[code] => AK04F
[version] => 2
[category] => Incentive Category
)
)
)
Has anyone ever bumped into this? It's not a problem until I want to retrieve an extended record (TechnologyGroup). Any thoughts would be much appreciated.
It looks like this was directly attributed to the non-standard keys. The Containable behavior, more so, it seems, that other elements of Cake really wants the convention followed. Moreover, some of the key associations included a double underscore (__) in the field name and that caused other problems.
Kids, don't try this at home. Follow the conventions, even if it means manipulating a legacy database.

Retrieve posts with their latest comment in CakePHP

Say I have a model Post and a model Comment related as follows:
Post hasMany Comment
Comment belongsTo Post
How do use find('all') to retrieve every Post with its associated latest Comment?
I have tried defining a hasOne relationship in Post as:
var $hasOne = array('LatestComment' => array('className' => 'Comment', 'order' => 'LatestComment.created DESC'));
But when I do a Post->find('all') it returns every Post multiple times, once per each Comment, with LatestComment set to the different Comments.
You can add 'limit' => 1 to your array of parameters to only return one comment.
Alternatively, instead of defining another relationship, you can simply limit the number of comments returned when you perform the find, using the Containable behaviour.
$this->Post->find('all',array(
'contain' => array(
'Comment' => array(
'order' => 'Comment.created DESC',
'limit' => 1
)
)
);
This is useful if you want to filter any related sets without defining relationships - by author, or within a date range, for example.
Make sure that you add the Containable behaviour to any model that you reference.
To remove the duplicates you want to use: GROUP BY in your find all query. I believe Cake has a 'group' => option as well.

Resources