Difference between #22 and #21 of 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.

unchanged
Title
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.
unchanged
Category
How-tos
unchanged
Tags
reuse filtering criteria; parameterized named scope in dataprovider
changed
Content
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:


~~~
[php]
$model=new myTable('search');
$model->unsetAttributes();
$model->recordstatus = 1;
~~~

In the model's search function:
~~~
[php]
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:
~~~
[php]
$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:
~~~
[php]
$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: 

~~~
[php]
 $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:
~~~
[php]
$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:**
~~~
[php]
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.
~~~
[php]
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. Here is a wiki on
[defaultScope](http://www.yiiframework.com/wiki/486/defaultscope/
"defaultScope") doing the same kind of thing as shown in this
wiki. 

Thanx
Write new article