I am using the tree behavior and when I query via ->find('threaded', ...) - as expected - I get the tree back.
But I want additional joins happen, so something like:
$data = $this->Category->find('threaded', array(
'joins' => array(
array('table' => 'videos',
'alias' => 'Video',
'type' => 'LEFT',
'conditions' => array(
'Category.id = Video.category_id',
)
)
)
));
Category hasMany Video, but Video is not a tree, just related.
Can I use the threaded query for that?
To produce the "threaded" output Cake 1) calls a find('all), then 2) puts the resulting array through Set::nest() function.
So just get your output using a standard find + your custom joins, then just use the Set::nest
(NB: Hash has replaced Set in Cake 2, but Cake still uses Set internally. Both will work for now. Hash::nest )
So if you take a look at Cake's model.php, the nest function is called like so:
return Set::nest($results, array(
'idPath' => '/' . $this->alias . '/' . $this->primaryKey,
'parentPath' => '/' . $this->alias . '/' . $parent
));
Use that as a template for your call. For your data it would look something like:
return Set::nest($results, array(
'idPath' => '/Category/id',
'parentPath' => '/Category/parent_id' ));
Related
I am calling a find on a model called Book which is associated with a model Page(book_id)
However Page is associated with a model called Asset(page_id). I would like to get the array with all three models
Book
Page1
Asset1
Asset2
Asset3
Page2
Asset1
Asset2
Asset3
The code I have at the moment only get me Book and Page
$options = array(
'conditions' => array('Book.' . $this->Book->primaryKey => $id),
'contain' => 'Page'
);
$books = $this->Book->find('first', $options);
Book hasMany Pages
Page hasMany Assets
You can contain deeper associations, like it says in the docs
Example from the docs
$this->User->find('all', array(
'contain' => array(
'Profile',
'Account' => array(
'AccountSummary'
),
'Post' => array(
'PostAttachment' => array(
'fields' => array('id', 'name'),
'PostAttachmentHistory' => array(
'HistoryNotes' => array(
'fields' => array('id', 'note')
)
)
),
'Tag' => array(
'conditions' => array('Tag.name LIKE' => '%happy%')
)
)
)
));
Same thing with your models...
$options = array(
'conditions' => array('Book.' . $this->Book->primaryKey => $id),
'contain' => array('Page' => array('Asset')))
);
$books = $this->Book->find('first', $options);
Should work if your associations are set correctly (and if all models implement containable behavior).
EDIT
(to address the confusion the OP had)
The nested contain options works for the model expanding the array. For example, if models are associated like this
Model-A -> Model-B -> Model-C & Model-D
-> Model-E -> Model-C
you could get the entire array with data like
Model-A
Model-B1
Model-C1
Model-C2
Model-D2
Model-B2
Model-C (null)
Model-D3
Model-E1
Model-C1
Model-C3
using something like
$this->ModelA->find('all'), array(
'contain' => array(
'Model-B' => array('Model-C', 'Model-D'),
'Model-E' => array('Model-C')
)
);
Also, you can add options to the containable array, including the ones used for searching, like 'conditions' (though be careful with this, it means that if the model doesn't match the condition it will return a null array, it does not mean the the whole "Model-A" will not be in the returned data since one of the nested conditions was not fulfilled).
I am using containable with CakePHP. My tried code is ...
public function detail($slug = "") {
$this->Poet->contain('User.id', 'User.full_name', 'Song.id', 'Song.name', 'Song.name_hindi', 'Song.slug');
$result = $this->Poet->findBySlug($slug);
if (!$result) {
throw new NotFoundException(__('Invalid Poet - ' . $slug));
}
pr($result);
die();
$this->Poet->id = $result['Poet']['id'];
$this->set('result', $result);
}
Like this. Now I have Song.status as my association with Song table. I want to fetch only those records that has status = 1. Is it possible? Can I select only active records with my piece of code.
Use a normal find
While the magic findBy* methods are handy from time to time, it's a good idea to only use them for trivial queries - your query is nolonger trivial. Instead use a normal find call e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
)
),
'conditions' => array(
'slug' => $slug,
'Song.status' => 1 // <-
)
));
Does a Poet hasMany songs?
You don't mention your associations in the question, which is rather fundamental to providing an accurate answer, however it seems likely that a poet has many songs. With that in mind the first example will generate an sql error, as there will be no join between Poet and Song.
Containable does permit filtering associated data e.g.:
$result = $this->Poet->find('first', array(
'contain' => array(
'User' => array(
'id',
'full_name'
),
'Song' => array(
'id',
'name',
'name_hindi',
'slug',
'Song.status = 1' // <-
)
),
'conditions' => array(
'slug' => $slug
)
));
This will return the poet (whether they have relevant songs or not), and only the songs with a status of "1". You can achieve exactly the same thing by defining the condition in the association definition (either directly in the model or by using bindModel).
In croogo's search-View from the Nodes-controller the results include only nodes, where the searched term was found on the Node's model fields, but will skip any translated content for the nodes.
I'm trying to override this functionality and add support for translated content as well, but having a hard time to override the search-view without having to override the whole node-controller.
Does anyone has done this before and could give me some advice, as which approach could I take?
For anyone interested, I ended up doing the following:
Add a new SearchController on my Plugin with a show-view, which is an adapted version of the search-View on the NodesController.
Override the search/* route with my new view.
In order to search in the translated fields, I came with 2 possible approaches:
1) If using only the paginate for this, then there would be necessary to join the node and the i18n tables, first And then look for the string additionally on the joined fields.
2) Or, first find all distinct nodes with matching content on the i18n table through a query. And then use this list of nodes to add condition of "OR Node.id in (list_of_nodes)"
I ended up with alternative 2, and this is how the show-view looks like:
public function show() {
if (!isset($this->request->params['named']['q'])) {
$this->redirect('/');
}
App::uses('Sanitize', 'Utility');
$q = Sanitize::clean($this->request->params['named']['q']);
$results = $this->Node->query(
"SELECT DISTINCT(foreign_key) FROM `i18n` " .
"WHERE content LIKE '%" . $q . "%';");
$node_ids = array();
foreach($results as $res) {
$node_ids[] = $res['i18n']['foreign_key'];
}
$this->paginate['Node']['order'] = 'Node.created DESC';
$this->paginate['Node']['limit'] = Configure::read('Reading.nodes_per_page');
$this->paginate['Node']['conditions'] = array(
'Node.status' => 1,
'AND' => array(
array(
'OR' => array(
'Node.title LIKE' => '%' . $q . '%',
'Node.excerpt LIKE' => '%' . $q . '%',
'Node.body LIKE' => '%' . $q . '%',
'Node.id' => $node_ids,
),
),
array(
'OR' => array(
'Node.visibility_roles' => '',
'Node.visibility_roles LIKE' => '%"' . $this->Croogo->roleId . '"%',
),
),
),
);
// some more stuff ...
}
I have encountered a weird problem, where in the Controller is passing a single record from a table, but the View ends up displaying the entire table.
I have extensive logging and am pretty sure, that the Controller is passing a single record via the $this->set().
Controller (ContactsController : show_list)
$arr_contacts = $this->Contact->find(
'all',
array(
'conditions' => array(
'Contact.state_id' => $i_state_id,
'Contact.city'=> $str_city
),
'fields' => array(
'Contact.id',
'Contact.name',
'Contact.city'
),
'recursive' => -1
)
);
$contacts = $arr_contacts;
$this->log ($contacts, 'debug');
$this->set('contacts', $this->paginate());
$this->log(__FUNCTION__." : ".__LINE__, 'debug' );
Output in log:
Debug: Array
(
[0] => Array
(
[Contact] => Array
(
[id] => 504
[name] => Michael
[city] => New York
)
)
)
Debug: show_list : 303
In the view file (show_list.ctp), I have
<div class="contacts index">
<?php echo print_r($contacts);?>
The view file, prints a list of all records from the table. The SQL dump that cakephp displays, shows that additional SQL calls are being made. However, it isn't clear, where those calls are coming from.
The rest of the controllers and actions seem to be working fine, ruling out any corruption issues.
Has anyone encountered a similar situation before? Any pointers?
You pass the output of the paginate() function to your view, which is different from your own find() call.
If you want the same conditions as with your find(). pass them to paginate() (Tip: put your conditions in an array first)
If you do not need Paginate as it seems you don't since you are only getting a single line of entries from Db, then you should rewrite your $this->set() as such:
$this->set('contacts',$contacts);
If you need Paginate (???), then you need to set you entire function as such:
$this->paginate = array(
'conditions' => array(
'Contact.state_id' => $i_state_id,
'Contact.city'=> $str_city
),
'fields' => array(
'Contact.id',
'Contact.name',
'Contact.city'
),
'recursive' => -1
);
$this->log ($this->paginate, 'debug');
$this->set('contacts', $this->paginate());
$this->log(__FUNCTION__." : ".__LINE__, 'debug' );
I have been busy with the cakePHP framework for a couple of months now and I really love it. At the moment I'm working on a very new project and it does the job like it should (I think ...) but I feel uncomfortable with some code I wrote. In fact I should optimize my paginate conditions query so I get immediately the right results (right now I manipulate the result set by a bunch of Set::extract method calls.
I'll sketch the relevant aspects of the application. I have a model 'Site' who has a hasMany relationship with the model 'SiteMeta'. This last table looks as follow: id, site_id, key, value, created.
In this last model I record several values of the site at various periods. The name of the key I want to store (e.g. alexarank, google pagerank, ...), and off course also the value. At a given interval I let my app update this database so I can track evolution of this values.
Now my problem is this.
On the overview page of the various websites (controller => Sites, action => index) I'd like to show the CURRENT pagerank of the website. Thus I need one exact SiteMeta record where the 'created' field is the highest and the value in 'key' should be matching the word 'pagerank'. I've tried several things I read on the net but got none of them working (containable, bindmodel, etc.). Probably I'm doing something wrong.
Right now I get results like this when I do a $this->paginate
Array
(
[0] => Array
(
[Site] => Array
(
[id] => 1
[parent_id] => 0
[title] => test
[url] => http://www.test.com
[slug] => www_test_com
[keywords] => cpc,seo
[language_id] => 1
)
[SiteMeta] => Array
(
[0] => Array
(
[id] => 1
[site_id] => 1
[key] => pagerank
[value] => 5
[created] => 2010-08-03 00:00:00
)
[1] => Array
(
[id] => 2
[site_id] => 1
[key] => pagerank
[value] => 2
[created] => 2010-08-17 00:00:00
)
[2] => Array
(
[id] => 5
[site_id] => 1
[key] => alexa
[value] => 1900000
[created] => 2010-08-10 17:39:06
)
)
)
To get the pagerank I just loop through all the sites and manipulate this array I get. Next I filter the results with Set::extract. But this doens't feel quite right :)
$sitesToCheck = $this->paginate($this->_searchConditions($this->params));
foreach($sitesToCheck as $site) {
$pagerank = $this->_getPageRank($site['Site']);
$alexa = $this->_getAlexa($site['Site']);
$site['Site']['pagerank'] = $pagerank;
$sites[] = $site;
}
if (isset($this->params['named']['gpr']) && $this->params['named']['gpr']) {
$rank = explode('-', $this->params['named']['gpr']);
$min = $rank[0];$max = $rank[1];
$sites = Set::extract('/Site[pagerank<=' . $max . '][pagerank>=' . $min .']', $sites);
}
$this->set(compact('sites', 'direction'));
Could you guys please help me to think about a solution for this? Thanks in advance.
Thanks for the contributions. I tried these options (also something with bindmodel but not working also) but still can't get this to work like it should be. If I define this
$this->paginate = array(
'joins'=> array(
array(
'table'=>'site_metas',
'alias'=>'SiteMeta',
'type' =>'inner',
'conditions' =>array('Site.id = SiteMeta.site_id')
)
),
);
I get duplicate results
I have a site with 3 different SiteMeta records and a site with 2 different record.
The paginate method returns me 5 records in total. There's probably an easy solution for this, but I can't figure it out :)
Also I tried to write a sql query myself, but seems I can't use the pagination magic in that case. Query I'd like to imitate with pagination options and conditions is the following. The query returns exactly as I would like to get.
$sites = $this->Site->query('SELECT * FROM sites Site, site_metas SiteMeta WHERE SiteMeta.id = (select SiteMeta.id from site_metas SiteMeta WHERE Site.id = SiteMeta.site_id AND SiteMeta.key = \'pagerank\' order by created desc limit 0,1 )');
As you are trying to retrieve data in a hasMany relationship, cakephp doesn't join the tables by default. If you go for joins you can do something like:
$this->paginate = array(
'joins'=>array(
array(
'table'=>'accounts',
'alias'=>'Account',
'type' =>'inner',
'conditions' =>array('User.id = Account.user_id')
)
),
'conditions'=> array('OR' =>
array(
'Account.name'=>$this->params['named']['nickname'],
'User.id' => 5)
)
);
$users = $this->paginate();
$this->set('users',$users);
debug($users);
$this->render('/users/index');
You have to fit this according to your needs of course. More on joins, like already mentioned in another answer.
Edit 1: This is because you are missing the second 'conditions'. See my code snippet. The first 'conditions' just states where the join happens, whereas the second 'conditions' makes the actual selection.
Edit 2: Here some info on how to write conditions in order to select needed data. You may want to use the max function of your rdbms on column created in your refined condition.
Edit 3: Containable and joins should not be used together. Quoted from the manual: Using joins with Containable behavior could lead to some SQL errors (duplicate tables), so you need to use the joins method as an alternative for Containable if your main goal is to perform searches based on related data. Containable is best suited to restricting the amount of related data brought by a find statement. You have not tried my edit 2 yet, I think.
Edit 4: One possible solution could be to add a field last_updated to the table Sites. This field can then be used in the second conditions statement to compare with the SiteMeta.created value.
Try something like this:
$this->paginate = array(
'fields'=>array(
'Site.*',
'SiteMeta.*',
'MAX(SiteMeta.created) as last_date'
),
'group' => 'SiteMeta.key'
'conditions' => array(
'SiteMeta.key' => 'pagerank'
)
);
$data = $this->paginate('Site');
Or this:
$conditions = array(
'recursive' => 1,
'fields'=>array(
'Site.*',
'SiteMeta.*',
'MAX(SiteMeta.created) as last_date'
),
'group' => 'SiteMeta.key'
'conditions' => array(
'SiteMeta.key' => 'pagerank'
)
);
$data = $this->Site->find('all', $conditions);
If that does not work check this and this. I am 100% sure that it is possible to get the result you want with a single query.
Try something like this (with containable set up on your models):
$this->Site->recursive = -1;
$this->paginate = array(
'conditions' => array(
'Site.title' => 'title') //or whatever conditions you want... if any
'contain' => array(
'SiteMeta' => array(
'conditions' => array(
'SiteMeta.key' => 'pagerank'),
'limit' => 1,
'order' => 'SiteMeta.created DESC')));
I use containable so much that I actually have this in my app_model file so it applies to all models:
var $actsAs = array('Containable');
Many thinks to all who managed to help me through this :)
I got it fixed after all hehe.
Eventually this has been the trick for me
$this->paginate = array(
'joins'=> array(
array(
'table'=>'site_metas',
'alias'=>'SiteMeta',
'type' =>'inner',
'conditions' => array('Site.id = SiteMeta.site_id'))
),
'group' => 'Site.id',
'contain' => array(
'SiteMeta' => array(
'conditions' => array(
'SiteMeta.key' => 'pagerank'),
'limit' => 1,
'order' => SiteMeta.created DESC',
)));
$sites = $this->paginate();