CakePHP Containable with HABTM - cakephp

To explain the issue I'm having, I'll use an example. Let's say that I'm building a system where students can sign up for one or more afterschool courses, but a school official has to approve the sign-up in order for it to be valid. So I have these models:
Course (belongs to "Teacher", hasAndBelongsToMany "Student")
Student (hasAndBelongsToMany "Course")
Teacher (hasMany "Course")
Now let's say I want to see a list of all of the unapproved sign-ups that are for ninth-graders. That's easy:
$this->request->data = $this->Student->CoursesStudent->find('all', array(
'conditions' => array(
'CoursesStudent.is_approved' => null,
'Student.grade' => 9
)
));
The thing that I'm struggling with is pulling the teacher's data as well (more than just their ID). I've tried this:
$this->request->data = $this->Student->CoursesStudent->find('all', array(
'conditions' => array(
'CoursesStudent.is_approved' => null,
'Student.grade' => 9
),
'contain' => array(
'CoursesStudent',
'Course' => array(
'Teacher' => array(
'fields' => array(
'Teacher.id',
'Teacher.name'
)
)
),
'Student'
)
));
But it doesn't have any effect whatsoever on the returned array. To make it easy for you to throw this into an existing CakePHP project to play with, here is the database schema and data and here is what you can paste into a random action of an existing CakePHP project. This is what I'm trying to get (notice that the teacher's data is there):
Array
(
[0] => Array
(
[CoursesStudent] => Array
(
[id] => 1
[course_id] => 1
[student_id] => 1
[is_approved] =>
[created] => 2012-12-11 00:00:00
[modified] => 2012-12-11 00:00:00
)
[Course] => Array
(
[id] => 1
[teacher_id] => 1
[title] => Introduction to Improvisation
[start_date] => 2012-12-17
[end_date] => 2012-12-21
[created] => 2012-12-11 00:00:00
[modified] => 2012-12-11 00:00:00
[Teacher] => Array
(
[id] => 1
[first_name] => John
[last_name] => Doe
[created] => 2012-12-11 00:00:00
[modified] => 2012-12-11 00:00:00
)
)
[Student] => Array
(
[id] => 1
[first_name] => Bill
[last_name] => Brown
[grade] => 9
[created] => 2012-12-11 00:00:00
[modified] => 2012-12-11 00:00:00
)
)
)
Thanks!

You must set $recursive to -1 before using contain() will work.
Also, make sure your model is set to use the Containable Behavior. In this case, since 'Course' is also containing something, it needs to use the Containable behavior too.
(You could think about setting the below $actsAs in your AppModel to make Containable available to every model.)
//in your CoursesStudent model (and in your Course model)
public $actsAs = array('Containable');
And here's your find, but with setting recursive first:
$this->Student->CoursesStudent->recursive = -1;
$this->request->data = $this->Student->CoursesStudent->find('all', array(
'conditions' => array(
'CoursesStudent.is_approved' => null,
'Student.grade' => 9
),
'contain' => array(
'CoursesStudent',
'Course' => array(
'Teacher' => array(
'fields' => array(
'Teacher.id',
'Teacher.name'
)
)
),
'Student'
)
));

Related

HasAndBelongsToMany returns unnecessary data

When I fetch records using hasAndBelongsToMany it sends me the following output:
[Location] => Array
(
[id] => 1
[uuid] => 5a8e4d7f-e1d0-11e4-9567-c03fd5623744
[name] => chandigarh
[address_line1] => dummy
[address_line2] => dummy
[latitude] => 30.737780
[longitude] => 76.784439
[timezone] => UTC
[is_deleted] => 0
[created] => 2015-04-13 16:59:31
[modified] => 2015-04-13
[entry_ts] => 2015-04-13 16:59:31
)
[Department] => Array
(
[0] => Array
(
[dept_name] => Heater
[LocationDepartment] => Array
(
[id] => 1
[location_id] => 1
[department_id] => 1
[is_deleted] => 0
[created] => 2015-04-13 17:00:50
[modified] => 2015-04-13 17:00:50
[entry_ts] => 2015-04-13 17:00:50
)
)
)
But I don't need the LocationDepartment array. Can any one help me?
If your using a quick find you can do this before you do it
$this->Location->recursive = 1;
$this->Location->findById($id);
If your using a full find you can add in recursive as part of the passed parameters
$this->Location->find('first', array('conditions' => array('id' => $id), 'recursive' => 1));
Posting your sample code will help solve something this simple
You can also include the 'containable' behavior on the Model which will let you specify exactly what models/data to retrieve back.
class Location extends AppModel {
var $actsAs = array('Containable');
function GetRecord($id) {
return $this->find('first', array(
'conditions' => array(
'id' => $id,
),
'contain' => array('Department'),
);
}
}

CakePHP HABTM add tags to post - won't update join table

I have Cakephp 2.x app. It is simple photo gallery app. I want to add one or more new tags to existing photos. Ideally, I want to be able to remove tags from a photo too.
Cakephp HABTM seems to be made for this and promises to do all of this.
I used a standard baked Edit Photo Form view and controller action, with the addition of a tag list as checkboxes that is populated by controller list.
This works well to view all existing tags for that photo. When I navigate to /app/photos/addtags/id the Edit Photo Form populates with that photo id's photo details, and a full list of available tags each with checkbox, and any previously selected tags are checked.
So far so good, but my problem is if I check a new tag and then click submit, it doesn't update photos_tag join table and instead it just adds a new record to photo table.
This is completely unexpected behaviour. I would expect it to only add new record(s) with photo_id and tag_id for new tags to photos_tag join table.
I know there are a bunch of similar SO questions and I've been to all of them here SO and elsewhere and from what I can tell my tables, models, controller and view are set up properly.
I need help with suggestions on why it isn't working as desired.
Photos table:
id
filename
account_id
created
Tags table:
id
tagname
account_id
category
created
Photos_Tags table:
photo_id
tag_id
Photo.php
public $hasAndBelongsToMany = array(
'Tag' => array(
'className' => 'Tag',
'joinTable' => 'photos_tags',
'foreignKey' => 'photo_id',
'associationForeignKey' => 'tag_id',
'unique' => 'keepExisting',
//'with' => 'PhotosTag',
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
Tag.php
public $hasAndBelongsToMany = array(
'Photo' => array(
'className' => 'Photo',
'joinTable' => 'photos_tags',
'foreignKey' => 'tag_id',
'associationForeignKey' => 'photo_id',
'unique' => 'keepExisting',
//'with' => 'PhotosTag',
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
PhotosController.php
public function addtags($id = null) {
$this->loadModel('Photo');
if ($this->request->is(array('post', 'put'))) {
//$this->Photo->id = $id;
if ($this->Photo->saveAll($this->request->data)) {
$this->Session->setFlash(__('The photo has been saved.'));
return $this->redirect(array('action' => 'view', $id));
} else {
$this->Session->setFlash(__('The photo could not be saved. Please, try again.'));
}
} else {
$options = array('conditions' => array('Photo.' . $this->Photo->primaryKey => $id));
$this->request->data = $this->Photo->find('first', $options);
}
//$this->request->data = $photo;
$this->set('tags', $this->Photo->Tag->find('list'));
}
Photo edit form:
<div class="photos form">
<?php echo $this->Form->create('Photo'); ?>
<fieldset>
<legend><?php echo __('Edit Photo'); ?></legend>
<?php
echo $this->Form->input('id');
echo $this->Form->input('account_id', array('type'=>'hidden'));
echo $this->Form->input('filename');
echo $this->Form->input('filename_th');
echo $this->Form->input('desc');
echo $this->Form->input('Photo.Tag' , array('label'=>'Tags', 'multiple'=>'checkbox'));
?>
</fieldset>
<?php echo $this->Form->end(__('Submit')); ?>
</div>
print_r($this->request->data) :
Array
(
[Photo] => Array
(
[id] => 156
[filename] => front_door_1
[desc] => 0
[account_id] => 1
[user_id] =>
[created] =>
)
[Account] => Array
(
[id] => 1
[account] => ABC Bank
[created] => 2014-06-17
[modified] => 2014-06-17
)
[User] => Array
(
[id] =>
[username] =>
[password] =>
[first_name] =>
[last_name] =>
[role] =>
[email] =>
[ip_address] =>
[activation_code] =>
[account_id] =>
[created] =>
[modified] =>
)
[Comment] => Array
(
)
[Tag] => Array
(
[0] => Array
(
[id] => 27
[tagname] => front_door
[keyname] =>
[category] => feature
[account_id] => 1
[user_id] => 3557
[created] => 2014-07-12 16:58:44
[modified] =>
[PhotosTag] => Array
(
[id] => 1
[photo_id] => 156
[tag_id] => 27
[account_id] => 1
[user_id] =>
[created] =>
)
)
[1] => Array
(
[id] => 65
[tagname] => Surrey_Branch
[keyname] =>
[category] => location
[account_id] => 1
[user_id] => 3557
[created] => 2014-07-12 16:58:44
[modified] =>
[PhotosTag] => Array
(
[id] => 32
[photo_id] => 156
[tag_id] => 65
[account_id] => 1
[user_id] =>
[created] =>
)
)
)
)
1
So after a few days fiddling with this app I created brand new CakePHP app based on same database. This was same CakePHP version. But voila the newly created CakePHP HABTM was working perfectly.
I could view photo tags in photo view and edit forms. I could add/remove tags from photos.
I have done a quick review of differences in code between these CakePHP apps and I just can't see any obvious differences.
I copied and pasted code and files from old app to new app folders including entire views, specialized controller and model code.
Can't say what the difference between them was.
The takeaway here is that a) the CakePHP HABTM worked perfectly with newly baked app, and b) if anyone else encounters similar problems, suggest you try quickly re-baking app.

CakePHP: HABTM relationship returns join table as an object - how do I get rid of it?

I have a 2 CakePHP models, Articles and Categories, attached using a hasAndBelongsToMany relationship, like so:
$category = new Category();
$category->bindModel(array('hasAndBelongsToMany' => array(
'Article' => array(
'className' => 'Article',
'joinTable' => 'articles_categories',
'foreignKey' => 'category_id',
'associationForeignKey' => 'article_id',
'fields' => 'id,name'
))));
$this->set('categories', $category->find('all',
array(
'fields' => 'id,name'
)));
...but then, when I print out $categories, I get the following:
Array
(
[0] => Array
(
[Category] => Array
(
[id] => 31
[name] => category name
[article_count] => 1
)
[Article] => Array
(
[0] => Array
(
[id] => 1445
[name] => article name
[teaser] =>
[author_id] => 3
[ArticlesCategory] => Array
(
[id] => 6634
[article_id] => 1445
[category_id] => 31
[author_id] => 3
[created] => 2014-03-10 12:27:26
[modified] => 2014-03-10 12:27:26
)
)
)
)
I really don't need the [ArticlesCategory] member of [Article]. This just leads me back to information I already have. I tried limiting recursion but that didn't help.
How would I get rid of this?
You have two options:
[1] Reduce recursive value to 0 (CakePHP default: 1)
$category->recursive = 0;
$category->find('all',
array(
'fields' => 'id,name'
))
[2] Start using ContainableBehaviour (my own preference), which gives you more control over model data retrieved
Add following to AppModel for App-wide coverage:
public $actsAs = array('Containable');
The find method becomes:
$category->find('all',
array(
'fields' => 'id, name',
'contain' => 'Article'
))
That should do it!

CakePHP 2.4.2: Contain Not Working as Expected

I have a model, Comment, that belongsto Module, Photo, Review, Article, and User. In turn, each of those models havemany Comment.
User belongsto Avatar and Avatar hasmany User
In the code below I successfully retrieve the latest 5 comments regardless of what model they come from (Photo, Article, Review) and I display some information about the User (username, online status).
$recentComments = $this->find('all',
array(
'limit' => '5',
'order' => array('Comment.id' => 'desc'),
'conditions' => array(
'Comment.page_id' => $page_id
),
'contain' => array(
'Photo' => array(
'fields' => array('Photo.id'),
'conditions' => array(
'Comment.module_id' => 8
),
),
'Review' => array(
'fields' => array('Review.title', 'Review.id'),
'conditions' => array(
'Comment.module_id' => 2
),
),
'Article' => array(
'fields' => array('Article.title', 'Article.id'),
'conditions' => array(
'Comment.module_id' => 3
),
),
'Module' => array(
'fields' => array('Module.model', 'Module.post_title'),
),
'User' => array(
'fields' => array('User.username', 'User.online'),
),
)
));
Unfortunately there are some issues with this. As you can see above I don't contain User->Avatar and all expected data is retrieved successfully.
$recentComments = Array
(
[0] => Array
(
[Comment] => Array
(
[id] => 179
[page_id] => 2
[module_id] => 3
[page] =>
[user_id] => 29
[post_id] => 9
[title] => test comment for article 9
[content] => here is the content for the comment
[created] => 2013-04-24 00:00:00
[redeemed] => 0
[status] => 0
)
[User] => Array
(
[username] => bowlerae
[online] => 0
)
[Module] => Array
(
[model] => Article
[post_title] => title
)
[Article] => Array
(
[title] => Test title for article 9
[id] => 9
[likes] => 0
[dislikes] => 0
[comments] => 2
)
[Photo] => Array
(
[id] =>
)
[Review] => Array
(
[title] =>
[id] =>
)
)
However, if I DO contain User->Avatar like this...
'User' => array(
'fields' => array('User.username', 'User.online'),
'Avatar' => array(
'fields' => array('Avatar.file')
)
),
Then the recursion basically becomes unlimited, ALL models related to Comment, User, Module, Article, Photo and Review are also retrieved which is A LOT.
Can anyone explain why this is happening? I will be happy to submit more code from some of the models if needed but I don't see any issues there.
Take a look at another example that works successfully...Below I am retrieving the 5 most recent articles. All information including the user Avatar is successfully retreived.
$this->set('articles_sidebar', ClassRegistry::init('Article')->find('all',
array(
'limit' => '5',
'order' => array('Article.id' => 'desc'),
'conditions' => array('
Article.page_id' => $page_id
),
'contain' => array(
'User' => array(
'fields' => array('User.username', 'User.online'),
'Avatar' => array(
'fields' => array('Avatar.file')
)
),
)
)));
Please note these two finds are performed in the AppController in the beforeRender given that $page_id > 0. $page_id is set in whatever the current controller is. I know people would probably ask about it but that's not what the issue is as I mentioned that example 2 retrieving the recent articles currently works.
EDIT: I discovered that it has something to do with my afterfind callback in the Article model. Is there a way I can tweak the queries in the afterfind and/or the $recentComments query so that they still work without breaking my contain? I don't need the likes, dislikes or comments virtual fields in my $recentComments query which is why they are not listed as one of the contained fields.
function afterFind($results, $primary = false) {
parent::afterFind($results, $primary);
foreach($results as $key => $val){
if (isset($val['Article']['id'])){
$results[$key]['Article']['likes'] = $this->Like->find('count', array('conditions' => array('Like.post_id' => $results[$key]['Article']['id'], 'Like.module_id' => 3, 'Like.status' => 0)));
$results[$key]['Article']['dislikes'] = $this->Like->find('count', array('conditions' => array('Like.post_id' => $results[$key]['Article']['id'], 'Like.module_id' => 3, 'Like.status' => 1)));
$results[$key]['Article']['comments'] = $this->Comment->find('count', array('conditions' => array('Comment.post_id' => $results[$key]['Article']['id'], 'Comment.module_id' => 3, 'Comment.status < 2')));
}
} // end for each
return $results;
} // end afterfind
My guess is your problem is this:
When using ‘fields’ and ‘contain’ options - be careful to include all
foreign keys that your query directly or indirectly requires.
Make sure you're including whatever field(s) are used to join User and Avatar models.
(read here)
For now I changed (in the Article model afterFind)
if (isset($val['Article']['id']))
to
if (isset($val['Article']['id']) && $val == "article")
Seems to be working in current controller but need further testing. Is there a better solution?

cakephp contain association conditions issue

I have the folowing associations
post->primary->secondary
$results = $this->Post->find('all', array(
'conditions' => array(
'Post.post_id =' => 2,
'Primary.secondary_id !=' => null
),
'contain' => array(
'Primary' => array(
'Secondary' => array(
'conditions' => array('Secondary.short_code =' => 'code')
)
)
)
));
Returns this.
Array
(
[0] => Array
(
[Post] => Array
(
[id] => 2
[created] => 2012-10-29 09:48:29
[modified] => 2012-10-29 09:48:29
)
[Primary] => Array
(
[id] => 3
[secondary_id] => 6
[Secondary] => Array
(
[id] => 6
[short_code] => code
[created] => 2012-10-31 11:19:56
[modified] => 2012-10-31 11:20:03
)
)
)
However when I change
'conditions' => array('Secondary.short_code =' => 'code')
to
'conditions' => array('Secondary.short_code !=' => 'code')
it still returns the primary record, when I dont want it to.
Array
(
[0] => Array
(
[Post] => Array
(
[id] => 2
[created] => 2012-10-29 09:48:29
[modified] => 2012-10-29 09:48:29
)
[Primary] => Array
(
[id] => 3
[secondary_id] => 6
[Secondary] => Array
(
)
)
)
It's hard to know exactly what you're hoping to achieve, but I THINK it sounds like you're trying to limit the 'Primary' results based on conditions against the 'Secondary' model.
If that's the case, you're going to need to use joins() instead of contain().
The reason: When you use CakePHP's Containable Behavior, it actually does separate queries, then combines the results before returning the data to you. Doing it this way is great in many ways, but it does not allow you to limit parent results based on conditions against it's children.
To do that, just use joins(). (CakePHP syntax which creates MySQL JOIN)

Resources