Cakephp, Retrieve Data for HABTM Models using conditional find - php

There are 2 Models: Project & Category that are bind with HABTM relationship.
I would like to perform a search from projects controller that can do the following:
FIND all DISTINCT Project.scedule WHERE Category.slug != 'uncategorised'
Apologies for the syntax, I'm no sequel expert.
What I have managed to do is to retrieve all projects that do not belong to Category uncategorised into an array however I'm not sure as to how to search again the array result for DISTINCT Project.schedule values (needed to fill out a form drop down)

Before answer this question,again I suggest you to read the HABTM in cookbook of CAKEPHP carefully,then you can finish jobs like this yourself.
$this->Project->bindModel(array(
'hasOne' => array(
'CategorysProject',
'FilterCategory' => array(
'className' => 'Category',
'foreignKey' => false,
'conditions' => array('FilterCategory.id = CategorysProject.category_id')
))));
$this->Project->find('all', array(
'fields' => array(DISTINCT (Project.scedule)),
'conditions'=>array('FilterCategory.slug !='=>'uncategorised')
));

Related

How to do join with 3 tables in CakePHP 2.x [duplicate]

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

Complex ordering in CakePHP using Containable

I`m having a problem with the Containable behaviour.
I would like to know if there is any way to access the contained model attributes for operations like ordering.
For example, I have a model B which belongs to a Model A. I need to order objects of B using an attribute (integer) of A. It would be something like:
'contain' => array(
'A' => array(
'B' => array(
'order' => 'A.integer_attribute'
)
)
)
I know that there are easier ways to do this without Containable, but for reasons which are not worth being detailed here, I need to use it. This is an abstract example, in truth model A belongs to other models and this is just a small part of a deep containable tree.
I'd be very glad with any help!
EDIT
OK, I'll try my best to describe the situation without being unclear:
I have 4 models: User, Category, Field and UserField, and their relationships are as follows:
Category hasMany User
Category hasMany Field
User hasMany UserField
Field hasMany UserField
The opposite of these relations are all belongsTo. The purpose here is that the user belongs to a category, which has many fields that he needs to fill with his information (city, state etc). The information he fills is stored in the UserField table, as each information needs to have its field and the user who provided it.
That said, I need to build a screen which displays, for each category, a list of users and their information. So, I retrieve all the categories and its fields, so I can build a table for each category. Each field has an attribute "no_order", which is the number that indicates the order in which the field appears.
At the same time, I need all of each category's users to display them correctly in the tables. Finally, and there's the problem, I need to have UserField objects ordered by the "no_order" of their respective fields, for each user. So I ended up with something like:
$categories = $this->Category->find('all', array(
'order' => 'Category.name',
'contain' => array(
'Field',
'User' => array(
'UserField' => array(
'order' => 'Field.no_order'
)
)
)
));
But this doesn't work, since UserField cannot reach its respective Field's no_order from there.
I apologize if this wasn't clear enough, but for anyone who would spend a little while reading this, I would be VERY grateful for your help!
I am not shure for what do you want so, but I thing that should use joins of cakephp
$A = $this->A->find('all', array(
'joins' => array(
array(
'table' => 'B',
'alias' => 'B',
'type' => 'LEFT',
'conditions' => array(
'B.field_id = A.id'
)
)
),
'conditions' => array(
'B.field' => 'if_you_want_more_conditions'
),
'contain' => array(
'A' => array(
'B' => array(
'order' => 'A.integer_attribute'
)
)
),
'fields' => array('A.field','B.field'),
'recursive' => -1
));
I finally came up with a solution. I don't know how efficient it is, but there it goes:
'contain' => array(
'Field',
'User' => array(
'User.privilege = "Solicitante"'
'UserField' => array(
'order' => '(SELECT f.no_order FROM fields AS f WHERE UserField.field_id = f.id LIMIT 1)'
)
)
)
Having the raw query solved my problem. Hope it helps anybody who comes across a similar problem!

CakePHP: Using multilevel Containable Behaviour

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.

CakePHP/Croogo: A veeery complex association

When I was working on my current project, I ran into a rather complex issue. I'll point out my problem much more detailed right now:
There are three Models: User, UsersExtendedField and UsersExtended.
UsersExtendedField contains custom fields that can be managed manually. All custom fields can be shown in the Users view as well, and be filled out of course. The values are then stored in UsersExtended, which has got two foreignKeys: user_id and field_id.
The relations look like this: User hasMany UsersExtendedField, UsersExtendedField hasMany UsersExtended, UsersExtended belongsTo User, UsersExtendedField.
The problem: When accessing the Users view, a form with user information input is shown. Any UsersExtendedFields are available as well, and since these hasMany UsersExtended, they've got plenty of UsersExtended values. But I want to reduce those to only the value(s) that belong to the User, whose view is shown at the moment. Here are my (desired) relations:
Croogo::hookBehavior('User', 'Crooboard.ExtendedUser', array(
'relationship' => array(
'hasMany' => array(
'UsersExtendedField' => array(
'className' => 'Crooboard.UsersExtendedField',
'foreignKey' => '',
'conditions' => array('status' => 1)
),
),
),
));
class UsersExtendedField extends AppModel {
var $name = 'UsersExtendedField';
var $displayField = 'fieldname';
var $hasMany = array(
'UsersExtended' => array(
'className' => 'Crooboard.UsersExtended',
'foreignKey' => 'field_id',
'conditions' => array(
'UsersExtended.user_id = User.id'
)
),
);
}
This is not the full code, these are the important parts. The problem starts right where I wrote 'UsersExtended.user_id = User.id'. Obviously, this won't work. But I do not have any idea how to access the User.id here. I also could not imagine a HABTM structure to solve this task. Do you have any idea how to get the semantics of this 'UsersExtended.user_id = User.id' to work?
Thank your very much for taking the time to read through this and helping me!
It sounds like you need to set up your HABTM relationship properly.
You already have the join table, UsersExtended, which contains your foreign keys.
Remove all previous relationships and set up HABTM in each of your User and UserExtendedField models.
The relationship code in your User model would look like this:
var $hasAndBelongsToMany = array(
'UsersExtended' => array(
'className' => 'UsersExtended',
'joinTable' => 'UsersExtended', //assuming this is the
//name of that model's table
'foreignKey' => 'user_id',
'associationForeignKey' => 'field_id'
)
);
For more information check out the page in the cakephp book
In addition, this blog post helped me grasp the relationship concepts when I was learning cakephp.

containable on hasMany relationship

Greetings,
I am trying to tear down query returned from find call using containable in CakePHP.
for example I have 2 models, User and Post. User hasMany Post.
Now when I am using containable on find call like so:
$User->id = 1;
$User->find('first', array(
'fields' => array('id'),
'contain' => array('Post')
))
It will not return the associated Post, instead will just return the id of the user.
It works however if I am trying to fetch the data the other way around.
i.e this works:
$Post->find('first', array(
'fields' => array('id', 'user_id'),
'conditions' => array('Post.user_id' => 1),
'contain' => array('User')
))
this doesn't:
$Post->find('first', array(
'fields' => array('id'),
'conditions' => array('Post.user_id' => 1),
'contain' => array('User')
))
From the returned values I then suppose that for the containable to works, the foreignKey has to be in the fields.
How then would I be able to filter out the User fields on the first call as the association of user is stored in Post.user_id?
Any help is greatly appreciated! Thank's.
-aw
As larryb82 said you'll need to define the relationship in both directions in order to retrieve Posts data from the User model
A user has many posts.
A post belongs to an user
CakePHP Doc example

Categories