I am not sure about the question title but tried to explain more below. Thanks in advance for all who helps me.
I sampled the tables to simplify my questions.
DB Tables
News: news_id, title
Comments: comment_id, news_id, content, created (timestamp)
Users are posting comments to the news. In a search with the input of "date interval 01.10.2015 - 05.11.2015", the result should include: the news which has been commented between this interval. And comment_count should be shown next to the title on the screen. This comment_count is not whole comment_count, only the count between this dates.
To do that, I ran the query first in comments. Then count comments. Then group by news_id.
$Data = $this->Comment->find('all', array(
'fields' => array(
'News.news_id','News.title'
),
'conditions' => array(
"Comment.created >=" => date('Y-m-d', strtotime("-1 days")) //yesterday's commented news
),
'contain' => array(
'News' => array(
'Comment => array(
'fields' => array('COUNT(Comment.id) as comment_count'),
)
)
),
'group' => array('News.title')
));
The result is below with too much nesting:
Array
(
[0] => Array
(
[News] => Array
(
[id] => 27
[title] => sample news title
[News] => Array
(
[id] => 27
[title] => sample news title
[Comment] => Array
(
[0] => Array
(
[News_id] => 27
[Comment] => Array
(
[0] => Array
(
[Comment_count] => 1
)
)
)
)
)
)
)
)
In conclusion, to reach comment_count, I have to write the nestings manually. However I would like to reach it in the same level with first [title] node.
$JsonData [] = array(
"id" => $Item["News"]["id"],
"title" => $Item["News"]["title"],
"comment_count" => $Item["News"]["News"]["Comment"][0]["Comment"][0]["comment_count"]
);
How can I reduce the life-long nesting?
Thanks in advance.
First of all, you should name your table id like id not news_id. For your question, I'm not sure from what controller you are displaying news, IMO this should be done in NewsController. Here is how you can retrieve your news with comments count from previous day:
$Data = $this->News->find('all', array(
'fields' => array(
'News.id',
'News.title',
'COUNT(Comment.id) as comment_count'
),
'group' => array('News.id'),
'joins' => array(
array(
'table' => 'comments',
'alias' => 'Comment',
'conditions' => array(
'News.id = Comment.news_id',
'Comment.created >=' => date('Y-m-d', strtotime("-1 days"))
)
)
)
));
This should give you resulting array like this:
array(
(int) 0 => array(
'News' => array(
'id' => '1',
'title' => 'New news'
),
(int) 0 => array(
'comment_count' => '2'
)
),
(int) 1 => array(
'News' => array(
'id' => '2',
'title' => 'Seconds news'
),
(int) 0 => array(
'comment_count' => '1'
)
),
)
Then you can get your comment count(assuming through foreach loop):
... "comment_count" => $Data[0]['comment_count'] ...
You should look counterCache it might be useful to you.
Related
Docs state that we can name calculated fields using a syntax like Timelog__TotalHours and PHP will parse it as:
[0] => Array
(
[ProductsItems] => Array
(
[Total] => 50
)
)
In my CakePHP/2.9.4 set up I'm trying it in several places, such as pagination, but I get the column name handled as any other string:
[0] => Array
(
[ProductsItems__Total] => 50
)
My test code in the controller is:
$this->Paginator = $this->Components->load(
'Paginator',
array(
'fields' => array(
"CONCAT_WS(' ', Usuario.nombre, Usuario.apellidos) AS Usuario__Empleado",
),
'contain' => array(
'Usuario',
),
'limit' => 2,
)
);
debug($this->Paginator->paginate());
... and prints:
array(
(int) 0 => array(
(int) 0 => array(
'Usuario__Empleado' => 'Juan García'
),
'Usuario' => array(
'id' => '56'
)
),
(int) 1 => array(
(int) 0 => array(
'Usuario__Empleado' => 'María López'
),
'Usuario' => array(
'id' => '385'
)
)
)
Do I need explicitly to enable this feature somewhere?
First you should define the virtual field before your query (can be model or controller).
$this->CurrentModel->Usuario->virtualFields['Empleado'] = 0;
$this->Paginator = $this->Components->load(
'Paginator',
array(
'fields' => array(
"CONCAT_WS(' ', Usuario.nombre, Usuario.apellidos) AS Usuario__Empleado",
),
'contain' => array(
'Usuario',
),
'limit' => 2,
)
);
debug($this->Paginator->paginate());
It should work.
hello I have a table name University_Industry. It has uni_id and ind_id as foriegn keys of University table and Industry table. So meaning university will have many industries and industries will have many universities. I am getting the result successfully by running this query
public function getAllUniv(){
return $this->find('all', array(
'order' => 'uni_id',
));
}
This query returns me universities and their industries.Result look like this
Array
(
[0] => Array
(
[University] => Array
(
[uni_id] => 1
[uni_name] => NYC
)
[Industry] => Array
(
[0] => Array
(
[ind_id] => 1
[ind_name] => Finance
[UniversityIndustry] => Array
(
[id] => 1
[uni_id] => 1
[ind_id] => 1
)
)
[1] => Array
(
[ind_id] => 2
[ind_name] => Accounting
[UniversityIndustry] => Array
(
[id] => 2
[uni_id] => 1
[ind_id] => 2
)
)
)
)
Now I have another table which called users which has uni_id also as a foreign key. So I am looking for a query which can fetch only those universities with the industries which are found in users table. So It should fetch only those universities whose uni_id is exist in users table
I have no idea how can I join users table in my query and get the results.
This is how my University Modal look like at the moment
class University extends AppModel
{
public $useTable = 'university';
public $primaryKey = 'uni_id';
public $hasAndBelongsToMany = array(
'Industry' =>
array(
'className' => 'Industry',
'joinTable' => 'university_industry',
'foreignKey' => 'uni_id',
'associationForeignKey' => 'ind_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
),
);
public function getAllUniv(){
return $this->find('all', array(
'order' => 'uni_id',
));
}
}
You can join data in your find query.
$this->find('all', array(
'order' => 'uni_id',
'joins' => array(
array(
'table' => 'users ',
'alias' => 'UserJoin',
'type' => 'inner',
'conditions' => array(
'UserJoin.uni_id = University.id'
)
),
));
Here is official doc. Hope it will help you
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!
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?
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
)
)
)
)