CakePHP: Retrieving records based on field in related model - php

I'm trying to search based on a field in a related model. I can do so with the belongsTo model, but not the hasMany model. I'm sure this is something simple I'm overlooking but I can't see it. Could someone point me in the right direction (or even the cakebook page I need - searched, but didn't find anything that looked like this). Thanks
In groups controller: (this doesn't work)
$group = $this->Group->find('all', array('conditions' => array('Voucher.id' => '55')));
This does:
$group = $this->Group->find('all', array('conditions' => array('TourOperator.id' => '3')));
Background file snippets:
Group model:
var $belongsTo = array(
'TourOperator' => array(
'className' => 'TourOperator',
'foreignKey' => 'tour_operator_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
var $hasMany = array(
'Voucher' => array(
'className' => 'Voucher',
'foreignKey' => 'group_id',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => 'Voucher.date, Voucher.meal_type_id'
)
);
Voucher model:
var $belongsTo = array(
'Group' => array(
'className' => 'Group',
'foreignKey' => 'group_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
Tour Operator model:
var $hasMany = array(
'Group' => array(
'className' => 'Group',
'foreignKey' => 'tour_operator_id',
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
);
Update (as posted in comment below, but clearer to read here)
I've ended up using this.
$groups = $this->Group->Voucher->find('all', array('fields' => 'DISTINCT group_id', 'conditions' => array('Voucher.status' => 'pending')));
$group_ids = Set::extract($groups, '/Voucher/group_id');
$data = $this->Group->find('all', array('conditions' => array('Group.id' => $group_ids)));
I get a distinct list of group IDs matching my criteria, create an array of just the IDs and then use that to pull the groups so that the arrays are ordered as I expect them in my view.

Can you recast your query to $this->Voucher->find, and then use the associated Group data returned from that query? As you say, there's no problem finding groups by their TourOperator id, and the Groups/Voucher relationship looks like it's the inverse of that.

With CakePHP's Containable behaviour you can't use conditions on related data in hasMany and hasAndBelongsToMany associations. This is because it uses seperate queries to get those associations.
With the Linkable behaviour, however, you can use data from hasMany associations to filter records. The behaviour can be downloaded here: https://github.com/Terr/linkable
Linkable uses standard JOINs to, well, join related data. This means it doesn't pull in all the related data (ie, all the Vouchers related to your Group(s)), just like a normal SELECT. However, it can work together with Containable to do both.
You can find examples of how Linkable works in the README.
(disclaimer: I currently maintain Linkable, but that is not the reason I point to it)

Related

Join single item from related table based on min of field

I have an Item model which has the following associations:
public $hasOne = array(
'Project' => array(
'className' => 'Project',
'foreignKey' => 'item_id'
)
);
public $hasMany = array(
'ItemPic' => array(
'className' => 'ItemPic',
'foreignKey' => 'item_id',
'dependent' => false
)
);
I am wanting custom data for different views of Item. It seems like CakePHP automatically includes Project data (maybe because it is hasOne?) and does not include the ItemPic data. In the index I really don't even want the Project data... however, I do want the ItemPic data. For each Item record pulled, I want a single ItemPic record joined to it. This ItemPic should be basically ItemPic.item_id = Item.id and ORDER BY ItemPic.rank LIMIT 1.
The purpose of this is basically so that in the index I can show a list of Items and a picture associated with each item. I would like all of the images along with the Project data in the view for a single Item, but not in the list/index.
I was told I could use containable like this:
// In the model
public $actsAs = array('Containable');
// In the controller
$this->paginate = array(
'conditions' => $conditions,
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
);
The above actually works how I want... however, I was also told that doing this would cause an extra query to be ran for every single Item... which I feel I should avoid.
I tried doing this, but I get duplicate data and it doesn't attach any ItemPic data:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'table' => 'item_pics',
'alias' => 'ItemPic',
'type' => 'LEFT',
'conditions' => array(
'ItemPic.item_id = Item.id'
),
'order' => 'rank ASC',
'limit' => 1
)
)
);
$paginated = $this->Paginator->paginate();
Can you please try this:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'table' => 'item_pics',
'alias' => 'ItemPic',
'type' => 'LEFT',
'conditions' => array(
'ItemPic.item_id = Item.id'
),
'order' => 'rank ASC',
'limit' => 1
)
),
'fields' => array('Item.*','Project.*','ItemPic.*')
);
In the fileds section you may or may not assign "Item" , "Project" according to your requirment.
Thanks

Attempt to replace CakePHP model binds with Containable is not working

I am trying to get a list of all Visitors with only the latest visit information and the associated VisitType description for an API call and I need to be able to do this in one find. Previously we had the second code block below, but this returns multiple records for Visitors when the Visitor has more than one associated Visit record (one for each Visit Record). I just need to get the most recent (MAX(Visit.visit_date) but obviously cannot use this if we intend to join against VisitType) with it's associated VisitType.type_description.
The beforeFind() function is on the Visitor model since we are trying to get a list of Visitors.
I am getting the follow issue, when I tried to use the Containable code below:
Warning (512): Model "Site" is not associated with model "Site" [CORE/Cake/Model/Behavior/ContainableBehavior.php, line 343]
Warning (512): Model "SubjectStatus" is not associated with model "SubjectStatus" [CORE/Cake/Model/Behavior/ContainableBehavior.php, line 343]
I have these associations established:
Visitor hasMany Visits, a Visit hasOne Visitor
Visitor hasOne Site, a Site hasMany Visitors
VisitorStatus hasMany Visitors, Visitor belongsTo VisitorStatus
Visit hasOne VisitKind, VisitKind hasMany Visits
Failing Containable code:
public function beforeFind($options, $primary=false) {
if ($this->__AjaxDataLoad) {
// Replacing a bunch of Model rebinds with Containable
$this->Behaviors->load('Containable');
$this->contain(array(
'Site' => array(
'conditions' => array('Site.id = Visit.site_id'),
'field' => array('Site.site_name')
),
‘VisitorStatus' => array(
'conditions' => array(‘VisitorStatus.id = Visitor.visit_status_id'),
'field' => array(‘VisitorStatus.status')
),
'Visit' => array(
'conditions' => array('Visit.subject_id = Visitor.id'),
'order' => 'Visit.visit_date DESC',
'limit' => 1,
'VisitType' => array(
'conditions' => array('Visit.visit_type_id = VisitType.id'),
'fields' => array('VisitType.type_description'),
)
)
));
}
}
Previously, the follow code "works" with the undesirable side-effect of including multiple rows per visitor, one row per Visit:
public function beforeFind($options, $primary=false) {
if ($this->__AjaxDataLoad) {
$this->unbindModelAll();
$this->bindModel(array(
'belongsTo' => array(
'Site' => array(
'className' => 'Site',
'foreignKey' => 'site_id',
),
‘VisitorStatus' => array(
'className' => ‘VisitorStatus',
'foreignKey' => ‘visitor_status_id',
)
),
'hasOne' => array(
'Visit' => array(
'className' => 'Visit',
'foreignKey' => ‘visitor_id',
'order' => 'Visit.visit_date DESC’,
‘limit’ => 1 // <—————————————— THIS DOES NOTHING
),
'VisitType => array(
'foreignKey' => false,
'conditions' => array("Visit.visit_type_id = VisitType.id"),
'fields' => array('VisitType.type_description’)
)
)
));
}
}
Is there something wrong with the Containable structure I am attempting?
I have configure Debug = 2, CakePHP version 2.2.5
The solution I ultimately came up with complete circumvents CakePHP relationship techniques. It came down to using virtualFields and a subquery as the last resort.
public function beforeFind($options) {
if ($this->__AjaxDataLoad) {
$options['joins'] = array(
array(
'table' => 'visits',
'alias' => 'Visit',
'type' => 'LEFT',
'conditions' => array('Visit.visitor_id = Visitor.id')
)
);
$options['group'] = array('Visitor.id');
if (is_null($options['fields'])) {
$options['fields'] = array(
'Visitor.*', 'Site.*', 'VisitorStatus.*'
);
}
$this->virtualFields['last_visit_date'] = 'MAX(Visit.visit_date)';
$this->virtualFields['last_visit_kind'] = 'SELECT visit_kinds.kind FROM visits INNER JOIN visit_kinds ON visits.visit_kind_id = visit_kinds.id WHERE visits.visitor_id=Visitor.id ORDER BY visits.visit_date DESC LIMIT 1';
}
return $options;
}

cakephp - using loadmodel and getting data related to the loaded model

In a controller which is completely unrelated - a DoctorsController, I need to get some info from my ChildrenMedicine model and also the name of the medicine which is in it's own related Medicine model. So in my (unrelated) DoctorsController controller I use:
$this->loadModel('ChildrenMedicine');
$this->ChildrenMedicine->recursive = 2;
$childsMeds = $this->ChildrenMedicine->find('all', array('conditions'=>array('child_id'=>$child)));
I expected this to get me an array with all the medicines which the child currently uses, plus the info from the medicines table so I can get the name. However it only gives me:
$childsMeds[0]['ChildrenMedicine']
$childsMeds[1]['ChildrenMedicine']
whereas I was hoping for
$childsMeds[0]['ChildrenMedicine']
$childsMeds[0]['Medicine']
$childsMeds[0]['Child']
$childsMeds[1]['ChildrenMedicine']
$childsMeds[1]['Medicine']
$childsMeds[1]['Child']
Can anyone enlighten me?
EDIT - my ChildrenMedicine model associations:
public $belongsTo = array(
'Child' => array(
'className' => 'Child',
'foreignKey' => 'child_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Medicine' => array(
'className' => 'Medicine',
'foreignKey' => 'medicine_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
Use Containable Behavior.
$this->loadModel('ChildrenMedicine');
$this->ChildrenMedicine->Behaviors->load('Containable');
$childsMeds = $this->ChildrenMedicine->find('all',
array(
'conditions' => array('child_id'=>$child),
'contain' => array('Child', 'Medicine')
));
A alternative method would be to use joins to solve this issue. It would be a better option that relying on setting recursive = 2.
$data = $this->ChildrenMedicine->find('all', array('conditions' => array('child_id'=>$child), 'joins' => array(
array(
'table' => 'medicines',
'alias' => 'Medicine',
'type' => 'inner', // could also do outter
'foreignKey' => false,
'conditions'=> array('ChildrenMedicine.medicine_id= Medicine.medicine_id')
))));
You can read more on joins here
For setting up HABTM relationships this article seems helpful. Look at "The Model" code

belongsTo relationship & filtering in cakePHP

I'm a newbie to cakePHP and I've taken on creating a very small "status reporting" project, that would allow a user to report their current status on a project that they were assigned on.
I'm currently using the acl, auth, and session components to allow for multiple tier users to manage one another with an admin creating users, projects, and assigning them to one another. I have also fixed it so that when a user is logged in and goes to add a "status" that their login session automatically takes care of the "user_id" for the status:
$this->data['Status']['user_id'] = $this->Auth->user('id');
What I need help doing now is filter the options for the project that the user can pick to add a "status" to.
I currently have a setup with a table that holds the relationships between users and projects named *projects_users* with *id, project_id, user_id* as fields. But, after baking this setup, when the user is adding a "status" the drop down menu provides all projects rather than strictly the ones they are "assigned to."
I would like to filter the user's options to the projects that they are assigned to. Is there considerable more relationships I should be setting up or is this a pretty easy little function to write? Any help would be greatly appreciated and if I have left out information, I'd be glad to post anything else.
Here is the Status Model setup:
class Status extends AppModel {
var $name = 'Status';
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Project' => array(
'className' => 'Project',
'foreignKey' => 'project_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
My User Model looks like this:
var $belongsTo = array(
'Group' => array(
'className' => 'Group',
'foreignKey' => 'group_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
var $hasMany = array(
'Status' => array(
'className' => 'Status',
'foreignKey' => 'user_id',
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
);
var $hasAndBelongsToMany = array(
'Project' => array(
'className' => 'Project',
'joinTable' => 'projects_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'project_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
In the database, You should have a column called project_id in the user table that is associated with the project table's id column.
Then, in your User model, you should create a hasMany relationship with the Project model.
class User extends AppModel {
var $name = 'User';
var $hasMany = 'Project';
}
Now, when you fetch a particular user, you will get all projects that the user is associated with.

How do I query data in CakePHP using HABTM relationships?

I'm working on a CakePHP 1.2 application. I have a model "User" defined with a few HABTM relationships with other tables through a join table.
I'm now tasked with finding User information based on the data stored in one of these HABTM tables. Unfortunately, when the query executes, my condition is rejected with an error about a missing table. Upon inspection it seems that CakePHP is not including any of the HABTM tables in the select statement.
My User HABTM relationship is as follows:
var $hasAndBelongsToMany = array(
'Course' => array(
'className' => 'Course',
'joinTable' => 'course_members',
'foreignKey' => 'user_id',
'associationForeignKey' => 'course_id',
'conditions' => '',
'order' => '',
'limit' => '',
'uniq' => false,
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
),
'School' => array(
'className' => 'School',
'joinTable' => 'school_members',
'foreignKey' => 'user_id',
'associationForeignKey' => 'school_id',
'conditions' => '',
'order' => '',
'limit' => '',
'uniq' => false,
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
),
'Team' => array(
'className' => 'Team',
'joinTable' => 'team_members',
'foreignKey' => 'user_id',
'associationForeignKey' => 'team_id',
'conditions' => '',
'order' => '',
'limit' => '',
'uniq' => false,
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
The error is:
SQL Error: 1054: Unknown column
'School.name' in 'where clause'
And finally, the query it is trying to execute
SELECT
`User`.`id`, `User`.`username`, `User`.`password`, `User`.`firstName`, `User`.`lastName`, `User`.`email
`, `User`.`phone`, `User`.`streetAddress`, `User`.`city`, `User`.`province`, `User`.`country`, `User
`.`postal`, `User`.`userlevel`, `User`.`modified`, `User`.`created`, `User`.`deleted`, `User`.`deleted_date
` FROM `users` AS `User` WHERE `User`.`id` = 6 AND `School`.`name` LIKE '%Test%' LIMIT 1
Turn your debug level up to 2 and look at the SQL output. Find the query that your code is generating and you'll notice there are several. The ORM layer in CakePHP doesn't join HABTM related tables in the first query. It gets the results from the first select, then separately fetches the HABTM data for each item. Because the join table is not in the first query, your condition, which is intended for use on a joined table, results in the error you are seeing.
The cook book has a section on HABTM associations and fetching data based on conditions in the HABTM table that will fit your requirements.
From the Cookbook: http://book.cakephp.org/view/83/hasAndBelongsToMany-HABTM
One option is to search the Tag model (instead of Recipe), which will also give us all of the associated Recipes.
$this->Recipe->Tag->find('all', array(
'conditions' => array('Tag.name' => 'Dessert')));
We could also use the join table model (which CakePHP provides for us), to search for a given ID.
$this->Recipe->bindModel(array('hasOne' => array('RecipesTag')));
$this->Recipe->find('all', array(
'fields' => array('Recipe.*'),
'conditions' => array('RecipesTag.tag_id' => 124) // id of Dessert
));
It's also possible to create an exotic association for the purpose of creating as many joins as necessary to allow filtering, for example:
$this->Recipe->bindModel(array('hasOne' => array('RecipesTag',
'FilterTag' => array(
'className' => 'Tag',
'foreignKey' => false,
'conditions' => array('FilterTag.id = RecipesTag.tag_id')
))));
$this->Recipe->find('all', array(
'fields' => array('Recipe.*'),
'conditions' => array('FilterTag.name' => 'Dessert')
));
Your table should be called "schools_users" and not "school_members" because it's many-to-many, thus using the plural form in the table name on both sides is appropiate.
You also set the Model ClassName "School" as Alias for the HABTM. You should change that to something more generic like "Schools" as in "User is in and has many SchoolS" to avoid conflicts.
And another hint: Try to find the user "via" the School Model rather than the User Model.
$this->User->Schools->find()
Hope this helps.
FWIW, your join tables do appear to be "oddly named" insofar as they don't follow the convention described here:
http://book.cakephp.org/view/83/hasAndBelongsToMany-HABTM
In any case, good luck, I remember HABTM being a butt-pain in CakePHP.

Categories