In order to provide filtering capabilities for data grids and views including ‘list’ and ‘admin’ and make its usage in a way similar to CSort and CPagination where it requires no change to the model or the controller, I created a CWebScope class to manage filtering of query results based on criteria defined by the end-user.
To use it, CWebScope can be used in a similar way the CSort and CPagination are used like the following:
[indent]
public function actionList()
{
$criteria=new CDbCriteria;
$scopes = new CWebScope('Post');
$scopes->attributes=array_slice(Post::model()->attributeLabels(), 8, 5);
$scopes->applyScope($criteria);
$pages=new CPagination(Post::model()->count($criteria));
$pages->pageSize=self::PAGE_SIZE;
$pages->applyLimit($criteria);
$sort=new CSort('Post');
$sort->attributes=array_slice(Post::model()->attributeLabels(), 7, 6);
$sort->multiSort=false;
$sort->applyOrder($criteria);
$models=Post::model()->findAll($criteria);
$this->render('list',array(
'models'=>$models,
'pages'=>$pages,
'sort'=>$sort,
'scopes'=>$scopes,
));
}
[/indent]
The CWebScope uses the same concept of CSort (actually the same code ) when it comes to defining what attributes are allowed to be used for filtering (sorting in the case of CSort) and attribute aliasing to hide the actual database column names from the end user when the page is rendered, both for security and for aesthetic reasons.
In the appropriate view, you can do the following to show a filtering bar. The bar will show a text box for every defined attribute along with a submit button for filtering and a link to clear the filter scope. The clear filter link is designed in a way to maintain any sorting and/or other URL querystring parameters.
[indent]
<tr>
<?php echo CWebScope::beginForm() ?>
<td><?php echo $scopes->textField('post_field_1', array('size'=>'10'));?></td>
<td><?php echo $scopes->textField('post_field_2', array('size'=>'10')); ?></td>
<td><?php echo $scopes->textField('post_field_3', array('size'=>'10')); ?></td>
<td><?php echo $scopes->textField('post_field_4', array('size'=>'10')); ?></td>
<td> </td>
<td><?php echo CWebScope::submitButton(); ?>
<?php echo CWebScope::linkClear(); ?></td>
<?php echo CWebScope::endForm(); ?>
<tr>
[/indent]
[b]
Other than the currently available capabilities, what do you think the CWebScope class should offer as options and methods. I thought of providing a drop down option to the users similar to the text box… What else do you think is appropriate for such a filtering class. -= Who knows, it might make it to an official Yii release and become part of the framework =-
[/b]
Below is the CWebScope code for your review. Please also do let me know of any enhancements related to security, SQL injections, URL tampering and other important issues to consider.
Thank You
/Kaf
[indent]
<?php
/**
* CWebScope class file.
*
* @author Khaled Afiouni <>
* @link http://www.afiouni.com/
*/
/**
* CWebScope provides services related to data filtering for Controller actions
*
*
* When data needs to be filtered according to one or several attributes,
* we can use CWebScope to manage the filtering information and add
* appropriate filtering criteria to the database SQL statements.
*
* CWebScope is designed to be used together with {@link CActiveRecord}.
* You can use CWebScope to modify a {@link CDbCriteria} instance so that
* it can limit the query results according to the defined filter
*
*
*/
class CWebScope extends CComponent
{
/**
* @var string the class name of data models that need to be filtered.
* This should be a child class of {@link CActiveRecord}.
*/
public $modelClass;
/**
* @var array list of attributes that are allowed for filtering.
* For example, array('user_id','create_time') would specify that only 'user_id'
* and 'create_time' can be used for filtering.
* Defaults to null, meaning all attributes of the {@link modelClass} can be used.
* This property can also be used to specify attribute aliases that should appear
* in the 'scope' GET parameter in place of the original attribute names.
* In this case, the aliases should be array values while the attribute names
* should be the corresponding array keys. Do not use '-' and '.' in the aliases
* as they are used as {@link separators}.
*/
public $attributes;
/**
* @var string the name of the GET parameter that specifies which attributes to
* be used for filtering
*/
public $scopeVar='scope';
/**
* Constructor.
* @param string the class name of data models that need to be filtered.
* This should be a child class of {@link CActiveRecord}.
*/
public function __construct($modelClass)
{
$this->modelClass=$modelClass;
}
/**
* Modifies the query criteria by changing its {@link CDbCriteria::condition} property.
* Appends the current search criteria to the passed {@link CDbCriteria
* @param CDbCriteria the query criteria
* @return an updated {@link CDbCriteria}
*/
public function applyScope ($criteria)
{
if(!isset($_GET[$this->scopeVar]))
return true;
$conditions = $_GET[$this->scopeVar];
if (isset($conditions['action']))
unset ($conditions['action']);
$scopes = array();
foreach ($conditions as $key=>$value)
{
if (($key=$this->validateAttribute($key))!==false)
if (isset($value) && $value != NULL && $value != '')
$scopes[] = $key . " like '%" . $value . "%'";
}
$scope = implode (' AND ', $scopes);
return $criteria->mergeWith(array('condition'=>$scope));
}
/**
Originally Written for CSort... Copied here for Convenience
* Resolves the attribute label based on label definition in the AR class.
* This will invoke {@link CActiveRecord::getAttributeLabel} to determine what label to use.
* @param string the attribute name.
* @return string the attribute label
*/
public function resolveLabel($attribute)
{
if(($pos=strpos($attribute,'.'))!==false)
{
$baseModel=CActiveRecord::model($this->modelClass);
if(($relation=$baseModel->getActiveRelation(substr($attribute,0,$pos)))!==null)
return CActiveRecord::model($relation->className)->getAttributeLabel(substr($attribute,$pos+1));
else
return $baseModel->getAttributeLabel(substr($attribute,$pos+1));
}
return CActiveRecord::model($this->modelClass)->getAttributeLabel($attribute);
}
/**
* Originaly written for CSort... Copied here for convenience
* Validates an attribute that is requested to be filtered.
* The validation is based on {@link attributes} and {@link CActiveRecord::attributeNames}.
* False will be returned if the attribute is not allowed to be used for Filtering.
* If the attribute is aliased via {@link attributes}, the original
* attribute name will be returned.
* @param string the attribute name (could be an alias) that the user requests to sort on
* @return string the real attribute name. False if the attribute cannot be used for filtering
*/
public function validateAttribute($attribute)
{
if(empty($this->attributes))
$attributes=CActiveRecord::model($this->modelClass)->attributeNames();
else
$attributes=$this->attributes;
foreach($attributes as $name=>$alias)
{
if($alias===$attribute)
return is_string($name) ? $name : $alias;
}
return false;
}
/**
* Uses the {@link CHtml::beginForm} to generate an opening form tag.
* Note, only the open tag is generated. A close tag should be placed manually
* at the end of the form.
* @param array additional HTML attributes (see {@link tag}).
* @return string the generated form tag.
* @see endForm
*/
public static function beginForm($htmlOptions=array())
{
// The form should submit the scope (filter) values as 'get'
// for the values to remain in the URL for the filtering to
// work hand in hand with sorting
return CHtml::beginform ('', 'get', $htmlOptions);
}
/**
* Uses the {@link CHtml::beginForm} to generate a closing form tag.
* @return string the generated tag
* @see beginForm
*/
public static function endForm()
{
return CHtml::endForm();
}
/**
* Uses the {@link CHtml::submitButton}to generate a submit button.
* @param string the button label. Defaults to 'Filter'
* @param array additional HTML attributes. Besides normal HTML attributes, a few special
* attributes are also recognized (see {@link clientChange} and {@link tag} for more details.)
* @return string the generated button tag
*/
public static function submitButton($label='Filter',$htmlOptions=array())
{
if (!isset($htmlOptions['name']))
$htmlOptions['name'] = 'scope[action]';
return CHtml::submitButton($label, $htmlOptions);
}
/**
* Uses the {@link CHtml::textField} to generate a text field input.
* @param string the input name
* @param string the input value
* @param array additional HTML attributes. Besides normal HTML attributes, a few special
* attributes are also recognized (see {@link clientChange} and {@link tag} for more details.)
* @return string the generated input field
*/
public function textField($attribute,$htmlOptions=array())
{
$attribute = $this->resolveLabel($attribute);
if (isset($_GET[$this->scopeVar][$attribute]))
$value = $_GET[$this->scopeVar][$attribute];
$name = $this->scopeVar.'['.$attribute.']';
return CHtml::textField($name,$value,$htmlOptions);
}
/**
* Creates a link that clears the filtering options and removes all filtering parameters
* from URL. Ues the {@link CHtml::link} to generate the URL.
* @param string the label of the hyperlink
* @param array additional HTML options
* @return string the generated hyperlink
*/
public static function linkClear($label='Clear Filter',$htmlOptions=array())
{
$urlVars = $_GET;
$urlVarsClean = array();
foreach ($urlVars as $key=>$value)
{
if ($key != 'scope')
$urlVarsClean[$key]=$value;
}
$url=Yii::app()->getController()->createUrl('',$urlVarsClean);
return CHtml::link($label,$url,$htmlOptions);
}
}
?>
[/indent]