Getting CakePHP's searchable behavior results to contain deeper associations - cakephp

I am trying to use CakePHP 1.3.5's searchable behavior with containable behavior to return search results for a specified model and an associated model (Article belongsTo User).
Ignoring the searchable behavior for a moment, the following call to find():
$this->Article->find('all', array(
'conditions' => array('Article.is_published' => 1),
'fields' => array('Article.id'),
'contain' => array('User.name')
));
Executes this SQL query:
SELECT `Article`.`id`, `User`.`name`, `User`.`id` FROM `articles` AS `Article` LEFT JOIN `users` AS `User` ON (`Article`.`user_id` = `User`.`id`) WHERE `Article`.`is_published` = 1
And returns the following array:
Array (
[0] => Array (
[Article] => Array (
[id] => 10
)
[User] => Array (
[name] => Author Name
[id] => 7
)
)
...
)
Which is exactly what's expected. However, the following call to search():
$this->Article->search($query, array(
'conditions' => array('Article.is_published' => 1),
'fields' => array('Article.id'),
'contain' => array('Article' => array('User.name'))
));
Executes this SQL query:
SELECT `Article`.`id` FROM `search_index` AS `SearchIndex` LEFT JOIN `articles` AS `Article` ON (`SearchIndex`.`model` = 'Article' AND `SearchIndex`.`association_key` = `Article`.`id`) WHERE `Article`.`is_published` = 1 AND MATCH(`SearchIndex`.`data`) AGAINST('search term' IN BOOLEAN MODE) AND `Article`.`id` IS NOT NULL
And returns this array:
Array (
[0] => Array (
[Article] => Array (
[id] => 9
)
)
...
)
Looking at the search() method, it is returning $this->SearchIndex->find('all', $findOptions);. $findOptions contains the following:
Array (
[conditions] => Array (
[Article.is_published] => 1
[0] => MATCH(SearchIndex.data) AGAINST('search term' IN BOOLEAN MODE)
)
[fields] => Array (
[0] => Article.id
)
[contain] => Array (
[Article] => Array (
[0] => User.name
)
)
)
The association isn't getting lost along the way, because inside SearchableBehavior, $this->SearchIndex->Article->belongsTo['User'] is present and intact immediately before and after the call to find() inside the search() method.
The call to search() returns the exact same thing for all of the following values of 'contain':
array('Article' => array('User.name'))
array('Article' => array('User'))
array('Article' => array('User' => array()))
array('Article' => array('User' => array('fields' => array('User.name'))))
array('Article' => array('User' => array('fields' => array('name'))))
Am I doing something wrong? I think I'm using the same format as is instructed in the CakePHP documentation, and I haven't found anything online that suggests that you have to do something special to get search results with associated data.
I know that I could easily achieve the result that I want by just looking up the Users with additional calls to find(), but I'd like to get containable behavior to work like it's supposed to and cut down on unnecessary extra database queries.

When using containable, set the recursive option to "true"
$this->Model->Behaviors->attach("Containable",array("recursive"=>true));

Related

CakePHP - two models on one page, can't get this->request->data automagic field population

I have two controllers: events and results. Events hasMany results, results belongsTo Events. I can save just fine, but when I go to edit, I can only get the information for the Event part of the form, to come in automagically.
I build the Results form info like this:
$option_number = 5;
for ($i = 0; $i < $option_number; $i++) {
echo $this->Form->select("Result.{$i}.object_id", $qual_options, array('empty' => false, 'class' => 'result-name'));
echo $this->Form->hidden("Result.{$i}.id");
echo $this->Form->hidden("Result.{$i}.type", array('value' => 'qual'));
echo $this->Form->hidden("Result.{$i}.action", array('value' => 'add')); ?>
}
In the backend, when I'm doing this to get the automagic population:
if ($this->request->is('get')) {
$this->request->data = $this->Event->findById($id);
}
It works just fine, but I can't figure out how to get it to show the Results. I've tried many things, most probable being:
$this->request->data['Result'] = $this->Result->findAllByEventId($id);
With that, I end up with a data structure like:
[Result] => Array
(
[0] => Array
(
[Result] => Array
(
[id] => 1
[object_id] => 1
[type] => qual
[action] => add
[amt] => 10
[event_id] => 1
)
)
[1] => Array
(
[Result] => Array
(
[id] => 2
[object_id] => 2
[type] => qual
[action] => add
[amt] => 1
[event_id] => 1
)
)
... etc.
)
)
Which definitely looks fishy, I just can't seem to manipulate it to work.
UPDATE I should have mentioned this; this is what my data looks like when I SAVE it, and I want to mimic this!
[Result] => Array
(
[0] => Array
(
[object_id] => 1
[type] => qual
[action] => add
[amt] => 0
[event_id] => 3
)
[1] => Array
(
[object_id] => 1
[type] => qual
[action] => add
[amt] => 1
[event_id] => 3
)
You can see that each numeric key after just has the information in it; instead, my numeric keys ALSO have an array INSIDE them name Result, and I have no idea how to make that go away properly! :} I could always loop through and build it in the format CakePHP wants, but I want to do it properly. And that single line above is what needs changing, but I have run out of ideas.
What about just using find('first') against the event? Since it's hasMany, it will return the Result model in a single [Result] key with many numeric keys.
$this->request->data = $this->Event->find('first', array(
'conditions' => array(
'Event.id' => $id
),
'contain' => array(
'Result'
)
));
This will return something like:
array(
'Event' => array(
'id' => 1,
'name' => 'event name'
),
'Result' => array(
0 => array(
'id' => 1,
...
),
1 => array(
'id' => 2,
...
)
)
);
You could unset the Event key if you needed to.

Cakephp contain and belongsTo hasMany conditions

From the CakePHP manual:
Let's say you had a belongsTo relationship between Posts and Authors. Let's say you wanted to find all the posts that contained a certain keyword ("magic") or were created in the past two weeks, but you want to restrict your search to posts written by Bob:
array (
"Author.name" => "Bob",
"OR" => array (
"Post.title LIKE" => "%magic%",
"Post.created >" => date('Y-m-d', strtotime("-2 weeks"))
)
)
However, its not working for me. I've got Scrapes belongsTo Vargroup, and Vargroup hasMany Scrape. My scrapes table has a field vargroup_id
When I do:
$this->Vargroup->contain('Scrape');
$this->Vargroup->find('first');
I get:
Array
(
[Vargroup] => Array
(
[id] => 16950
[item_id] => 1056
[image] => cn4535d266.jpg
[price] => 22.95
[instock] => 1
[timestamp] => 2012-10-29 11:35:10
)
[Scrape] => Array
(
[0] => Array
(
[id] => 18163
[item_id] => 1056
[vargroup_id] => 16950
[timestamp] => 2012-05-23 15:24:31
[instock] => 1
[price] => 22.95
)
)
)
But when I do :
$this->Vargroup->contain('Scrape');
$this->Vargroup->find('first', array(
'conditions' => array(
'not' => array(
array('Scrape.timestamp >' => $today)
)
)
));
Im getting The following error with associated sql output
Warning (512): SQL Error: 1054: Unknown column 'Scrape.timestamp' in 'where clause'
Query: SELECT `Vargroup`.`id`, `Vargroup`.`item_id`, `Vargroup`.`image`, `Vargroup`.`price`, `Vargroup`.`instock`, `Vargroup`.`timestamp` FROM `vargroups` AS `Vargroup` WHERE `Scrape`.`timestamp` = '2012-10-29
It doesn't look like its binding the table at all..
Any help would be appreciated.
Thanks
Try with this:
$this->Vargroup->contain('Scrape');
$this->Vargroup->find('first', array(
'conditions' => array(
'not' => array(
'Scrape.timestamp >' => array($today)
)
)
));
i think this examples from cakephp page tell the right way to do a "not" condition,
check it out.
array(
"NOT" => array("Post.title" => array("First post", "Second post", "Third post"))
)
this is the complex find conditions page if you have more questions. Cakephp complex find conditions

CakePHP: saving associated data

I'm trying to save IssueHistoryDescription, which belongsTo IssueHistory.
So IssueHistory hasMany IssueHistoryDescription. This has all been set in the model.
Yet, when I save this in IssueHistory, using $IssueHistory->save($data);
(With or without a $IssueHistory->create(); before...)
Array
(
[IssueHistory] => Array
(
[id] => 22
)
[IssueHistoryDescription] => Array
(
[old_description] => OLD
[description] => NEW
)
)
It doesn't work, nothing is saved.
When I try to use saveAssociated() I get an error:
Fatal error: Cannot use string offset as an array in /var/www/xdev/kipdomanager/cakephp/lib/Cake/Model/Model.php on line 2248
You can try this:
$data = array(
'IssueHistory' => array('id' => 2),
'IssueHistoryDescription' => array(
array('old_description' => 'OLD', 'description' => 'new')
)
);
$IssueHistory->create();
$IssueHistory->saveAll( $data );

Fetch all Items in each Category where Item.name = bla: how?

Imagine I have 2 models: Item and Category. Category hasMany Items and Item belongsTo Category.
I would like to obtain an array "reversed" on how cakephp normally return it: Usually I get something like
[Item]
...some props...
[Category]
...some props...
While I want something like:
Array
[0][Category]
Array
[0][Item]
...some props...
[1][Item]
[1][Category]
...and so on
How can I obtain this result with find method if possible? Otherwise, how to obtain it?
My current model is much more complex but I think I could apply this teory to all my models (I have a Category => Group => Item relationship).
Thanks for suggestions and answers.
According to the title of your question, if the point is to get only Items that are linked to categories named 'bla', you can just call the find() method on the Category model instead of the Item model.
Instead of
$this->Item->find('all', array('conditions' => array('Category.name' => 'bla')));
do
$this->Item->Category->find('all', array('conditions' => array('Category.name' => 'bla')));
This way it will first filter the corresponding Categories and then get the linked Items. So you will obtain something like
Array
(
[0] => Array
(
[Category] => Array
(
[id] => 1
[name] => bla
...
)
[Item] => Array
(
[0] => Array
(
[id] => 1
...
)
[1] => Array
(
[id] => 2
...
)
)
)
)
EDIT
With three models linked together, it is a bit more complicated. The only solution I can think of to prevent loading a lot of data is to build a query with INNER JOINs:
$categories = $this->Category->find('all', array('fields' => array('Category.id', 'Category.name', 'Item.id', 'Item.name'),
'joins' => array(
array( 'table' => 'groups',
'alias' => 'Group',
'type' => 'inner',
'conditions' => 'Group.category_id = Category.id'
),
array( 'table' => 'items',
'alias' => 'Item',
'type' => 'inner',
'conditions' => array('Item.group_id = Group.id', 'Item.name' => 'bla')
))));
It will return an array like this:
Array
(
[0] => Array
(
[Category] => Array
(
[id] => 2
[name] => Shop
)
[Item] => Array
(
[id] => 1
[name] => Bla
)
)
)
EDIT2
A last word about the structure of the array you'll get: it is not exactly like you wanted. The Item array is at the same level that the Category array. So you may end up with many pairs of Category-Item for the same Category.
But a simple foreach loop would then allow you to build an array with the exact struture you wish and this query has the advantage to get only what is neccessary from the database.

CakePHP: include fieldname alias under related model

I am willing to group MySQL results by a column, and also count the records which belong to the same category:
$this->find('all', array(
'fields' => array('UserCheckin.id', 'UserCheckin.capital_id', 'COUNT(UserCheckin.id) as capital_checkins'),
'group' => 'UserCheckin.capital_id'
));
The problem is that capital_checkins is grouped as a separate key in the returned array, whilst I would like it to be under the UserCheckin model. Here's how the results are returned:
Array
(
[0] => Array
(
[UserCheckin] => Array
(
[id] => 3
[capital_id] => 10
)
[0] => Array
(
[capital_checkins] => 2
)
[Capital] => Array
(
[id] => 10
[name] => London
)
)
[1] => Array
(
...
)
)
Is there a way to include the alias under UserCheckin model?
In cakephp 1.3 you can use virtual field:
in your UserCheckin model:
var $virtualFields = array(
'capital_checkins' => 'COUNT(UserCheckin.id)'
);
IMPORTANT UPDATE: As #Rob Wilkerson correctly notices, adding a virtual field with aggregate function permanently to your model will [probably] mess up your find queries which do not have a GROUP BY (implicitly group all rows in MySQL or just fail with SQL error in SQL Server). Solution is to add a virtual field temporarily at a runtime before query that uses it:
$this->virtualFields['capital_checkins'] = 'COUNT(UserCheckin.id)';
$result = $this->find('all', array(
'fields' => array('UserCheckin.id', 'UserCheckin.capital_id', 'UserCheckin.capital_checkins'),
'group' => 'UserCheckin.capital_id'
));
//reset virtual field so it won't mess up subsequent finds
unset($this->virtualFields['capital_checkins']);
return $result;
'COUNT(UserCheckin.id) as UserCheckin__capital_checkins'
http://book.cakephp.org/2.0/en/models/virtual-fields.html#virtual-fields-in-sql-queries

Resources