Parameterized Named Scope. Re-use the same set of filtering criteria in various models and controllers. Also re-use the same Parameterized Named Scope in all find() functions and in dataprovider.

You are viewing revision #21 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#22) »

Hi guys

First the problem:

I have a field in my records called recordstatus. I use it as follows:

recordstatus = 0: Fully restricted. User may not CRUD. (Used for system records.)

recordstatus = 1: Semi restricted. User may read and use it's id as FK. User may not update/delete. Used for default system records that the user may use - such as 'roles' and 'tax percentages'.

recordstatus = 2: Normal user record. User may CRUD.

If I only want to display records with recordstatus >= 1 in my CGridview, then I can do this:

In the controller:

$model=new myTable('search');
$model->unsetAttributes();
$model->recordstatus = 1;

In the model's search function:

public function search()
{
	$criteria=new CDbCriteria;
	
	If ($this->recordstatus == null){
		// recordstatus was not set in the controller
		$criteria->condition = 't.recordstatus = 100');  /* this will return 0 records, because 100 is not a valid recordstatus. */
	}
	else{
		$criteria->condition = 't.recordstatus >= $this->recordstatus';  /* note that operator is '>='. Return records with recordstatus >= 1 */
	}

	/* put your normal other filtering criteria here */

	return new CActiveDataProvider($this, array(
		'criteria'=>$criteria,
	));
}

But the problem with the above approach is that these filtering criteria are locked inside your search() function. So you can't use them in a direct find() statement like this:

$posts= myTable::model()->findAll();

What you can do is to repeat these filtering criteria in your controller and then incorporate them in a statement like this:

$posts= myTable::model()->findAll($criteria);

But that could result in having duplicate filtering criteria all over your code.

So how do you have only ONE set of filtering criteria that you can use in both the model's search() function - i.e. dataprovider generation - as well as in your controller (all find() functions)?

Now for the solution:

To the rescue came parameterized named scopes:

Here is an example of a parameterized named scope (pns) with 2 parameters (obviously you can use more if needed). The first parameter is used to receive an operator - which can be any of these: =, >=, >, <, <=. The second parameter is used to receive the filtering value.

Now for the important bit: This pns works with two methods.

Method-1: You can pass the parameter values from the controller directly to the pns, via any find() function:

$posts= myTable::model()->pns_recordstatus('>=', 1)->findAll();

Method-2: You can store the parameter values in the empty model (let's call it the filtering-model) that you create in the controller to:

  1. store the filtering criteria that the user might have entered in the gridview; and

  2. store your own filtering criteria that you want to incorporate into the gridview.

In the model's search() function, the DataProvider will invoke the pns, which will incorporate the filtering-model's parameters, so that it will reflect in the CGridView's rows.

In the controller you have the following:

$model=new myTable('search');
$model->unsetAttributes();
$model->var_operator = '>=';
$model->var_recordstatus = 1;

Two separate properties (var_operator and var_recordstatus) are created in the model to store the operator and filtering value.

Why create $model->var_recordstatus if the model already has $model->recordstatus (obtained from the record)?

Because you use $model->var_recordstatus in the pns to only include records with recordstatus = 1 or 2. And you then use $model->recordstatus to store the filtering criteria that the user might enter in the gridview to FURTHER filter the records. If you use $model->recordstatus for both purposes, then the user's filtering criteria will be constantly overwritten by your own value.

(Obviously, if the recordstatus column is not displayed in the gridview, or if it should not be filtered any further by the user, then you only need to use $model->recordstatus.)

The model:

public $var_operator;
public $var_recordstatus;

public function search()
{
	$criteria=new CDbCriteria;
	
	/* Put your other normal filtering criteria here */

	/* The dataprovider invokes the pns. Here you don't pass any parameters to
	the pns, because Method-2 uses the parameters stored in the filtering-model
	*/
	return new CActiveDataProvider($this->pns_recordstatus(), array(
		'criteria'=>$criteria,
	));
}

/* The parameterized named scope */
public function pns_recordstatus($operator=null, $value=null)
{	/*Method-1 uses $operator and $value, which were passed directly to
	this pns by the find() function. */
	
	/*Method-2 uses $this->var_operator and $this->var_recordstatus, which
	were stored in the filtering-model. */


	/* Test if Method-1 must be used ($value must be between 0 and 2;
	 and $operator must be set correctly) */
	if($value >= 0 && $value <= 2 &&
	in_array($operator,array('=','>=','>','<','<=',))){
		$this->getDbCriteria()->mergeWith(array(
			'condition'=>'t.recordstatus'.$operator.$value,
		));
	}

	/* Test if Method-2 must be used ($this->var_recordstatus must be
	 between 0 and 2; and $this->var_operator must be set correctly) */
	elseif ($this->var_recordstatus >= 0 && $this->var_recordstatus <= 2 &&
 	in_array($this->var_operator,array('=','>=','>','<','<=',))){
		$this->getDbCriteria()->mergeWith(array(
			'condition'=>
			't.recordstatus'.$this->var_operator.$this->var_recordstatus,
		));
	}
	else{
		/* Return no records if neither Method-1 nor Method-2 were used. */
		$this->getDbCriteria()->mergeWith(array(
			'condition'=>'t.recordstatus=100', /* this will return 0
			records, because 100 is not a valid recordstatus. */

		));
	}
	return $this;
}
Tip1:

You can use multiple pns's at the same time; or set flags in your model to determine which pns's to use.

return new CActiveDataProvider($this->pns1()->pns2()->pns3(), array(
		'criteria'=>$criteria,
	));
Tip2:

Since all my tables have a recordscope field, I have put the pns in my own parent-model, so that all my models can use the same pns over and over again.

Use-Case:

Use method-1 whenever you directly load records e.g. actionUpdate and actionDelete - via loadModel($id).

Use method-2 when you need a dataprovider e.g. CGridView & CListView.

Warning:

This Row-Level-Access-Control I used is just an example to explain pns. However, Row-Level-Access-Control should probably involve defaultScope(), because pns is currently (Yii 1.1.12) not enforced on related tables in relational queries.

Thanx

3 1
3 followers
Viewed: 23 862 times
Version: Unknown (update)
Category: How-tos
Written by: Gerhard Liebenberg
Last updated by: Gerhard Liebenberg
Created on: Jan 7, 2013
Last updated: 11 years ago
Update Article

Revisions

View all history