CakePHP Containable multidimensional array hierarchy - cakephp

If I do this:
$cdata = $this->Party->find('first',
array('contain' => array(
'Person' => array(
'Employee' => array(
'Volunteer'),
'Title'))));
Debugger::dump($cdata, 10);
I get this:
array(
'Party' => array(
'id' => '9',
'save_bit' => true
),
'Person' => array(
'id' => '5',
'title_id' => '1',
'first_name' => 'bob',
'middle_name' => '',
'last_name' => 'brown',
'is_active' => false,
'date_of_birth' => '1999-07-07',
'gender' => 'M',
'party_id' => '9',
'Title' => array(
'id' => '1',
'title' => 'Ms',
'description' => ''
),
'Employee' => array(
'id' => '5',
'person_id' => '5',
'Volunteer' => array(
'id' => '5',
'employee_id' => '5'
)
)
)
)
But I really want to get this:
array(
'Party' => array(
'id' => '9',
'save_bit' => true,
'Person' => array(
'id' => '5',
'title_id' => '1',
'first_name' => 'bob',
'middle_name' => '',
'last_name' => 'brown',
'is_active' => false,
'date_of_birth' => '1999-07-07',
'gender' => 'M',
'party_id' => '9',
'Title' => array(
'id' => '1',
'title' => 'Ms',
'description' => ''
),
'Employee' => array(
'id' => '5',
'person_id' => '5',
'Volunteer' => array(
'id' => '5',
'employee_id' => '5'
)
)
)
)
)
This way the array is following the natural hierarchy of my database tables and also works with my website forms for saving and editing which I have declared like so:
echo $this->Form->input('Party.Person.first_name');
echo $this->Form->input('Party.Person.is_active');
Is it possible to do this with Containable, if so how? Thanks!

As already mentioned in my comment, you cannot change the way how containable formats the data, it will be formatted to the CakePHP conventions depending on your associations.
Changing the format would have to be done manually after the find operation, but there's actually no need to do that, instead stick to the conventions with regards to associations and form helper usage, that way CakePHP can automatically glue everything together as needed.
Assuming the association is Party hasMany Person, you should simply use Person.0.first_name in the form, that way the helper can properly populate the field, and when passed to Model::saveAll() or Model::saveAssociated() CakePHP can determine how things need to be saved based on the associations defined on your models.
See Saving Related Model Data (hasOne, hasMany, belongsTo) for more information.

Related

cakephp find(all) optimization

I am using cakephp 2.5.2
I have 5 tables
1- users
2- projects
3- tags
4- project_tags
5- images
a user can have many projects
a project can have many (tags,images)
I have 2 questions
$projects = $this->Project->find('all',array('conditions'=>array('User.id'=>$this->Auth->user('id'))));
debug($projects);exit();
this gives
array(
(int) 0 => array(
'Project' => array(
'id' => '1',
'name' => 'project 1'
),
'User' => array(
'password' => '*****',
'id' => '1',
'name' => 'User 1',
'email' => 'user1#gmail.com'
),
'Image' => array(),
'ProjectTag' => array(
(int) 0 => array(
'id' => '1',
'project_id' => '1',
'tag_id' => '1'
),
(int) 1 => array(
'id' => '2',
'project_id' => '1',
'tag_id' => '8'
),
(int) 2 => array(
'id' => '3',
'project_id' => '1',
'tag_id' => '6'
),
(int) 3 => array(
'id' => '4',
'project_id' => '1',
'tag_id' => '10'
),
(int) 4 => array(
'id' => '5',
'project_id' => '1',
'tag_id' => '4'
)
)
),
(int) 1 => array(
'Project' => array(
'id' => '2',
'name' => 'project 2'
),
'User' => array(
'password' => '*****',
'id' => '1',
'name' => 'user 1',
'email' => 'user1#gmail.com'
),
'Image' => array(),
'ProjectTag' => array(
(int) 0 => array(
'id' => '6',
'project_id' => '2',
'tag_id' => '1'
),
(int) 1 => array(
'id' => '7',
'project_id' => '2',
'tag_id' => '8'
),
(int) 2 => array(
'id' => '8',
'project_id' => '2',
'tag_id' => '4'
)
)
)
)
I need Project & ProjectTag array while do not want user and images array
I tried recursive -1,0,1,2 but could not get what I needed
My Question Number 2 is
How can I find projects which has tag = 'html'
$projects = $this->Project->find('all',array('conditions'=>array('Tag.tag'=>'HTML')));
it says
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Tag.tag' in 'where clause'
First off, just a note. ALWAYS use $recursive=-1;. It's ideal just to set this in the AppModel, then never mess with recursive again. If you want to get additional data, use CakePHP's Containable Behavior, not recursive.
Overall issue - you cannot add conditions against a recursive (or contained) model, which is what you appear to be doing in both of your questions. If you want to condition against a model other than the one you're doing the find on, you need to either use JOINs or swap your query to run on the other model.
Answer to Question 1
Change this:
$projects = $this->Project->find('all',array(
'conditions'=>array(
'User.id'=>$this->Auth->user('id')
)
));
To this:
$projects = $this->Project->find('all',array(
'conditions'=>array(
'user_id'=>$this->Auth->user('id') // <-- notice this
),
'contain' => array(
'ProjectTag' => array(
'Tag' // <-- optional, as you didn't mention you needed
)
)
));
Answer to Question 2:
There are a number of ways to do this. I would suggest this one using joins:
$projects = $this->Project->find('all', array(
'fields' => array(
'Project.*',
'Tag.*'
),
'joins'=>array(
array(
'table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'conditions' => array(
'Tag.project_id = Project.id',
'Tag.tag' => 'HTML'
)
)
)
));
Another way would be to swap your query to run on Tags and just use contain to contain the projects for that tag. I like the join better because if you ever way to extend it to retrieve projects for more than just one tag, your data will still be in an easy-to use manner, as opposed to Containable, which would put your projects in different arrays under their corresponding tag.
If you want to optimize your queries in CakePHP, turn off recursive (set it to -1), then use the containable behavior to pick and choose exactly which data you need.
As for your specific query, associated models are by default gotten by left joins, which means if you did your query from projects, you will get all projects plus any tags they have that have "html" in their tag field. The easiest way to fix this problem, is to do your search from tags instead:
$tags = $this->Tag->find('first', array('conditions' => array('tag' => 'html'), 'contain' => array('Project')));
This will get the tag that has 'html' for its tag field, and all the projects associated with it.

CakePHP: why does format of data retrieved depend on which associated model is called

I have a site with the usual sort of sellers, items and images. The models look like this:
class Seller extends AppModel {
$hasMany = array('Item');
...
}
class Item extends AppModel {
$belongsTo = array('Seller')
$hasMany = array('Image');
...
}
class Image extends AppModel {
$belongsTo = array('Item');
...
}
My problem is that when I retrieve a list of Items, the data comes out in a different format depending on whether I am getting a list of all Items (to display to the public) in the ItemsController, or the details of a Seller and their Items in the SellersController.
Briefly Item->find('all') retrieves an array of
array( 'Item' => array( <item stuff> ),
'Images' => array of array( <image stuff> ) );
whereas the Seller->find('first', ...) retrieves the Items as
array( 'Seller' => array( <seller stuff> ),
'Item' => array of array ( <item stuff>
'Images' => array of array( <image stuff> ) );
In the 2nd case the Images element is nested within the Item part. Having the data formatted so differently makes it harder to re-use View code. Also my Item afterFind() won't work properly has problems when the Images bit isn't where it expects it to be.
QUESTION
Is there a simple way to prevent the 'Images' part being nested within the 'Item' part in the 2nd situation?
I am using Cake 2.4
Formats in More Detail
All items:
// in ItemsController.php
debug($this->Item->find('all'));
produces this:
array(
(int) 0 => array(
'Item' => array(
'id' => '1',
'title' => 'tom item 1',
'seller_id' => '1'
),
'Seller' => array(
'id' => '1',
'name' => 'tom'
),
'Images' => array(
(int) 0 => array(
'id' => '1',
'item_id' => '1',
'path' => 'tom_1_1'
),
(int) 1 => array(
'id' => '2',
'item_id' => '1',
'path' => 'tom_1_2'
)
)
),
(int) 1 => array(
'Item' => array(
'id' => '2',
'title' => 'tom item 2',
'seller_id' => '1'
),
'Seller' => array(
'id' => '1',
'name' => 'tom'
),
'Images' => array(
(int) 0 => array(
'id' => '3',
'item_id' => '2',
'path' => 'tom_2_1'
),
(int) 1 => array(
'id' => '4',
'item_id' => '2',
'path' => 'tom_2_2'
)
)
),
...
A single Seller + associated Items:
// In SellersController.php
debug($this->Seller->find('first',
array('conditions' => array('id' => $id), 'recursive' => 2)));
produces:
array(
'Seller' => array(
'id' => '1',
'name' => 'tom'
),
'Item' => array(
(int) 0 => array(
'id' => '1',
'title' => 'tom item 1',
'seller_id' => '1',
'Seller' => array(
'id' => '1',
'name' => 'tom'
),
// Look! 'Images' is WITHIN 'Item !!!
'Images' => array(
(int) 0 => array(
'id' => '1',
'item_id' => '1',
'path' => 'tom_1_1'
),
(int) 1 => array(
'id' => '2',
'item_id' => '1',
'path' => 'tom_1_2'
)
)
),
(int) 1 => array(
'id' => '2',
'title' => 'tom item 2',
'seller_id' => '1',
'Seller' => array(
'id' => '1',
'name' => 'tom'
),
'Images' => array(
(int) 0 => array(
'id' => '3',
'item_id' => '2',
'path' => 'tom_2_1'
),
(int) 1 => array(
'id' => '4',
'item_id' => '2',
'path' => 'tom_2_2'
)
)
),
....
This is the expected and correct behavior as Image is not directly associated with Seller, but with Item.
Imagine, how would you determine the proper association when Image would be defined on the same level as Item? That would indicate an association with Seller which doesn't exist.
If you want a different structure, format it in your afterFind callback or wherever it's appropriate. However, I wouldn't recommend trying to work against the CakePHP standards. In case applicable, adapt your view code.
Your question is not clear to me. If just wants to prevent Image to coming in 2nd case, try this
$this->Seller->Item->unbindModel(array(
'hasMany' => array('Image')
));
$result = $this->Seller->find('first',
array('conditions' => array('id' => $id), 'recursive' => 2)));
debug($result);

Containable to do show deeper data or join table

I have 3 tables: projects, project_reminder_users, project_types.
The relations is as follow:
Project => belong to => ProjectType
hasMany => ProjectReminderUser
ProjectReminderUser => belong to => Project
ProjectType => hasMany => Project
I get all data based on who assigned(ProjectReminderUser) by this
$this->Project->ProjectReminderUser->Behaviors->load('Containable');
$this->paginate = array(
'ProjectReminderUser' => array(
'limit' => $limit,
'contain' => array(
'Project' => array(
'ProjectComment',
'ProjectFile',
),
'User'
),
'conditions' => array(
'User.group_id' => $this->Session->read('Auth.User.group_id'),
'ProjectReminderUser.user_id' => $this->Session->read('Auth.User.id'),
'Project.project_status_id' => PROJECT_STATUS_OPEN,
),
'order' => 'Project.due_date',
)
);
$this->set('myTasks', $this->paginate('ProjectReminderUser'));
and the result look like this
array(
'ProjectReminderUser' => array(
'id' => '96',
'user_id' => '1',
'project_id' => '46'
),
'Project' => array(
'id' => '46',
'project_type_id' => '9',
'contact_id' => null,
'company_id' => null,
'subject' => 'Test Modified Field',
'description' => 'Test Modified Field',
'ProjectFile' => array(
(int) 0 => array(
'id' => '19',
'project_id' => '46',
'user_id' => '6',
'file_path' => '46_bhbinary_xmm_1728.jpg',
'notes' => null,
'created' => '2013-11-26 18:37:49'
),
),
'ProjectComment' => array(
)
),
'User' => array(
'password' => '*****',
'id' => '1',
'group_id' => '1',
'email' => 'xxx#xxxxx.com',
'first_name' => 'xxxx',
'deleted' => false,
'displayName' => 'xxxxx'
)
)
In the result There is data for Project.project_type_id, but I would like to have more detail of that. So I can show it as name instead of number. maybe like ProjectType.name instead.
How can I achieve this, so I can sort it in the view? something like this
$this->Paginator->sort('ProjectType.name', 'Type');
The problem is that Paginator doesn't play nice with deep model associations. I think though if rather than using Cake's methods for doing model associations, you instead do your joins manually, you may be able to work out the sorting as you want. See the below discussion.
http://sinkpoint.railsplayground.net/cakephp-pagination-deep-sort-and-habtm/
Worst comes to worse, you may have to also rewrite the model's paginate function to deal with sorting how you need it to.
How about
'contain' => array(
'Project' => array(
'ProjectComment',
'ProjectFile',
'ProjectType',
),
'User'
),
Looks like you did not contain "ProjectType"

saveAll: save new data for related model or use existing record

I'm trying to update data about many shipments with one form.
Shipment model belongsTo Shippingaddress model and Shippingaddress hasMany Shipment.
In interface user can use his previously saved Shippingaddress from select field or fill small form and add new one.
Currently example structure of data which are posted to controller looks like this:
array(
(int) 0 => array(
'Shipment' => array(
'id' => '223',
'shippingaddress_id' => '8',
)
),
(int) 1 => array(
'Shipment' => array(
'id' => '224',
'shippingaddress_id' => '',
),
'Shippingaddress' => array(
'recipient' => 'some name',
'address' => 'some addres data',
'zip' => '00-555',
'city' => 'some city',
)
),
(int) 2 => array(
'Shipment' => array(
'id' => '225',
'shippingaddress_id' => '12',
)
)
)
It would be nice to save Shiippingaddress data and update all Shipments in one transaction with
$this->Shipment->saveAll($data).
Is it possible?
I was trying many combination of posted array structure but without success.
Your $data structure should looks like this:
$data = array(
'Shippingadress' => array(
'0' => array(
'id' => '8',
'Shipment' => array(
'id' => '223'
)
),
'1' => array(
'recipient' => 'some name',
'address' => 'some addres data',
'zip' => '00-555',
'city' => 'some city'
'Shipment' => array(
'id' => '224'
)
),
'2' => array(
'id' => '12',
'Shipment' => array(
'id' => '255'
)
),
)
);
For save use saveMany:
$this->Shippingadress->saveMany($data, array('deep' => true) );
You don't need to specify shippingadress_id to empty string because Cake already takes care of it and put all relevant foreign keys based on the relationship of your models.

Cake PHP joining same model on two tables

I have a question regarding model association. Consider the following application: each month a new question is asked to the users. These have the ability to give a tip on this question and other users have the ability to react on this tip. My model-setup is like this:
Users hasMany Tips, Reactions
Tips belongsTo User
Tips hasMany Reactions
Reactions belongsTo User, Tip
Now I want to get all the tips on that question from all users. I do a simple find all on the tips-table which return me all tips joined with their user and reactions. The reactions however do not contain the user, which is very important, obviously.
Cake automatically converts my find all to two queries: one on the tips-table joining the user and one on the reactions table, but there is no join on the users-table, which is what i need.
So my question is: why is the users-table not joined with the reactions and how can i solve this?
Code:
User-model:
public $hasMany = array('Tip', 'Reaction');
Tip-model:
public $hasMany = array('Reaction' => array(
'foreignKey' => 'tip_id'
));
public $hasOne = array('User' => array(
'fields' => array(
'User.id, User.first_name, User.last_name'
),
'foreignKey' => 'id'
));
Reaction-model:
public $belongsTo = array('Tip', 'ReactionUser' => array(
'className' => 'User',
'foreignKey' => 'id'
));
Controller:
$tips = $this->Tip->find('all');
OUTPUT:
array(
(int) 0 => array(
'Tip' => array(
'id' => '1',
'user_id' => '1',
'question_id' => '1',
'tip' => 'Testing da new datavbase',
'votes' => '0',
'approved' => '1',
'pic' => '',
'created' => '2012-05-18 09:22:56'
),
'User' => array(
'id' => '1',
'first_name' => 'Bart',
'last_name' => 'De Bock'
),
'Reaction' => array(
(int) 0 => array(
'id' => '1',
'tip_id' => '1',
'user_id' => '1',
'reaction' => 'lorem ipsum',
'votes' => '0',
'approved' => '1',
'pic' => '',
'created' => '0000-00-00 00:00:00'
)
)
),
(int) 1 => array(
'Tip' => array(
'id' => '2',
'user_id' => '2',
'question_id' => '1',
'tip' => 'Test',
'votes' => '0',
'approved' => '1',
'pic' => '',
'created' => '2012-05-18 10:22:51'
),
'User' => array(
'id' => '2',
'first_name' => 'Deborah',
'last_name' => 'Rochtus'
),
'Reaction' => array(
(int) 0 => array(
'id' => '2',
'tip_id' => '2',
'user_id' => '3',
'reaction' => 'Say whaat?',
'votes' => '2',
'approved' => '1',
'pic' => '',
'created' => '2012-05-18 10:33:32'
),
(int) 1 => array(
'id' => '3',
'tip_id' => '2',
'user_id' => '4',
'reaction' => 'Hmmm..',
'votes' => '0',
'approved' => '1',
'pic' => '',
'created' => '2012-05-18 10:33:44'
)
)
)
)
As you can see, these reactions do not contain the associated user...
Thanks for your help!
Use Containable:
In AppModel:
public $uses = array('Containable');
Change your find call to:
$this->Tip->find('all', array('contain' => array('User', 'Reaction' => array('User'))));
Please try this
$tips = $this->Tip->find('all'
array(
'recursive'=>'2'
)
);
recursive value maybe 1, 2, 3, 4 etc as you need.

Resources