CakePHP HABTM find ONLY associated model data - cakephp

I have the following relations:
class Platformuser extends AppModel {
public $hasAndBelongsToMany = array(
'Service'
);
}
class Service extends AppModel {
public $hasAndBelongsToMany = array(
'Platformuser'
);
}
I am doing a action on the PlatformusersController to get the services associated with this user with the following query:
$this->Platformuser->find('all', array(
'conditions' => array('Platformuser.id' => $userId),
));
It returns everything about Platformuser/Service and I ONLY want the data of the service:
array(
(int) => array(
[Platformuser] => array(
[id] => [1]
[name] => [Domingo]
),
[Service] => array(
(int) 0 => array(
[id] => [1]
[name] => [dropbox],
[PlatformusersService] => array(
[id] => [1],
[platformuser_id] => [1],
[service_id] => [1],
[modified] => [2013-10-10 00:00:00],
[created] => [2013-10-10 00:00:00];
)
)
)
)
I want something like:
array(
[Service] => array(
(int) 0 => array(
[id] => [1]
[name] => [dropbox]
)
)
Any ideas?.

You should use the containable behavior. It's easy to use and you'll able to fetch data according to your need like.
$plateFormuser = $this->Platformuser->find('first', array(
'conditions' => array('Platformuser.id' => $userId),
'contain' => array(
'Service' => array(
'fields' => array('id', 'name')
)
)
));
$services = Set::merge(
Set::classicExtract($plateFormuser, 'Service.{n}.{id}'),
Set::classicExtract($plateFormuser, 'Semester.{n}.{name}')
);
Now you can json encode the $services to get result as you've mentioned [{"id":"1", "name":"dropbox"}].

Related

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 saveAssociated not saving HasMany Through Model Data

I'm trying to get my associated models in CakePHP 2.3 to save properly, but I'm having issues. I'm storing posts, and I want to know what links are in those posts. For each of those links, I'd like to store anchor text if it is available. My database is set up as in the following diagram.
(source: derekperkins.com)
Anchor Model
class Anchor extends AppModel {
public $hasMany = array(
'PostsUrl' => array(
'className' => 'PostsUrl',
'foreignKey' => 'anchor_id',
'dependent' => false
)
);
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Anchor::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('anchor' => $data['anchor'])
));
if( $id )
$data['id'] = $id['Anchor']['id'];
return parent::save($data, $validate, $fieldList);
}
}
URL Model
class Url extends AppModel {
public $hasMany = array(
'PostsUrl' => array(
'className' => 'PostsUrl',
'foreignKey' => 'url_id',
'dependent' => false
)
);
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Url::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('url' => $data['url'])
));
if( $id )
$data['id'] = $id['Url']['id'];
return parent::save($data, $validate, $fieldList);
}
}
PostsUrl Model
class PostsUrl extends AppModel {
public $belongsTo = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'post_id'
),
'Url' => array(
'className' => 'Url',
'foreignKey' => 'url_id'
'Anchor' => array(
'className' => 'Url',
'foreignKey' => 'anchor_id'
)*/
);
}
Post Model
class Post extends AppModel {
public $hasMany = array(
'PostsUrl' => array(
'className' => 'PostsUrl',
'foreignKey' => 'post_id',
'dependent' => false
)
);
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Post::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('external_post_id' => $data['external_post_id'])
));
if( $id )
$data['id'] = $id['Post']['id'];
return parent::save($data, $validate, $fieldList);
}
}
Submitting Data
I've created a form to test my model. This is the code I'm using to save the array created by the form. I am getting a message saying that things saved successfully, but only the post saves. Nothing is entered into the other three tables. I'm also using DebugKit and no SQL calls reference any of that data.
$this->Post->saveAssociated($this->request->data, array('deep' => true))
Array
(
[Post] => Array
(
[external_post_id] => 12345
[sentiment_score] => 3.3
)
[URL] => Array
(
[url] => http://test.com
)
[Anchor] => Array
(
[anchor] => Test Anchor
)
)
I've also tried formatting my arrays to have the URL and Anchor underneath PostsUrl as a subarray, but that didn't work either.
My Model::save functions are there to keep me from duplicating data, and they work properly in other models I have used in the past (though I'm open to suggestions on a better way to do this, as this uses a database call for each check). I've also tried commenting them out, and it doesn't affect my code. How should I structure this to save properly?
First of all about your Model::save functions, for example:
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Post::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('external_post_id' => $data['external_post_id'])
));
if( $id )
$data['id'] = $id['Post']['id'];
pr($data);
return parent::save($data, $validate, $fieldList);
}
it prints $data in this way :
Array
(
[id] => 3 //for example 3
[Post] => Array
(
[external_post_id] => 12345
[sentiment_score] => 3.3
)
[URL] => Array
(
[url] => http://test.com
)
[Anchor] => Array
(
[anchor] => Test Anchor
)
)
this $data is incorrect, the correct data is:
Array
(
[Post] => Array
(
[id] => 3 //for example 3
[external_post_id] => 12345
[sentiment_score] => 3.3
)
[URL] => Array
(
[url] => http://test.com
)
[Anchor] => Array
(
[anchor] => Test Anchor
)
)
You must change your Model::save function this way:
public function save($data = NULL, $validate = true, $fieldList = array()) {
$id = Post::find('first', array(
'fields' => array('id'),
'recursive' => -1,
'conditions' => array('external_post_id' => $data['external_post_id'])
));
if( $id )
$data[$this->name]['id'] = $id['Post']['id'];
return parent::save($data, $validate, $fieldList);
}
second , you can't save this data with single save, you should save your data this way:
$postData = array
(
'Post' => array
(
'external_post_id' => 12345
'sentiment_score' => 3.3
)
);
$this->Post->save($data);
$postUrlId = $this->PostsUrl->find('first', array(
'conditions' => array(
'post_id' => $this->Post->id
),
'fields' => array('id')
));
$urlAnchorData = array(
'URL' => array
(
'url' => 'http://test.com'
),
'Anchor' => array
(
'anchor' => 'Test Anchor'
),
'PostsUrl' => array(
'id' => $postUrlId['PostsUrl']['id']
)
);
$this->PostsUrl->saveAll('$urlAnchorData');

Cakephp many to many recursive

Hy i am trying to create a follow feature to my website so user can follow each other , I am using Cakephp what kind of relation should i use , what should i name the tables.
NB: I created a user table + follow table containing user_id and follower_id !
If you don't need to save any information about the relation, then hasAndBelongsToMany is the natural relation to use in this case.
Try this:
// User Model
var $hasAndBelonsToMany = array(
'Follower' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'associationForeignKey' => 'follower_id'
'joinTable' => 'followers_users'
)
)
then you must create the users table as normal, and a table 'followers_users' with columns: 'id', 'user_id', 'follower_id' (and 'created' and 'updated' if you need them).
EDIT :
To retrieve your data (read here) you do it as usual:
$this->User->find('all', array('conditions' => array('id' => 1)));
Then you'll get an array like:
Array(
[User] => array(
[id] => 1
[name] => xxxx
)
[Follower] => array(
[0] => array(
[id] => 2
[name] => yyyy
)
[1] => array(
[id] => 3
[name] => zzzz
)
)
)
To save your data (read here and here), you need to create an array like:
array(
[User] => array(
[id] => 1
[name] => xxx
)
[Follower] => array(
[0] => array(
[id] => 2
)
[1] => array(
[id] => 3
)
)
)
Explanation of what you are looking / example is right in the book:
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#multiple-relations-to-the-same-model
Similar example per the book:
"It is also possible to create self associations as shown below:"
<?php
class Post extends AppModel {
public $name = 'Post';
public $belongsTo = array(
'Parent' => array(
'className' => 'Post',
'foreignKey' => 'parent_id'
)
);
public $hasMany = array(
'Children' => array(
'className' => 'Post',
'foreignKey' => 'parent_id'
)
);
}

correct data array to save with this kind of relationship

class Post extends AppModel {
var $name = 'Post';
var $hasMany = array(
'CategoryPost' => array(
'className' => 'CategoryPost'
)
);
var $belongsTo = array(
'Page' => array(
'className' => 'Page'
)
);
class Category extends AppModel {
var $name = 'Category';
var $hasMany = array(
'CategoryPost' => array(
'className' => 'CategoryPost'
)
);
class CategoryPost extends AppModel {
var $name = 'CategoryPost';
var $validate = array(
'category_id' => array(
'rule' => array('multiple', array('in' => array(1, 2, 3, 4))),
'required' => FALSE,
'message' => 'Please select one, two or three options'
)
);
var $belongsTo = array(
'Post' => array(
'className' => 'Post'
),
'Category' => array(
'className' => 'Category'
)
);
Will this be the correct format of array that is needed to save this with saveAll? This doesn't save the CategoryPost model. If it isn't what should be the format of the array?
Array
(
[Post] => Array
(
[title] => query
[body] =>
query
[page_id] => 122
[modified] => 2010-12-30 23:33:47
[created] => 2010-12-30 23:33:47
[uri] => query-9
)
[CategoryPost] => Array
(
[0] => Array
(
[category_id] => 1
)
[1] => Array
(
[category_id] => 2
)
)
[Page] => Array
(
[meta_keywords] => query
[meta_description] => query
[title] => query
[layout] => index
)
)
Well, everthing seems fine, and the CategoryPost data should be saved.
Maybe comment out the validation from the CategoryPost model and try then.

How do I Find Categories within in Contained Model in CakePHP

I had a recent question here regarding Finding records via conditions in habtm. Now, I could search for posts within a category I searched for.
Now, my questions is how to I retrieve the categories of each post. Sometimes a post has one or more categories in this query:
$this->set('posts', $this->Category->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post')
)
));
I'd imagine something like this:
$this->set('posts', $this->Category->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post' => array(
'contain' => 'Category'
))
)
));
Here's what my models look like.
// Category Model
class Category extends AppModel {
var $name = 'Category';
var $hasAndBelongsToMany = array(
'Post' => array(
'className' => 'Post'
)
);
var $actsAs = array('Containable');
}
// Post Model
class Post extends AppModel {
var $name = 'Post';
var $hasAndBelongsToMany = array(
'Category' => array(
'className' => 'Category'
)
);
var $actsAs = array('Containable');
var $virtualFields = array(
'date_posted' => 'DATE_SUB(Post.created, INTERVAL 7 DAY)'
);
}
A sample data would be like this:
categories
id name
1 holidays
2 destinations
posts
id title
1 I am a post
2 I am another post
categories_posts
post_id category_id
1 1
2 2
2 1
I am retrieving posts from holidays.
Array
(
[Category] => Array
(
[id] => 3
[name] => holidays
[uri] => holidays
[created] => 2010-11-25 20:43:03
[modified] => 2010-11-25 20:43:03
)
[Post] => Array
(
[0] => Array
(
[id] => 1
[title] => I am a post
),
[1] => Array
(
[id] => 2
[title] => I am a another post
)
)
)
The problem is that 1 of the posts are in two categories. I'd like to have that information also available.
Close. Try this:
$this->set('posts', $this->Category->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post' => array('Category'))
)
));
Tangent
Writing Controller code like this has problems:
It will bloat your controller methods and make your controllers less readable.
It prevents you from re-using business logic among your different controllers.
You can satisfactorily correct these problems by moving your find call to a model method:
// Categories Controller
$this->set('posts', $this->Category->get_posts_with_cats($uri));
// Category Model
function get_posts_with_cats($uri) {
$this->find('first', array(
'conditions' => array('Category.uri' => $uri),
'contain' => array('Post' => array('Category'))
));
}
This makes your code More Awesome:
The controller is nice and Skinny, and super readable.
Now you can call the same method from any associated model. So, for instance, in your PostsController you could call: $this->Post->Category->get_posts_with_cats($uri);. Your code is now DRY.
Tangent #2
After you learn how to nest containable models like I've shown you, you may be tempted in the future to do something like this:
$this->find('first', array(
'conditions' => array('Category.uri' => $uri),
'contain' => array(
'Post' => array(
'Category' => array(
'Tag' => array('Author')
)
)
)
));
The good news? You will get the desired model data from the above method (as long as you've defined all of these relationships).
The terrible news? CakePHP doesn't know how to optimize the queries for deep associations like this. So, as your database grows, you'll end up with dozens (if not hundreds (if not thousands)) of SELECT queries to the database, effectively bringing your application to a crawl (if not a halt.)
The good folk that design CakePHP are working on a better containable behavior for CakePHP 2.0, but you'll have to wait until then to get this level of precision.
I'm leaving my other answer as-is because it contains a lot of good info for those who may stumble upon it. As far as solving your problem more elegantly? You'll need to flip the find on it's head and search from the Post model with an ad-hoc join, like this:
// From the CategoriesController
$this->Category->Post->find('all', array(
'joins' => array(
array(
'table' => 'categories_posts',
'alias' => 'CategoryFilter',
'type' => 'inner',
'conditions' => array(
'CategoryFilter.post_id = Post.id'
)
),
array(
'table' => 'categories',
'alias' => 'CategoryUriFilter',
'type' => 'inner',
'conditions' => array(
'CategoryUriFilter.id = CategoryFilter.category_id'
)
)
),
'conditions' => array(
'CategoryUriFilter.uri' => $uri
),
'contain' => array('Category')
));
The array returned will be formatted a bit differently, but it should contain all of the data you were looking for.
I actually did this to get what I want:
function getPostsFromCategory($uri) {
return $this->populatePosts($this->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post' => array('Category'))
)
));
}
function populatePosts($categoryPosts) {
$posts = $categoryPosts['Post'];
$count = count($categoryPosts['Post']);
for ($i = 0; $i < $count; $i++) {
$categories = $this->Post->find('first', array('conditions' => array('Post.id' => $categoryPosts['Post'][$i]['id']),
'contains' => array('Category')));
// unset($categories['Post']);
foreach ($categories['Category'] as $cat) {
$categoryPosts['Post'][$i]['Categories'][] = $cat;
}
unset($categories);
}
return $categoryPosts;
}
I really don't know if this can be done in a more CakePHP-y way though:
I returns something like this:
Array
(
[Category] => Array
(
[id] => 3
[name] => festivals
[uri] => festivals
[created] => 2010-11-25 20:43:03
[modified] => 2010-11-25 20:43:03
)
[Post] => Array
(
[0] => Array
(
[id] => 28
[title] => A Post
[Categories] => Array
(
[0] => Array
(
[id] => 3
[name] => festivals
[uri] => festivals
[created] => 2010-11-25 20:43:03
[modified] => 2010-11-25 20:43:03
)
[1] => Array
(
[id] => 4
[name] => destinations
[uri] => destinations
[created] => 2010-11-28 13:04:52
[modified] => 2010-11-28 13:04:52
)
)
)
)

Resources