I am aiming to create a summary page of activity on an application I am currently working on. I have identified that I must do the following:
Get all stories from people I am subscribed to and format them like the following:
[Username] has posted [StoryName] - View story
Get all stories that users I am connected to have posted comments on
[Username] has posted a comment on [StoryName] - View story
I am unsure how I can get both arrays, display them the format I want but order them by the posted date (in the same way people like Facebook do)
What is the best way to go about this?
Please Note: The answer must be something which is easily extendible. I am considering following wordpress' model and creating a Posts table which has a Post Type field.
What you're trying to do is pretty much built-in with CakePHP. The big thing is to make sure that your models are properly associated. Once this is accomplished, Cake will do most of the heavy lifting for you.
For your situation, use 3 models associated like this:
class Story extends AppModel{
var $belongsTo = 'Author';
var $hasMany = 'Comment';
}
class Author extends AppModel{
var $hasMany = array( 'Story', 'Comment' );
var $hasAndBelongsToMany = 'User';
}
class Comment extends AppModel{
var $belongsTo = array( 'Author', 'Story' );
}
Set up your tables according to the Cake conventions. Then a little CakePHP magic in your controller:
$this->Story->Author->bindModel( array( 'hasOne' => array( 'AuthorsUsers' ) ) );
$myAuthors = $this->Story->Author->find( 'list', array(
'fields' => array( 'id' ),
'conditions' => array( 'AuthorsUsers.user_id' => $userId ),
'recursive' => false
) );
$stories = $this->Story->find( 'all', array(
'fields' => array( 'Story.id', 'Story.title', 'Author.id', 'Author.name' ),
'order' => 'published_date DESC',
'conditions' => array( 'Author.id' => $myAuthors ),
'recursive' => 2
) );
Quick explanation of what's going on:
bindModel() lets Cake know that you want to use the HABTM association to find Authors by the associated User id. (Cake convention is to have a table called 'authors_users' to join the Author->User HABTM association.)
If you debug the $myAuthors variable, you'll see that it gets a simple array of ids.
'conditions' => array( 'field' => array() ) will get parsed as "WHERE field IN (...)". In this example, we get all models WHERE 'Author.id' IN $myAuthors.
The short $this->Story->find() call is the beauty of Cake. It will automatically find all Story models matching the specified conditions, and it will find the other models associated with each found Story. (You can tell it not to find associated models by turning off Recursive or using Containable behavior.)
Debugging the $stories variable will show you a structure like the following:
Array
(
[0] => Array
(
[Story] => Array
(
[id] => 1
[title] => 'Common Sense'
[published_date] => 1776-01-10
)
[Author] => Array
(
[id] => 1
[name] => 'Thomas Paine'
)
[Comment] => Array
(
[0] => Array
(
[id] => 1
[text] => 'Revolutionary!'
[Author] => Array
(
[id] => 3
[name] => 'Silence Dogood'
)
)
[1...n] => ...
)
)
[1...n] => ...
)
You can then use that structure in your View to display data as you desire.
I have a feeling there should be a way to do it with just 1 query, but this way works and doesn't require you to do any subqueries or insert custom SQL into your Cake find() call.
Gary, it is difficult to answer this without knowing your models. Assuming you have got correctly defined schema and models it should be relatively easy.
For your find queries order by date DESC and retrieve the last 10 rows. (this would give you last 20 results for example)
Merge the result sets into one and sort by date DESC again. It's then some trivial rendering logic to render the strings as you need.
Well, I would do something like this
Create the following Relationships
User hasMany StoryName
User hasMany Comments
StoryName hasMany Comments
Comments belongsTo User
Comments belongsTo StoryName
That way would be really easy to retrieve all the information needed, since you could fetch all the needed data though the User model with something like
$this->User->recursive = 2;
$this->User->find('all', $params) *// on $params you could use the conditions that the user retrieved should be friends to the current user*
Hopefully that would give me an Array populated with the following structure
[USER]
[POSTS]
[COMMENTS]
And was just a matter of passing it to the view and using a foreach in the view to create the Html
I don't know if you you already have other models setup (User, Story, etc.). But to keep the answer to the point, I'll just focus on Activity model (activities table): You can use this table like a log table.
A simple table: id, user_id, story_id, created (datetime), action_type(0 for post story, 1 for comment on a story, for example). Then it's simply a matter of querying this table to find the activities.
Or a more generic, extensible one: id, subject_id, subject_type (User or whatever is acting), verb_type, object_id, object_type, created. You don't really have to bind this model to any other model (Although you can with the 'conditions' when you specify relationship) or bindModel on the fly.
Related
So quick overview of what I'm trying to do. User hasMany BusinessUnitsUser. BusinessUnit hasMany BusinessUnitUser.
In this manner, I've normalized their relationship to two hasManys (I hate working with HABTM related data).
On a new user form, I'd like to give the user the option of adding a new Business Unit at the same time.
Currently, my $this->request->data array looks like this:
Array
(
[User] => Array
(
[title] => Mr
[first_name] => Kyle
[last_name] => O'Brien
[username] => 55546#mylife.unisa.ac.za
[producing_office_id] => 4
)
[BusinessUnit] => Array
(
[name] => lskfjsldkfjsdlfk
)
)
Now, this is obviously incorrect, but I'm struggling to think of how to resolve this. I thought giving you a visual of the array, you might be able to tell me what my array should look like.
Here's my relationship array in BusinessUnitsUser:
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'BusinessUnit' => array(
'className' => 'BusinessUnit',
'foreignKey' => 'business_unit_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
EDIT (thanks ndm)
Note the goal: I'm adding a user and I may or may not be adding BusinessUnitsUser associations as well but I'm definitely also going to be adding a new BusinessUnit - the form for which exists on the new_user form.
Currently, the User is saving with a $this->User->saveAll($this->request->data) but NOT the new BusinessUnit and certainly not the new BusinessUnitsUser entry (which I expect to contain the new user id and the new business unit id.
Save via the join table model
The solution should be pretty simple, for that type of association you'll have to save via the join table model (even if you don't save additional data on the that table):
$this->User->BusinessUnitsUser->saveAssociated($this->request->data);
See also http://book.cakephp.org/.../saving-your-data.html#saving-hasmany-through-data
That way the ORM can properly save the belongsTo associated data, and create an appropriate record in the join table, using foreign key values obtained from the save operations for the associated data.
I have an images table and a servers table. images has a server_id field which is a foreign key to the id field in the servers table. The servers table also has a field called name, which is what I want to retrieve.
Here's my controller action code:
$images = $this->Image->find('all', array(
'conditions' => array('Image.user_id' => $this->Auth->user('id')),
'order' => array('Image.uploaded DESC')
));
$this->set('images', $images);
It gets data like this:
Array
(
[0] => Array
(
[Image] => Array
(
[id] => 103
[orig_name] => Untitled-5.jpg
[hash] => MnfWKk
[filename] => MnfWKk.jpg
[uploaded] => 2012-07-12 00:09:08
[views] => 0
[album_id] =>
[user_id] => 15
[server_id] => 1
)
)
)
Instead of server_id, I want to get the name field from the servers table. How can I adapt my find() method to get this? I know it's an SQL join, but I have no idea how to tell Cake to do one in order to get the servers name.
Thanks.
TLDR:
Set up the correct CakePHP associations, and use CakePHP's Containable. (with recursive -1).
Longer Description:
It's best practice to keep your find code in the model itself, so that's what I'll show, but feel free (if you must) to move it back into the controller.
Doing it this way allows you to call the same getImages() function from any controller, and just pass different parameters based on what you want returned. The benefit to coding like this is, you always know if you're looking for code related to queries/database, that you should be looking in the model. It's VERY beneficial when the next person who looks at your code doesn't have to go searching.
Because of the association set up between Image and Server, you can then "contain" the Server info when you query images. But - you can't use "contain" until you specify that you want your model to $actAs = array('Containable');. [ CakePHP Book: Containable ]
Lastly, in your AppModel, it's good practice to set $recursive = -1;. That makes it default to -1 for all models. If for some reason you're against doing that, just make sure to set recursive to -1 any time you use containable. And - once you learn to use containable, you'll never look back - it's awesome. There are a lot more things you can
Code:
//AppModel *******
//...
$recursive = -1;
//...
//Images controller *******
//...
public function whatever() {
$opts = array();
$opts['user'] = $this->Auth->user('id');
$images = $this->Image->getImages($opts);
$this->set(compact('images'));
}
//...
//Image model *******
//...
public $actsAs = array('Containable');
public belongsTo = array('Server');
public function getImages($opts = array()) {
$params = array('condtions'=>array());
//specific user
if(!empty($opts['user'])) {
array_push($params['conditions'], array('Image.user_id'=>$opts['user']);
}
//order
$params['order'] = 'Image.uploaded DESC';
if(!empty($opts['order'])) {
$params['opts'] = $opts['order'];
}
//contain
$params['contain'] = array('Server');
//returns the data to the controller
return $this->find('all', $params);
}
A few other notes
You should also set the association in your Server model.
The code example I gave is written fairly verbosely (is that a word?). Feel free to condense as you see fit
You can also extend the model's getImages() method to accept a lot more parameters like find, limit...etc. Customize this all you want - it's not THE way to do it - just similar to what I usually use.
Per your question, if you only need one field, you can specify in the "contain" what fields you want - see the book for details.
It might seem confusing now, but it's SOO worth learning how to do this stuff right - it will make your life easier.
Cake have a lot of model relationships to achieve this. Check out this page, I think you'll be using the belongsTo relationship
Easy and alternate way for begginers
$images = $this->Image->find('all', array(
'conditions' => array('Image.user_id' => $this->Auth->user('id')),
'joins' => array(
array(
'table' => 'servers',
'alias' => 'Server',
'type' => 'inner', //join of your choice left, right, or inner
'foreignKey' => true,
'conditions' => array('Image.server_id=Server.id')
),
),
'order' => array('Image.uploaded DESC')
));
This is very good in performance
I've been quite some time trying to use the Containable Behavior in CakePHP but I can't get to make it work as I expected.
My application is different, but to simplify I'll put this example. Let's say I have a forum with threads and activities, and the activities can be rated. The general relations would be:
Forum: hasMany [Thread]
Thread: belongsTo [Forum], hasMany [Activity]
Activity: belongsTo [Thread], hasMany [Rating]
Rating: belongsTo [Activity]
What I want to achieve is, using the find method, get all the ratings performed on a certain forum. What I suppose should be done is the following:
$this->Rating->find('count', array(
'contain' => array(
'Activity' => array(
'Thread'
)
),
'conditions' => array(
'Thread.forum_id' => 1
)
));
But the result query is:
SELECT COUNT(*) AS `count` FROM `ratings` AS `Rating` LEFT JOIN `activities` AS `Activity` ON (`Rating`.`activity_id` = `Activity`.`id`) WHERE `Thread`.`forum_id` = 1;
I've accomplished this using the 'joins' option, but it's more complex and I have to use this kinda action in many situations.
All the files related with the example can be found here: http://dl.dropbox.com/u/3285746/StackOverflow-ContainableBehavior.rar
Thanks
Update 23/11/2011
After investigating the framework and thanks to the answers of Moz Morris and api55 I found the source of the problem.
The basic problem was that, as I understood CakePHP, I thought it was querying using joins each time. The thing it that it doesn't do that, the real operation it would perform to obtain the result I was looking for would be something like this:
SELECT * FROM Rating JOIN Activity...
SELECT * FROM Activity JOIN Thread...
SELECT * FROM Activity JOIN Thread...
...
Meaning that it would do a query to get all the activities and then, for each activity, perform a query to get the Threads... My approach was failing not because of the Containable Behaviour being used wrong, but because the 'conditions' option was applied to all queries and, on the first one, it crashed because of the absence of the Thread table. After finding this out, there are two possible solutions:
As api55 said, using the conditions inside the 'contain' array it would apply them only to the queries using the Thread table. But doing this the problem persists, because we have way too many queries.
As Moz Morris said, binding the Thread model to Rating would also work, and it would perform a single query, which is what we want. The problem is that I see that as a patch that skips the relations betweem models and doesn't follow CakePHP philosophy.
I marked api55 solution as the correct because It solves the concrete problem I had, but both give a solution to the problem.
First of all, have you put the actAs containable variable in the appModel?? without it this beahaviour won't work at all (i see it is not working correctly since it didn't join with Thread table)
I would do it from the top, i mean from forum, so you choose your forum (im not sure you want forum or thread) and get all its rating, if theres no rating you will end up with the rating key empty.
something like this
appModel
public $actsAs = array('Containable');
rating controller
$this->Rating->Activity->Thread->Forum->find('count', array(
'contain' => array(
'Thread' => array(
'Activity' => array(
'Rating' => array (
'fields' => array ( 'Rating.*' )
)
)
)
),
'conditions' => array(
'Forum.id' => 1
)
));
Then if you need only a value in rating table just use Set:extract to get an array of this value.
As you did it IT SHOULD work anyways but i sugest not to use forum_id there, but in conditions inside contain like this
'contain' => array(
'Activity' => array(
'Thread' => array(
'conditions' => array('Thread.forum_id' => 1)
)
)
),
Also, never forget the actsAs variable in the model using the containable behaviuor (or in app model)
Whist I like api55's solution, I think the results are a little messy - depends on what you intend to do with the data I guess.
I assume that when you said using the 'joins' method you were talking about using this method:
$this->Rating->bindModel(array(
'belongsTo' => array(
'Thread' => array(
'foreignKey' => false,
'conditions' => 'Thread.id = Activity.thread_id',
),
'Forum' => array(
'foreignKey' => false,
'conditions' => 'Forum.id = Thread.forum_id'
)
)
));
$ratings = $this->Rating->find('all', array(
'conditions' => array(
'Forum.id' => 1 // insert forum id here
)
));
This just seems a little cleaner to me, and you don't have to worry about using the containable behaviour in your AppModel. Worth considering.
Im using cakephp and Im stuck about the idea of listing multiple models, here's my scenario:
I have two major models namely Task and Events. I used the Events model to track all changes. this has the ff fields: id, model, model_id, changed
I used Events table to track multiple model changes. so when something changed to Task model it will be logged to Events models (get the idea??)
What I want to do is whenever i used the find method for Event model, i want to list the model information (for example, the Task model) together with my Events information like this:
array(
[0] => array (
[Event] => array(
id => 1
model => Task
model_id => 2
changed => array()
)
[Task] => array(
id => 2
name => Task 1
descp => Test Task
)
)
)
Note: The model can be any model, it can be Projects model.
At my task.php, it is no problem because i can easily declare:
var $hasMany = array(
'Events' => array(
'className' => 'Events',
'foreignKey' => 'model_id',
'dependent' => false,
'conditions' => array('Events.model' => 'Task')
)
);
I can get Events information using the find method on task although using
$this->Task->Event->find('all');
will not work, i dont know why.
So as soon as I declare, something like this in my Event Model:
var $belongsTo = array(
'Task' => array(
'className' => 'Task',
'foreignKey' => 'model_id',
'conditions' => array('Event.model' => 'Task', 'Event.model_id' => 'Task.id'),
'fields' => '',
'order' => ''
)
);
will throw an SQL error. Do you guys have an idea how to implement it??
Thanks in advanced. :)
I use polymorphic models all the time. The reason that $this->Task->Event->find('all') doesn't work is that you were performing the find on the Event model which, until you defined its belongsTo association, didn't have any means of retrieving the Task info.
As for now, the only thing I see that looks odd is that you've aliased your Task association as Events (plural) rather than its singular version which is the convention for models. You also identified the className as the plural version.
I'm having trouble composing a CakePHP find() which returns the records I'm looking for.
My associations go like this:
User ->(has many)-> Friends ,
User ->(has many)-> Posts
I'm trying to display a list of all a user's friends recent posts, in other words, list every post that was created by a friend of the current user logged in.
The only way I can think of doing this is by putting all the user's friends' user_ids in a big array, and then looping through each one, so that the find() call would look something like:
$posts = $this->Post->find('all',array(
'conditions' => array(
'Post.user_id' => array(
'OR' => array(
$user_id_array[0],$user_id_array[1],$user_id_array[2] # .. etc
)
)
)
));
I get the impression this isn't the best way of doing things as if that user is popular that's a lot of OR conditions. Can anyone suggest a better alternative?
To clarify, here is a simplified version of my database:
"Users" table
id
username
etc
"Friends" table
id
user_id
friend_id
etc
"Posts" table
id
user_id
etc
After reviewing what you have rewritten, I think I understand what you are doing. Your current structure will not work. There is no reference in POSTS to friends. So based on the schema you have posted, friends CANNOT add any POSTS. I think what you are trying to do is reference a friend as one of the other users. Meaning, A users FRIEND is actually just another USER in the USERS table. This is a self referential HABTM relationship. So here is what I would propose:
1- First, make sure you have the HABTM table created in the DB:
-- MySQL CREATE TABLE users_users ( user_id char(36) NOT NULL,
friend_id char(36) NOT NULL );
2- Establish the relationships in the User model.
var $hasAndBelongsToMany = array(
'friend' => array('className' => 'User',
'joinTable' => 'users_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'friend_id',
'unique' => true,
),
);
var $hasMany = array(
'Post' => array(
'className' => 'Post',
'foreignKey' => 'user_id'
),
);
3- use the scaffolding to insert a few records, linking friends and adding posts.
4- Add the view record function to the Users controller:
function get_user($id)
{
$posts = $this->User->find('first', array(
'conditions' => array('User.id' => $id),
'recursive' => '2'
));
pr($posts);
}
5- Now you can query the User table using recursive to pull the records using the following command:
http://test/users/get_user/USER_ID
6- Your output will show all of the records (recursively) including the friends and their posts in the returned data tree when you pr($posts)
I know this is a long post, but I think it will provide the best solution for what you are trying to do. The power of CakePHP is incredible. It's the learning curve that kills us.
Happy Coding!
If Post.user_id points to Friend.id (which wouldn't follow the convention btw) then it would be
$posts = $this->Post->find('all',array(
'conditions' => array(
'Post.user_id' => $user_id_array
)
);
which would result in .. WHERE Post.user_id IN (1, 2, 3) ..
Depending on your setup, it might be quicker to run two queries rather than trying to chain them together via the Cake stuff. I'd recommend adding something like getFriendsPosts() in the Users model.
<?php
class UserModel extends AppModel {
// ... stuff
function getFriendsPosts( $user_id )
{
$friends = $this->find( ... parameters to get user IDs of all friends );
// flatten the array or tweak your params so they fit the conditions parameter. Check out the Set class in CakePHP
$posts = $this->find( 'all', array( 'conditions' => array( 'User.id' => $friends ) ) );
return $posts;
}
}
?>
Then to call it, in the controller just do
$friends = $this->User->getFriendsPosts( $this->Auth->User('id') );
HTH,
Travis
Isn't CakePHP already generating the efficient code of:
SELECT * from Posts WHERE user_id IN (id1, id2 ...)
if not, you can do
$conditions='NULL';
foreach($user_id_array as $id) $conditions.=", $id";
$posts = $this->Posts->find('all', array(
'conditions' => "Post.user_id IN ($conditions)",
));
If your models are properly associated, Cake will automatically retrieve related model records. So, when you search for a specific user, Cake will automatically retrieve related friends, and related posts of these friends. All you need to do is set the recursion level high enough.
$user = $this->User->find('first', array('conditions' => array('User.id' => $id), 'recursive' => 2));
debug($user);
// gives something like:
array(
User => array()
Friend => array(
0 => array(
...
Post => array()
),
1 => array(
...
Post => array()
)
)
)
All you need to do is extract the posts from the user's friends, which is as easy as:
$postsOfFriends = Set::extract('/Friend/Post/.', $user);