HABTM accessing array value in condition - arrays

Hey my problem is to define a condition for a HABTM Model
(int) 0 => array(
'Article' => array(
'id' => '',
'title' => '',
'body' => '',
'created' => null,
'modified' => null
),
'Channel' => array(
'id' => '',
'channelname' => '',
'created' => null,
'modified' => null
),
'Tag' => array(
(int) 0 => array(
'id' => '1',
'value' => 'example',
),
(int) 1 => array(
[maximum depth reached]
),
(int) 2 => array(
[maximum depth reached]
)
),
),
I want to define a condition that searches the Tag values for specific values .. like that:
$this -> Article -> find('all', array('conditions' => array('Tag.0.value' => 'test'))
So does someone knows how to "foreach" that array in this condition? thanks (:

You either need to:
Run your find from the other direction (using CakePHPs awesome Containable Behavior to get the Articles):
$this->Tag->find('all', array(
'conditions' => array(
'Tag.value' => 'test'
),
'contain' => array(
'Article'
)
));
Or, use JOINs. Run your find like you're doing, but do an INNER JOIN on tags where the tag value='test'
You cannot limit the main Model's results based on an associated model's conditions. The reason is, when you rely on recursive, or use Contain, CakePHP actually creates separate queries to pull in your different models' data. Therefore, you can't have a condition in one query affect the other query.
See similar question/answers:
How do I restrict query results based on sub-results in CakePHP?
Conditions in JOINed tables shows error CakePHP
cakephp contain filtering by parent model value
cakephp contain association conditions issue
Adding conditions to Containable in CakePHP

Related

CakePHP conditions for deep associations

I have some deep associations using containable and need to filter back the results. For the sake of this question, let's say we are selling cars and want to narrow the results down by features.
Car hasmany make hasmany model HABTM features
$options = array(
'order' => array('Car.price'),
'contain' => array(
'make',
'model' => array(
'order' => 'Model.name ASC'
),
'features'
)
);
$cars = $this->Car->find('all', $options);
How would I go about excluding all cars that don't have power windows (Features.name != power_windows).
Containable is only suitable for you to specify what models you wanted to include when fetching data, but not limiting the parent model from fetching data at all. One obvious symptom is that sometimes your parent data may have some null contained data.
So to achieve it, I think we should use joins here so you can specify condition:
$options = array(
'order' => array('Car.price'),
'contain' => array(
'make',
'model' => array(
'order' => 'Model.name ASC'
),
'features'
),
'joins' => array(
array(
'table' => 'features',
'alias' => 'Feature',
'type' => 'LEFT',
'conditions' => array(
'Car.id = Feature.car_id'
)
)
),
'conditions' => array(
'Features.name !=' => 'power_windows',
)
);
But one drawback of this is that you might have duplicated Car due to joining. That's a separate issue ;)

Doing manual joins in CakePHP does not invoke the constructor of the joined model (Virtual Fields needed)

I am trying to build a dynamic query through an array of joined tables. These generate dynamic aliases to avoid conflicts (non unique aliases).
However, in my joined tables - I have some virtualFields which are not getting processed. Upon further inspection, it appears that the joined tables __construct() functions are not getting called.
Is there a way to get virtualFields on a joined table in CakePHP v2.2.8?
Thanks
Manual joins do not use your models
Manual joins do not make use of your models. Using manual joins, you are manually defining a join and giving it an alias. Although this alias may be the same as an existing model, CakePHP will not use your model for the joined tables.
If you need virtualFields, depending on 'what' data is used, you may be able to move the virtualField to the 'main' model you're querying, for example:
$this->Foo->virtualFields['foobar'] = 'CONCAT(\'Hello \', Bar.name)';
$foo = $this->Foo->find(
'all',
array(
'fields' => array(
'Foo.name',
'Foo.foobar',
),
'joins' => array(
array(
'table' => 'bars',
'alias' => 'Bar',
'type' => 'INNER',
'conditions' => array(
'Bar.id = Foo.bar_id',
)
)
),
'recursive' => -1
)
);
debug($foo);
Returns;
array(
(int) 0 => array(
'Foo' => array(
'title' => 'Foo One',
'foobar' => 'Hello World'
)
),
(int) 1 => array(
'Foo' => array(
'title' => 'Foo Two',
'foobar' => 'Hello Planet'
)
),
)

Containable behavior doesn't return deeper model association and selecting fields

I want to limit the fields returned by a deeper association using containable.
My associations:
Game hasMany Review
The paginate and containable code:
$this->paginate = array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Game' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
),
)
);
$games = $this->paginate('Game');
Currently, all of the fields in the Review table are returned. 'ROUND(AVG(Review.score),1)' is never returned. How can I specify what fields I want returned from the Review association?
SQL dumps for two search results using #theJetzah's answer. The first is a search with one game as a result and the second is a search returning three games.
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` = (55)
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` IN (55, 56, 57)
Not a full answer, but an attempt to get it working :)
Approach1 (UPDATE: Containable doesn't support 'group by')
First of all, try to add the 'Game' model to the $uses array of your Controller, if it is not included yet, and re-organise the pagination array (as previously suggested by Sam), so that you'll be pagination the Game model itself.
Then, It may help to create a virtual field for the calculated score, but the results of 'Review' need to be grouped, otherwise you'll not be able to calculate the average score.
I'm not able to test this, but it may worth trying
something like this;
public $uses = array(
'Game',
// other models
);
public function myfunction()
{
$this->Game->Review->virtualFields['average_score'] = 'ROUND(AVG(Review.score),1)';
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array(
'Review.game_id,
'Review.average_score',
),
'group' => array(
'Review.game_id,
),
)
)
)
);
// Conditions can be passed to paginate,
// that way you can specify 'paginate' at
// one place and don't have to modify it
// to include the conditions
$games = $this->paginate('Game', $conditions);
}
Alternative approach: Using joins and a database-view
Apparently, the Containable behavior doesn't like group-by clauses; See this ticket for more information: Containable behavior does not implement 'group' option
CakePHP allows you to manually specify a join: Joining Tables
To simplify things and to prevent having to add a 'group by' for all fields, create a simple database-view in your database;
CREATE VIEW review_scores AS
SELECT
game_id,
ROUND(AVG(score),1) AS average_score,
COUNT(id) AS total_reviews
FROM
reviews
GROUP BY
game_id;
If you're unfamiliar with this; a database 'view' is basically a 'stored query', which can be accessed as if it was a regular table. See Create View
Then, use a 'manual' join, using the newly created database-view as the source-table. In your case, this will look something like this;
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name',
'ReviewScore.average_score',
'ReviewScore.total_reviews',
),
'joins' => array(
array(
'table' => 'review_scores',
'alias' => 'ReviewScore',
'type' => 'LEFT',
'conditions' => array(
'ReviewScores.game_id = Game.id',
)
)
)
)
);
Hope this helps
I think your array is a configured a little wrong, try:
$this->paginate = array(
'Game' => array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
)
)
);
$games = $this->paginate('Game');
As an aside, from personal experience, specifying the fields in a query doesn't always speed it up (certainly for small number of fields), assuming this is the motive for doing so. It does reduce memory occupancy but this is only relative to original size of the record and the number of records returned.

cakephp unable to sort on afterfind virtual fields with paginate

I have a query that I am running through paginate. This query contains a model ("PaymentException") that has an afterfind method that tacks on a copy of the last "ExceptionWorkflowLog", and calls it "LastWorkflowLog".
The query being passed to paginate:
$this->paginate = array(
'fields' => array(
'PaymentException.*', 'Procedure.id', 'Procedure.cpt',
'Procedure.expected_amount', 'Procedure.allowed_amount', 'Procedure.difference_amount',
'Claim.id', 'Claim.number', 'Payer.abbr'
),
'limit' => 50,
'joins' => array(
array(
'table' => 'procedures',
'alias' => 'Procedure',
'conditions' => array('Procedure.id = PaymentException.procedure_id')
),
array(
'table' => 'claims',
'alias' => 'Claim',
'conditions' => array('Claim.id = Procedure.claim_id')
),
array(
'table' => 'payers',
'alias' => 'Payer',
'conditions' => array('Payer.id = Procedure.payer_id')
),
array(
'table' => 'groups',
'alias' => 'Groups',
'conditions' => array('Groups.id = Claim.group_id')
)
),
'conditions' => $conditions,
'contain' => array('ExceptionWorkflowLog')
);
The resulting array (from the query that combines both "PaymentException", "ExceptionWorkflowLog", and "LastWorkflowLog") looks like below:
0 =>
'PaymentException' => array(fields and values),
'ExceptionWorkflowLog' => array(of ExceptionWorkflowLogs),
'LastWorkflowLog' => array(fields and values of the last indexed ExceptionWorkflowLog)
1 => ...
ExceptionWorkflowLog is mapped to PaymentException by PaymentException.id. It's a many to one relationship (thus the array of results under the ExceptionWorkflowLog).
I would like to use paginate to sort on the "updated" field on either the last indexed ExceptionWorkflowLog or the LastWorkflowLog.
Is there a way to do this with paginate? Currently, if I set the table heading to point to "LastWorkflowLog.updated", the query returns false because the query doesn't know what "LastWorkflowLog" is.
Since this has a couple hundred views, I figured I'd come back and post what I did. CakePHP's handling of joins is absolutely terrible. I rewrote the query to not use joins, but use contains. That seems to have solved it. I feel dirty.

CakePHP - find conditions with associations

I have an issue with a cakePHP find conditions. I have a Club model, a User model and a Post model :
Club hasMany Post
Club HABTM User
Basically, my clubs_users table also contains additional fields such as let's say 'limit' and 'diff' that respectively indicate the maximum number of posts a user want to display and how old those posts are allowed to be. I'd like to select the appropriate posts for every club related to a given user. I'm doing someting like this
$clubs = $this->Club->User->find('first', array(
'conditions' => array('User.id' => $id),
'contain' => array(
'Club' => array(
'fields' => array(
'id',
'name'
),
'Post' => array(
'fields' => array(
'id',
'title',
'description',
'created',
),
'order' => array('Post.created' => 'DESC'),
'conditions' => array(
'DATEDIFF(NOW(), `Post.created`) <= /* 1 */',
),
'limit' => '/* 2 */'
)
)
)
));
What should i put instead of 1 and 2 for this to work ? I tried ClubsUser.diff and ClubsUser.limit but i got an error stating that those fields were unknown in the where clause.
Any help would be welcome.
Thanks for reading.
edit
After bancer's comment, i looked deeper into the MySQL doc and it appeared that LIMIT expects only numeric arguments. So I now just want to return the posts that are not too old. My new find is (with the actual fields name)
$overview = $this->Club->Follower->find('first', array(
'conditions' => array('Follower.id' => $this->Auth->user('id')),
'contain' => array(
'Club' => array(
'fields' => array(
'id',
'name'
),
'Post' => array(
'fields' => array(
'id',
'title',
'description',
'created',
),
'order' => array('Post.created' => 'DESC'),
'conditions' => array(
'DATEDIFF(NOW(), `Post.created`) <= ClubsUser.max_time_posts',
),
'limit' => 10
)
)
)
));
It generates the three following SQL queries (i replaced the fields name by * for clarity reasons) :
SELECT * FROM `users` AS `Follower`
WHERE `Follower`.`id` = 1
LIMIT 1
SELECT * FROM `clubs` AS `Club`
JOIN `clubs_users` AS `ClubsUser`
ON (`ClubsUser`.`user_id` = 1 AND `ClubsUser`.`club_id` = `Club`.`id`)
ORDER BY `ClubsUser`.`role_id` DESC
SELECT * FROM `posts` AS `Post`
WHERE DATEDIFF(NOW(), `Post`.`created`) <= `ClubsUser`.`max_time_posts` AND `Post`.`club_id` = (1)
ORDER BY `Post`.`created` DESC
LIMIT 10
The last query returns the error : field 'ClubsUser.max_time_posts' unknown in where clause
Ideally, i would like to get a query close to the one below instead of the last two queries above :
SELECT * FROM `clubs` AS `Club`
JOIN `clubs_users` AS `ClubsUser`
ON (`ClubsUser`.`user_id` = 1 AND `ClubsUser`.`club_id` = `Club`.`id`)
LEFT JOIN `posts` AS `Post`
ON (`Post`.`club_id` = `Club`.`id` AND DATEDIFF(NOW(), `Post`.`created`) <= `ClubsUser`.`max_time_posts`)
ORDER BY `ClubsUser`.`role_id` DESC, `Post`.`created` DESC
LIMIT 10
Any ideas ?
You should not use HABTM if you have extra fields in the join table, because Cake actually deletes and recreates those joins. You should use a "hasMany through" association:
Club hasMany ClubUser
User hasMany ClubUser
ClubUser belongsTo Club
ClubUser belongsTo User
When you do your find on User, you just contain ClubUser then contain Club.
$this->User->find('all', array(
'contain' => array(
'ClubUser' => array(
'fields' => array(
'diff',
'limit'),
'Club'))));
More details here:
http://book.cakephp.org/1.3/view/1650/hasMany-through-The-Join-Model
and here:
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany-through

Resources