CakePHP - trying to order posts by relevance to tags shared with main post and date created - mysql

I'm trying to display eight of the most relevant posts, in order of relevance, based on tags shared with the current post being viewed and the date created...
Models:
Tag habtm Post
Post habtm Tag
DB:
posts(id, slug, ...)
tags(id, tag, ...)
posts_tags(post_id, tag_id)
Within controller action:
$post = $this->Post->find('first', array('conditions' => array('slug' => $slug)));
$this->set('post', $post);
$tags = $post['Tag'];
$relOrd = '';
foreach($tags as $tag){
$tagId = $tag['id'];
$relOrd .= " + (CASE WHEN PostsTag.tag_id = ".$tagId." THEN 1 ELSE 0 END)";
}
$relOrd = '(' . substr($relOrd, 3) . ') AS Relevance';
$morePosts = $this->Post->find('all', array(
'joins' => array(
array(
'table' => 'posts_tags',
'alias' => 'PostsTag',
'type' => 'LEFT',
'conditions' => array(
'PostsTag.post_id = Post.id',
)
)
),
'group' => 'Post.id',
'fields' => array($relOrd, 'Post.*'),
'order' => array('Relevance' => 'DESC', 'Post.created' => 'DESC'),
'limit' => 8,
));
$this->log($morePosts);
$this->set('morePosts', $morePosts);
It's almost working, although the relevance value is being treated as if each post has only one tag (being only 0 or 1). So it seems that the relevance value for each post is taking either 0 or 1 depending on the posts LAST tag rather than being accumulative based on ALL tags.

First of all, I'd take all of the logic out of the controller. Consider this:
$post = $this->Post->find('first', array('conditions' => array('slug' => $slug)));
$this->set('post', $post);
$this->set('morePosts', $this->Post->findRelevant($post));
Now your controller is easy to read and does it's job. You essentially start by describing what data you want by naming an imaginary model function, and then you write the model code to fulfill that request.
So here's a stab at the model code:
var $actsAs = array('Containable');
function findRelevant($post, $limit = 8) {
// create an array of ids of the tags from this post
$tags = array();
foreach($post['Tag'] as $num => $tag) {
$tags[$tag['id']] = $tag['id'];
}
// find other posts that have any of those tags
$relevant = $this->find('all', array(
'conditions' => array('Post.id <>' => $post['Post']['id']),
'order' => 'Post.created desc',
'contain' => array('Tag' => array('conditions' => array(
'Tag.id' => $tags
))),
));
// count the number of tags of each post and call it relevance
// (this number is essentially the number of tags in common
// with the original post because we used contain to get only
// the tags from the original post)
foreach($relevant as &$p) {
$p['Post']['relevance'] = count($p['Tag']);
}
// sort by relevance
$relevant = Set::sort($relevant, '{n}.Post.relevance', 'desc');
// limit the number of posts returned (defaults to 8)
return array_splice($relevant, 0, $limit);
}
Obviously it would be great to use the database logic to fetch the records (as you are attempting to do) so that it's as fast as possible and so that you minimize the amount of data that you retrieve, but I can't see how to do that for what you're trying to achieve.
This method should work fine and is not database specific. :)

Related

WordPress query posts without ACF repeater subfields AND specific value

I have two custom post types, playlists and games. Playlists have a ACF repeater field, that contain games.
My goal: I am trying to build a simple query, that gets all playlists that DO NOT have this game listed in the repeater field AND alll playlists that are empty (do not have any repeater fields / database entries at all). I want to use the get_posts() function to achieve my goal, if possible.
My problem: I can only get the query to show ONLY the playlists that HAVE the game by using the = operator in the "game_filter" meta query compare field (but I want the opposite). If I choose the != or <> or IS NOT operator, it spits out all available posts. And if I also include my "empty_playlists" meta query, it ALWAYS shows me all playlists, no matter what operators I use, even tho it is a OR and the listed posts do have entrys.
I tried a lot but just cant get it to work. All exmaples I found were about different things, mostly about the % problem with the repeater field names and its solution. I hope someone can help me with this real quick. What am I doing wrong? Please help fellow and better coders! :-)
This is my code
$excluded_playlists_game_id = 274;
$user_id = 1;
$args = array(
'post_type' => PSiCorePostTypes::PLAYLISTS,
'author' => $user_id,
'order_by' => 'title',
'order' => 'ASC',
'suppress_filters' => false,
'meta_query' => array(
'relation' => 'OR',
'game_filter' => array(
'type' => 'NUMERIC',
'key' => 'psi_playlist_games_%_item',
'compare' => '!=',
'value' => $excluded_playlists_game_id,
),
'empty_playlists' => array(
'key' => 'psi_playlist_games_%_item',
'compare' => 'NOT EXISTS',
),
),
);
$user_playlists = get_posts( $args );
and this function for the % problem.
add_filter( 'posts_where', 'get_user_playlists_query_allow_wildcard' );
function get_user_playlists_query_allow_wildcard( $where ) {
global $wpdb;
$where = str_replace(
"meta_key = 'psi_playlist_games_%_item",
"meta_key LIKE 'psi_playlist_games_%_item",
$wpdb->remove_placeholder_escape( $where )
);
return $where;
}

CakePHP 3: Retrieving deep associations

My model has
Conversations - hasMany - Messages
Conversations - hasMany - ConversationsRecipients
ConversationsRecipients - belongTo - Users or Applicants (depending on the flag set by field recipient_type. If recipient_type is A then it means Applicants)
So when I try to retrieve conversations for a particular Applicant, I use the following code
$conversationsTable = TableRegistry::get('Conversations');
$conversations = $conversationsTable->find()
->join([
'ConversationsRecipients' => [
'table' => 'conversations_recipients',
'type' => 'inner',
'conditions' => ['recipient_id' => $id, 'recipient_type' => 'A']
]
])
->contain([
'Messages.Users' => function ($q) {
return $q
->select(['Users.username'])
->contain(['UsersProfiles']);
},
'Messages.Applicants' => function($q) {
return $q
->select(['Applicants.firstname', 'Applicants.lastname']);
}
])
->all();
return $conversations;
This works fine - except for one part - but it doesn't retrieve the deeply contained model - UsersProfiles. Am I missing something?
Try this:
return $q
->select(['Users.username'])
->autoFields(true)
->contain(['UsersProfiles']);
When you include a select in your query, that's all that Cake will include, unless you include the autoFields call.

CakePHP Model Query - Complex - Multi Table - Sum - Count - Group By

Hello and happy holidays everyone.
Recently I have been tasked with transforming a beta application from pure PHP/jQuery to CakePHP/ExtJS (Which I am new to).
My issue is with the most complex query that populates the main grid.
To keep things simple there are 3 tables with correct baked relationships and models: Projects, ToDo, ExtraToDo
Projects hasMany ToDo and ExtraToDo.
ToDo and ExtraToDo have columns Title and Complete.
My goal is to get a completion percent for each project based on these three tables.
The way I have gone about this is the SUM of the Complete column divided by the COUNT of the Complete column. I am trying in a CakePHP way for readability/performance/otherstuffIdontknowyet.
Originally, in raw SQL I had done it like this:
SELECT
`idProject`,
(SELECT
ROUND((SUM(`Complete`) / COUNT(`Complete`)) * 100),
FROM
(SELECT `Complete`, `ProjectID` FROM `ToDo`
UNION ALL
SELECT `Complete`, `ProjectID` FROM `ExtraToDo`) orders
WHERE
`ProjectID` = `idProject`
) AS 'Completion'
FROM
`Projects`
I also got this to work in the Kohana PHP MVC framework fairly easily which I tried before deciding on CakePHP. I LOOOVED how their queries were created...:
private function get_completion() {
$Query = DB::select('ProjectID', array(DB::expr('ROUND((SUM(`Complete`) / COUNT(`Complete`)) * 100)'), 'Completion'))
->from(array('ToDo', 'ExtraToDo'))
->group_by('ProjectID');
return $Query;
}
public function get_all() {
$Query = DB::select()
->from('Projects')
->join(array(self::get_completion(), 'Completion'))
->on('projects.id', '=', 'Completion.ProjectID')
->execute()
->as_array();
return $Query;
}
Unfortunately I have completely struggled to get this working in CakePHP while doing it the CakePHP way.
I'm pretty sure virtualFields are the key to my answer but after reading the documents and trying x, y, AND z. I have been unable to comprehend them and how they relate.
Thank you in advance
-T6
That is a lot of nested selects. IMO you would be better off building a better query.
This should get you going.
class Project extends AppModel {
public $findMethods = array(
'completion' => true
);
// other code
protected function _findCompletion($state, $query, $results = array()) {
if ($state == 'before') {
$this->virtualFields['total'] = 'ROUND((SUM(Todo.Complete + TodoExtra.Complete) / (COUNT(Todo.Complete) + COUNT(TodoExtra.Complete))) * 100)';
$query['fields'] = array(
$this->alias . '.' . $this->primaryKey,
'total'
);
$query['joins'] = array(
array(
'table' => 'todos',
'alias' => 'Todo',
'type' => 'left',
'foreignKey' => false,
'conditions'=> array('Todo.project_id = ' , $this->alias . '.' . $this->primaryKey)
),
array(
'table' => 'todo_extras',
'alias' => 'TodoExtra',
'type' => 'left',
'foreignKey' => false,
'conditions'=> array('TodoExtra.project_id = ' . $this->alias . '.' . $this->primaryKey)
),
);
$query['group'] = array(
$this->alias . '.' . $this->primaryKey
);
return $query;
}
return $results;
}
// other code
}
Now you have a custom find method that can be used like find('first') or find('all').
From the controller:
$this->Project->find('completion');
Or in the Project model
$this->find('completion');
It should return something like this:
$results = array(
0 => array(
'Project' => array(
'id' => 1,
'total' => 50
)
),
1 => array(
'Project' => array(
'id' => 2,
'total' => 75
)
)
);
I would suggest either creating an afterFind() function to the Project model class, or simply just adding a function that you would call when you need to perform this calculation.
The function to perform the calculation would look like:
getPercentageComplete($project){
{
$total_todos = count($project['ToDo']);
$completed_todos = 0;
foreach($project['ToDo'] as $todo){
if($todo['Complete']) //assuming this is a boolean field
$completed_todos++;
}
return $completed_todos / $total_todos;
}
Then, your afterFind would look something like this:
function afterFind(&$results)
{
foreach ($results as &$project)
{
$project['Project']['percentageComplete'] = $this->Project->getPercentageComplete($project);
}
return $results;
}
You can see more about afterFind() at the CakePHP Bakery - > Callback Methods

Retrieve all data from one table, and some from another in CakePHP

In my CakePHP site, I want to make a drop-down list of all Venues, and any Restaurants that have is_venue=1.
I've tried this in my events_controller:
$venueOptions = array(
'fields' => array('id', 'name_address'),
'order' => array('name'),
'join' => array(
array(
'table' => 'restaurants',
'alias' => 'Restaurants',
'type' => 'inner',
'fields' => array('id', 'name'),
'foreignKey' => false,
'conditions' => array('restaurants.is_venue = 1')
)
),
);
$venues = $this->Event->Venue->find('list', $venueOptions);
But it appears to still just be getting the venues. I don't really need an association between the two, since their associations will both be with an event, not each other.
Where have I gone wrong? Am I close, but just need to tweak this code, or am I just all-together doing it wrong?
I think you could do something along the lines of:
<?php
....
$v = $this->Venue->find( 'list' );
$r = $this->Restaurant->find( 'list' );
$venues = Set::merge( $v, $r );
natcasesort( $venues );
// print_r( $venues );
$this->set( 'venues', $venues );
...
?>
Which is quite like the code above - I just use the Set class and make sure to Controller::set the variable to the view.
Also added some basic sorting to show you one option even though array sorting has nothing really specific to do with CakePHP.
Also fixed some bad variable names where I had originally used $venues, and $restaurants - changed to be consistently $v and $r.
Join will not work if there's no relation between. Venue and Restaurant. You should call them separately and merge the results
$venues = $this->Event->Venue->find('list', $venueOptions);
$restaurants = $this->Event->Restaurant->find('list', array('conditions' => array('is_venue' => '1')));
$results = array_merge($venues, $restaurants);
// sort results
asort($results);

Cakephp model associastion

I have thee following simple model:
Item belongsTo CatalogItem
CatalogItem hasMany Item, and belongsTo Section
Section hasMany CatalogItem
I'm trying to get counts of items, grouped by catalogitem, for a certain section-
the equivalent of:
SELECT catalogitem.id, count(*) FROM section LEFT JOIN catalogitem ON section.id=catalogitem.section_id LEFT JOIN item ON item.catalogitem_id=catalogitem.id WHERE section.id=5 GROUP BY catalogitem.id
So simple in sql, yet I can't get it to work with cake models. Can anyone point as to how to do it with cake models, using the model->find?
I can't get it to group by correctly or join correctly on 3 tables :(
Edit:
highly prefer to get the info in single query
Here's a longer way, "cakeish" way:
class Item extends AppModel
{
/* snip */
var $virtualFields = array('item_count' => 'count(Item.id)');
function getCountForSection($sectionId)
{
$ca = $this->Catalogitem->find
(
'all',
array
(
'fields' => array('Catalogitem.id'),
'conditions' => array('Catalogitem.section_id' => $sectionId),
'recursive' => -1
)
);
$ca = Set::extract('/Catalogitem/id', $ca);
$ret = $this->find
(
'all',
array
(
'fields' => array('Item.catalogitem_id', 'item_count'),
'conditions' => array('Item.catalogitem_id' => $ca),
'group' => array('Item.catalogitem_id'),
'recursive' => -1
)
);
return $ret;
}
}
Then simply use it in your controller:
$ret = $this->Item->getCountForSection(1);
debug($ret);
How does it work:
Define a virtual field (cake 1.3+ only AFAIK) which will count items
Fetch all the Catalogitems belonging to a Section you're interested in
Use Set::extract() to get the Catalogitems in a simple array
Use the array of Catalogitems to filter Items while counting and grouping them
NB: You don't seem to be using Cake's naming conventions in your database. This may hurt you.
Sorry, in my first answer I somehow missed your GROUP BY requirement, which was the whole point of the question, I now realize. I haven't used this yet, but I came across it recently, and it looks like it might accomplish what you are looking for: Linkable Behavior.
http://planetcakephp.org/aggregator/items/891-linkable-behavior-taking-it-easy-in-your-db
Like Containable, but works with only right and left joins, produces much more compact queries and supports GROUP BY.
http://github.com/rafaelbandeira3/linkable
#azv
Would this work for you:
$section_id = 5;
$fields = array('CatalogItem.id as CatalogItemId', 'count(*) AS SectionCount');
$conditions = array('Section.id' => $section_id);
$joins = array(
array('table' => 'catalogitem',
'alias' => 'CatalogItem',
'type' => 'LEFT',
'conditions' => array('Section.id' => 'CatalogItem.section_id')
),
array('table' => 'item',
'alias' => 'Item',
'type' => 'LEFT',
'conditions' => array('Item.catalogitem_id' => 'CatalogItem.id')
));
$data = $this->Section->find('all',
array('fields' => $fields,
'conditions' => $conditions,
'joins' => $joins,
'group' => 'CatalogItem.id',
'recursive' => -1)
);
// access your data values
foreach ($data['Section'] as $i => $datarow) {
$catalogitem_id = $datarow['CatalogItemId'];
$section_count = $datarow['SectionCount'];
}
This way you are explicitly setting your joins and doing it all in one query. See here for more info on joins in Cake:
http://book.cakephp.org/view/1047/Joining-tables
Hope this helps. All the best,
-s_r