I have a data provider which generates content like:
array(
[0] => ParentObjects array(
Child1Object array(...),
Child2Object array(...),
)
)
The first level of this array will always be 1.
I want to show in a CGridView the content of both childs.
$dataProvider=new CActiveDataProvider('Campanii', array(
'criteria'=>array(
'with'=>array('stocs','produse'),
),
'pagination'=>array(
'pageSize'=>20,
),
));
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider'=>$dataProvider,
'columns'=>array(
array('name'=>'Cod','value'=>'$data->stocs[0]->cod_produs'),
),
));
As you see, the column cod has the value array('name'=>'Cod','value'=>'$data->stocs[1]->cod_produs'). Notice that [0]. Instead of that zero, I want something like an iterator. In this case, it shows me the content of that attributes in a column.
Is this possible?
Yes, you can write any complex expression you want. However, for readability it is better to use an actual function. This example assumes PHP 5.3+, if your environment doesn't support that, just create a normal function instead of an anonymous one (or find a better environment).
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider' => $dataProvider,
'columns' => array(
array(
'name' => 'Cod',
// needed when returning HTML instead of plain text
'type' => 'raw',
// this function is called each time a cell needs to be rendered
'value' => function(Companii $data) {
$codes = array();
foreach($data->stocs as $stoc) {
$code = CHtml::encode($stoc->cod_produs);
$codes []= CHtml::tag('li', array(), $code);
}
$codes = implode("\n", $codes);
return CHtml::tag('ul', array(), $codes);
},
),
),
));
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).
I want to limit the fields returned by a deeper association using containable.
My associations:
Game hasMany Review
The paginate and containable code:
$this->paginate = array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Game' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
),
)
);
$games = $this->paginate('Game');
Currently, all of the fields in the Review table are returned. 'ROUND(AVG(Review.score),1)' is never returned. How can I specify what fields I want returned from the Review association?
SQL dumps for two search results using #theJetzah's answer. The first is a search with one game as a result and the second is a search returning three games.
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` = (55)
SELECT `Review`.`id`, `Review`.`review_text`, `Review`.`score`, `Review`.`user_id`, `Review`.`game_id`, `Review`.`created`, `Review`.`platform_id`, (ROUND(AVG(`Review`.`score`),1)) AS `Review__average_score` FROM `videogamedb`.`reviews` AS `Review` WHERE `Review`.`game_id` IN (55, 56, 57)
Not a full answer, but an attempt to get it working :)
Approach1 (UPDATE: Containable doesn't support 'group by')
First of all, try to add the 'Game' model to the $uses array of your Controller, if it is not included yet, and re-organise the pagination array (as previously suggested by Sam), so that you'll be pagination the Game model itself.
Then, It may help to create a virtual field for the calculated score, but the results of 'Review' need to be grouped, otherwise you'll not be able to calculate the average score.
I'm not able to test this, but it may worth trying
something like this;
public $uses = array(
'Game',
// other models
);
public function myfunction()
{
$this->Game->Review->virtualFields['average_score'] = 'ROUND(AVG(Review.score),1)';
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array(
'Review.game_id,
'Review.average_score',
),
'group' => array(
'Review.game_id,
),
)
)
)
);
// Conditions can be passed to paginate,
// that way you can specify 'paginate' at
// one place and don't have to modify it
// to include the conditions
$games = $this->paginate('Game', $conditions);
}
Alternative approach: Using joins and a database-view
Apparently, the Containable behavior doesn't like group-by clauses; See this ticket for more information: Containable behavior does not implement 'group' option
CakePHP allows you to manually specify a join: Joining Tables
To simplify things and to prevent having to add a 'group by' for all fields, create a simple database-view in your database;
CREATE VIEW review_scores AS
SELECT
game_id,
ROUND(AVG(score),1) AS average_score,
COUNT(id) AS total_reviews
FROM
reviews
GROUP BY
game_id;
If you're unfamiliar with this; a database 'view' is basically a 'stored query', which can be accessed as if it was a regular table. See Create View
Then, use a 'manual' join, using the newly created database-view as the source-table. In your case, this will look something like this;
$this->paginate = array(
'Game' => array(
'fields' => array(
'Game.id',
'Game.name',
'Publisher.id',
'Publisher.name',
'ReviewScore.average_score',
'ReviewScore.total_reviews',
),
'joins' => array(
array(
'table' => 'review_scores',
'alias' => 'ReviewScore',
'type' => 'LEFT',
'conditions' => array(
'ReviewScores.game_id = Game.id',
)
)
)
)
);
Hope this helps
I think your array is a configured a little wrong, try:
$this->paginate = array(
'Game' => array(
'conditions' => $conditions,
'fields' => array(
'Game.id', 'Game.name',
'Publisher.id', 'Publisher.name'
),
'contain' => array(
'Review' => array(
'fields' => array('Review.id', 'ROUND(AVG(Review.score),1)')
)
)
)
);
$games = $this->paginate('Game');
As an aside, from personal experience, specifying the fields in a query doesn't always speed it up (certainly for small number of fields), assuming this is the motive for doing so. It does reduce memory occupancy but this is only relative to original size of the record and the number of records returned.
In my app I have a Burger model, a Joint model, and a Location model.
Joints have many Burgers, and have many Locations.
What I want to do is filter Burgers two ways: based on a rating, and also bases on whether they belong to a Joint that has a Location that matches a condition, in this case, a city.
Here's what I've tried, but she no workey:
$burgers = $this->Burger->find('all', array(
'conditions' => array('Burger.official_rating !=' => 0),
'contain' => array(
'Joint',
'Joint.Location.conditions' => array(
'Location.city' => 'Toronto'
)
),
'order' => array('Burger.official_rating DESC'),
'limit' => 10
));
Joint.Location.conditions seems to have no effect.
Edit: now using joins, but am unsure if this is the most efficient way (notice, I had to add a Contain clause, to make sure the associated data was returned in the results).
$options['joins'] = array(
array('table' => 'joints',
'alias' => 'Joint1',
'type' => 'inner',
'conditions' => array(
'Joint1.id = Burger.joint_id'
)
),
array('table' => 'locations',
'alias' => 'Location2',
'type' => 'inner',
'conditions' => array(
'Location2.joint_id = Burger.joint_id'
)
)
);
$options['conditions'] = array(
'Location2.city' => 'Toronto',
'Burger.official_rating !=' => 0
);
$options['order'] = array(
'Burger.official_rating DESC',
);
$options['limit'] = 10;
$options['contain'] = array(
'Joint','Joint.Location'
);
$burgers = $this->Burger->find('all', $options);
Problem:
This is a very common issue people have, but is easily remedied as soon as you understand that you cannot limit the main find model based on conditions against it's contained items.
The reason you can't is because doing a contain actually creates separate queries on the tables, as opposed to JOIN, which creates a single query.
Solution:
Your best bet in this case is to use CakePHP JOINs.
In some cases (not yours), the alternate to using JOINs is to change which model you're doing the query on. If you only need one condition, then you could change the find to the model with the condition instead of the one within the contain and contain in the reverse order.
$swimmer = $this->Swimmer->find('list', array(
'conditions' => array('Swimmer.group' => $this->data['Swimmer']['group_id']),
'order' => array('Swimmer.first_name ASC'),
'fields' => 'Swimmer.first_name'
));
First of all, setup a virtual field in your Swimmer model as suggested by Ann Pham. eg:
var $virtualFields = array(
'name' => "CONCAT(Swimmer.first_name, ' ', Swimmer.last_name)"
);
Then, fetch the data for your dropdown list like so: (assuming Swimmers controller)
$this->Swimmer->find('list', array('fields' => array('Swimmer.id', 'Swimmer.name')));
You could also try doing this in your SwimmerModel: var $displayName = 'Swimmer.name'; (not 100% sure if this would work). If it does work, you won't need the 'fields' array in the find.
An another approach is to concat the field on the fly in the fields list.
Ex:
$swimmer = $this->Swimmer->find('list',
array(
'conditions' => array(
'Swimmer.group' => $this->data['Swimmer']['group_id']),
'order' => array(
'Swimmer.first_name ASC'
),
'fields' => 'CONCAT(Swimmer.first_name, " ", Swimmer.first_name) AS full_name'
));
http://book.cakephp.org/view/1588/virtualFields