CSqlDataProvider, CGridView, CComponent.evaluateExpression()

I have just started to use the new CSqlDataProvider class to do a more complicated query to prepare data for a CGridView. There is an issue with doing it this way. The normal way you code for a CGridView is to say something like the following:




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

	'dataProvider'=>$dataProvider,

	'columns'=>array(

		array(

			'name'=>'Field',

			'value'=>'$data->field',

		),



Typically, when using the CActiveDataProvider, the $data variable comes through as an Object. But when you use CSqlDataProvider, the $data variable is simply an Array.

What are we supposed to do here? Should we change the way our CGridView operates to accommodate whatever dataProvider type we are using? Or should there be some mechanism to always treat things the same way in the View?

My coworker came up with a solution in the Yii Framework/base/CComponent.php class.

The function evaluateExpression($expression,$data=array())




//CComponent.php ~ line 611

public function evaluateExpression($_expression_,$_data_=array())

{

 if(is_string($_expression_))

 {

  extract($_data_);

  return eval('return '.$_expression_.';');

 }

 else

 {

  $_data_[]=$this;

  return call_user_func_array($_expression_, $_data_);

 }

}



If you check to see what the expression is expecting, you can typecast $data to Object. See our change below:




//CComponent.php ~ line 611

public function evaluateExpression($_expression_,$_data_=array())

{

 if(is_string($_expression_))

 {

  extract($_data_);


  /*

   HERE'S OUR ADD:

   THIS WILL CAST TO OBJECT IF THE EXPRESSION SHOWS WE INTEND TO TREAT IT AS SUCH

  */

  if(strpos($_expression_,'->')){

   $data = (object) $data;

  }


  return eval('return '.$_expression_.';');

 }

 else

 {

  $_data_[]=$this;

  return call_user_func_array($_expression_, $_data_);

 }

}



Now, we can make sure $data is always an Object this way. Or we can change the way we do our CGridView such that the expression is:

‘value’=>’$data[“field”]’ instead of ‘value’=>’$data->field’

But I don’t think it makes sense to have to change CGridView this way. I think it should have one standard way of behaving so that no matter what dataProvider we throw at it, the code should work.

What do you guys think about this?

-Jaz

Hey, I’d really like to get some feedback on this, so I’m bumping it up.

Here’s the scenario. If you have a CGridView that is fed by a CActiveDataProvider, when specifying the columns, you may use ‘value’=>’$data->field_name’. If you keep your view code the same, but change the CActiveDataProvider to a CSqlDataProvider, your view will be broken. You will get an error “Trying to get property of non-object”. You would have to change it to ‘value’=>’$data[“field_name”]’ for it to work.

Is this a bug in Yii? Or are we supposed to change our CGridView column code to match whatever type of dataProvider is going into it?

The solution I mention above is a fix to Yii that checks to see whether or not the GridView is expecting an object or an array. I’m not sure what else uses the function in CComponent that I modified above, so I’m hesitant to say this is a good solution.

i had a chat with some other who has the same problem and I found a simple workaround for this issue. creating an object of $data if it’s only an array.

if you add following code (marked with +) to the top of your renderDataCellContent function of CDataColumn you can have the same value code in your gridview with CSqlDataProvider.




protected function renderDataCellContent($row,$data)

	{


+        if(!is_object($data))

+           $data = (object) $data;


		if($this->value!==null)

			$value=$this->evaluateExpression($this->value,array('data'=>$data,'row'=>$row));

		else if($this->name!==null)

			$value=CHtml::value($data,$this->name);

		echo $value===null ? $this->grid->nullDisplay : $this->grid->getFormatter()->format($value,$this->type);

	}



So you can work with the data array like it is an object instead of an array. This is btw just a quick and dirty hack, don’t know if this could cause some trouble elsewhere.

Some sample code for the problem:





$count=Yii::app()->db->createCommand('SELECT COUNT(*) FROM user')->queryScalar();

$sql='SELECT * FROM user';

$dataProvider=new CSqlDataProvider($sql, array(

    'totalItemCount'=>$count,

    'sort'=>array(

        'attributes'=>array(

             'id', 'username', 'email',

        ),

    ),

    'pagination'=>array(

        'pageSize'=>10,

    ),

));

// $dataProvider->getData() will return a list of arrays.


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

        'dataProvider'=>$dataProvider,

        'columns'=>array(

                array(

                    'name'=>'id',

                    'value'=>'$data->username." ".$data->id',  // doesn't display anything without changes

                ),

                array(

                     'name'=>'username',

                     'value'=>'$data->username',  // doesn't display anything

                ),

                'email', // will be displayed

                array(

                     'name'=>'email',

                     'type'=>'raw',

                     'value'=>'CHtml::mailto($data->username,$data->email);',  // doesn't display anything without changes

                ),


        )));




you would also have the same problem when you use an array as an dataprovider e.g.




$data = array(

    array('id'=>1,'username'=>'username1','email'=>'email1@test.com'),

    array('id'=>2,'username'=>'username2','email'=>'email2@test.com'),

    array('id'=>3,'username'=>'username3','email'=>'email3@test.com'),

    array('id'=>4,'username'=>'username4','email'=>'email4@test.com'),

    );

 $dataProvider = new CArrayDataProvider($data);



And maybe there is some better solution for this.

Ey! Thanks! Your post help me a lot! (:

Thanks so much for your help Jaz, was having the same issue with CArrayDataProvider and your trick resolved my issue.