CActiveDataProvider - fill up with model related items

Hi,

I’d like to fill CActiveDataProvider with model’s related items.

Lets say I have an "Order" model, which has many "Item" models related (HAS_MANY).

I’d like to do something like new CActiveDataProvider($model->items).

Using CArrayDataProvider is not quite the same, as using model attributeLabels for grid headers is much more convenient.

Currently I’m setting up CDbCriteria manually, however I’d like to use the relation definition (following DRY ;) )

Maybe you missed this wiki? Searching and sorting by related model in CGridView

This seems to be completely unrelated ;) The article is about searching and sorting by field from related model (the relation is 1:1), while what I’m looking for is to to have CActiveDataProvider filled with model’s HAS_MANY related data

Refering to the example of Author having MANY Posts:




class Author extends CActiveRecord {

 ...

    function relations() {

        return array(

            'posts'=>array( self::HAS_MANY, 'Post', 'author_id' ),

        );

}



Having single Author model instance ( $author = Author::model()->findByPk($id); ) I’d like to obtain CActiveDataProvider filled with $author->posts

Doing this manually looks like this:




$author = Author::model()->findByPk($id);

$cdb = new CDbCriteria();

$cdb->compare('author_id', $author->id);

return new CActiveDataProvider($cdb);



however this sucks a bit, as I need to duplicate both the code for each case I’m using it, and the relation definition.

I was wondering if that is possible using any Yii tricks, at least to obtain CDbCriteria which is used for finding related models ?

Hi,

I don’t quite understand what you mean by “fill CActiveDataProvider with related models”.

If you just want to retrieve "all" posts that belong to an author, then there should be no need to do something tricky. "$author->posts" will do the job. All that you want is just a CActiveDataProvider for authors.

The last line should be either




return new CActiveDataProvider('Post', $cdb);



Or




return new CActiveDataProvider('Author', $cdb);



Which do you want?

And what use case do you have in mind?

Not really. $author->posts returns array of retrieved Posts models, assigned to the Author.

For some cases, like using CGridView, CActiveDataProvider is required.

You’re right, my bad. It should be




return new CActiveDataProvider('Post', $cdb);



I see.

Then what about using ‘author’ relation of Post?




class Post extends CActiveRecord {

 ...

    function relations() {

        return array(

            'author'=>array( self::BELONGS_TO, 'Author', 'author_id' ),

        );

}



You can get a provider for Post and filter the result by author name for example.

Doesn’t it fit your needs?

[EDIT]

Well, now I think I got it.

Yours is not a question but rather a request about CActiveDataProvider or CArrayDataProvider.

CArrayDataProvider can receive array of model objects, for example $model->posts as rawData. In that case it should be able to tell what model it is dealing with. And CGridView should be able to replace the header labels according to the model.

well, I’m not quite sure if that is a feature request or is there some feature I don’t know of ;)

there are several drawbacks of using CArrayDataProvider with related models array:

  1. it does not allow for sorting, filtering etc

  2. it doesn’t use model’s attributeLabels

  3. all the related models are obtained at once, they’re paginated just before rendering. for huge sets of data it is unnaceptable.

Currently my ActiveRecord class has the following method




    /**

     * Return the CActiveDataProvider of related models, 

     * according to relations() definition

     * @returns CActiveDataProvider

     * @author Michał "migajek" Gajek

     */

    public function getRelatedProvider($relation_name)

    {

        $rels = $this->relations();

        if (!is_array($rels) || !isset($rels[$relation_name]))

            throw new CException(get_class($this). " has no relation named \"{$relation_name}\" defined!");

        

        $relation = $rels[$relation_name];

        if ($relation[0] !== self::HAS_MANY)

            throw new CException("getRelatedProvider works with HAS_MANY relations only");

            

        $cdb = new CDbCriteria();

        $cdb->compare($relation[2], $this->primaryKey);

        return new CActiveDataProvider($relation[1], array(

            'criteria' => $cdb,

        ));

    }



which is used as following:




<?php $this->widget('bootstrap.widgets.BootGridView', array(

    'dataProvider'=> $model->getRelatedProvider('posts'),

)); ?>



This method should be extended to handle all the relation parameters, currently it just takes the model name and foreign key.

I was wondering if there’s any ready to use method in Yii core.

If not, I believe there should be one ;)

Um, interesting, but …

I’d rather keep things simple as they are.

For example, when Author HAS_MANY Posts, I would write a view page for Author like this:




// controller

public function actionView($id)

{

	$author = $this->loadModel($id);


	$post = new Post('search');

	$post->unsetAttributes();

	$post->author_id = $author->id;

	if (isset($_GET['Post']))

	{

		$model->attributes = $_GET['Post'];

	}


	$this->render('view',array(

		'author' => $author,

		'post' => $post,

	));

}

...

// view

$this->widget('zii.widgets.CDetailView', array(

	'data'=>$author,

	...

));

$this->widget('zii.widgets.grid.CGridView', array(

	'dataProvider' => $post->search(),

	'filter' => $post,

	...

));



It’s just an Author’s view page combined with Post’s admin page.

There’s nothing fantastic in it. It’s boring simple and it works.

I’m afraid that with your getRelatedProvider() we would not be able to filter the output by any attribute other than the FK to the parent model. For example, how do you filter the posts by1) that belong to the author and 2) that has a ‘post_date’ that is later than yesterday? 1) is OK but 2) is not. A provider that getRelatedProvider() returns can’t have a flexible filter.

that would be so much against DRY principle ;) Following that way, one can easily say that using relations doesn’t make sense at all.

In the application I’m building I’m using grids and detailviews extensively, and repeating so much code would really be boring as you mentioned ;)

You are right. I haven’t been using it, so didn’t implemented, but that could be easily extended, with a simple modification:




public function getRelatedProvider($relation_name, $criteria = array())

...

$cdb->compare($relation[2], $this->primaryKey);

$cdb->mergeWith($criteria);

...



how about that? ;)

But how do you construct the additional criteria outside of the function?

You have to create some model instance as the holder of the search parameters if you want some interaction with the user. And you also need some function to construct the criteria based on the user input.

Gii-generated admin action uses a CActiveRecord model instance (Post model instance in this example) for the search parameters. It is used in ‘Advanced Search Form’ and set as the ‘filter’ attribute of the grid. And gii has also generated “search()” method to construct the criteria from the user input.

I thought you were talking about defining criteria in view code ;)

right, my solution is not capable of filtering related data by user-provided filters, but neither is ActiveRecorc relations capable of that.

It’d be easy to modify the method I provided to create instance of model (Post) by its name defined in relation, than use search() method to return CActiveDataProvider, but obtaining data directly from $_POST in model code would be against MVC pattern.

Passing $_POST data to the method should be done in controller, but all I wanted was not to modify the controller ;)

What I proposed and implemented is just a simple way to display model’s related record in CGridView ;)

I didn’t said that is complete solution for obtaining ActiveDataProvider with full filtering capabilities etc ;)

Yeah, I think I’ve got your points in discussion. And hopefully you got mine, too. :)

hi guys!

so what is final conclusion? is there a way, or is there no way?

I think I need the same thing, but so far I don’t think I have found the right solution.

Helpful thread, thanks!

I ended up with a hybrid of the above approaches:




<?php


class ActiveRecord extends CActiveRecord

{


	/**

	 * Returns a model used to populate a filterable, searchable

	 * and sortable CGridView with the records found by a model relation.

	 *

	 * Usage:

	 * $relatedSearchModel = $model->getRelatedSearchModel('relationName');

	 *

	 * Then, when invoking CGridView:

	 * 	...

	 * 		'dataProvider' => $relatedSearchModel->search(),

	 * 		'filter' => $relatedSearchModel,

	 * 	...

	 * @returns CActiveRecord

	 */

	public function getRelatedSearchModel($name)

	{


		$md = $this->getMetaData();

		if (!isset($md->relations[$name]))

			throw new CDbException(Yii::t('yii', '{class} does not have relation "{name}".', array('{class}' => get_class($this), '{name}' => $name)));


		$relation = $md->relations[$name];

		if (!($relation instanceof CHasManyRelation))

			throw new CException("Currently works with HAS_MANY relations only");


		$className = $relation->className;

		$related = new $className('search');

		$related->unsetAttributes();

		$related->{$relation->foreignKey} = $this->primaryKey;

		if (isset($_GET[$className]))

		{

			$related->attributes = $_GET[$className];

		}

		return $related;

	}


}