3
* @link https://www.yiiframework.com/
4
* @copyright Copyright (c) 2008 Yii Software LLC
5
* @license https://www.yiiframework.com/license/
11
use yii\base\InvalidConfigException;
13
use yii\helpers\ArrayHelper;
14
use yii\validators\BooleanValidator;
15
use yii\validators\EachValidator;
16
use yii\validators\NumberValidator;
17
use yii\validators\StringValidator;
18
use yii\validators\DateValidator;
19
use yii\validators\Validator;
22
* DataFilter is a special [[Model]] for processing query filtering specification.
23
* It allows validating and building a filter condition passed via request.
33
* "name": "some name",
41
* "id": {"in": [2, 5, 9]},
51
* In the request the filter should be specified using a key name equal to [[filterAttributeName]]. Thus actual HTTP request body
52
* will look like following:
56
* "filter": {"or": {...}},
62
* Raw filter value should be assigned to [[filter]] property of the model.
63
* You may populate it from request data via [[load()]] method:
66
* use yii\data\DataFilter;
68
* $dataFilter = new DataFilter();
69
* $dataFilter->load(Yii::$app->request->getBodyParams());
72
* In order to function this class requires a search model specified via [[searchModel]]. This search model should declare
73
* all available search attributes and their validation rules. For example:
76
* class SearchModel extends \yii\base\Model
81
* public function rules()
84
* [['id', 'name'], 'trim'],
92
* In order to reduce amount of classes, you may use [[\yii\base\DynamicModel]] instance as a [[searchModel]].
93
* In this case you should specify [[searchModel]] using a PHP callable:
97
* return (new \yii\base\DynamicModel(['id' => null, 'name' => null]))
98
* ->addRule(['id', 'name'], 'trim')
99
* ->addRule('id', 'integer')
100
* ->addRule('name', 'string');
104
* You can use [[validate()]] method to check if filter value is valid. If validation fails you can use
105
* [[getErrors()]] to get actual error messages.
107
* In order to acquire filter condition suitable for fetching data use [[build()]] method.
109
* > Note: This is a base class. Its implementation of [[build()]] simply returns normalized [[filter]] value.
110
* In order to convert filter to particular format you should use descendant of this class that implements
111
* [[buildInternal()]] method accordingly.
113
* @see ActiveDataFilter
115
* @property array $errorMessages Error messages in format `[errorKey => message]`. Note that the type of this
116
* property differs in getter and setter. See [[getErrorMessages()]] and [[setErrorMessages()]] for details.
117
* @property mixed $filter Raw filter value.
118
* @property array $searchAttributeTypes Search attribute type map. Note that the type of this property
119
* differs in getter and setter. See [[getSearchAttributeTypes()]] and [[setSearchAttributeTypes()]] for details.
120
* @property Model $searchModel Model instance. Note that the type of this property differs in getter and
121
* setter. See [[getSearchModel()]] and [[setSearchModel()]] for details.
123
* @author Paul Klimov <klimov.paul@gmail.com>
126
class DataFilter extends Model
128
const TYPE_INTEGER = 'integer';
129
const TYPE_FLOAT = 'float';
130
const TYPE_BOOLEAN = 'boolean';
131
const TYPE_STRING = 'string';
132
const TYPE_ARRAY = 'array';
133
const TYPE_DATETIME = 'datetime';
134
const TYPE_DATE = 'date';
135
const TYPE_TIME = 'time';
138
* @var string name of the attribute that handles filter value.
139
* The name is used to load data via [[load()]] method.
141
public $filterAttributeName = 'filter';
143
* @var string label for the filter attribute specified via [[filterAttributeName]].
144
* It will be used during error messages composition.
146
public $filterAttributeLabel;
148
* @var array keywords or expressions that could be used in a filter.
149
* Array keys are the expressions used in raw filter value obtained from user request.
150
* Array values are internal build keys used in this class methods.
152
* Any unspecified keyword will not be recognized as a filter control and will be treated as
153
* an attribute name. Thus you should avoid conflicts between control keywords and attribute names.
154
* For example: in case you have control keyword 'like' and an attribute named 'like', specifying condition
155
* for such attribute will be impossible.
157
* You may specify several keywords for the same filter build key, creating multiple aliases. For example:
169
* > Note: while specifying filter controls take actual data exchange format, which your API uses, in mind.
170
* > Make sure each specified control keyword is valid for the format. For example, in XML tag name can start
171
* > only with a letter character, thus controls like `>`, '=' or `$gt` will break the XML schema.
173
public $filterControls = [
188
* @var array maps filter condition keywords to validation methods.
189
* These methods are used by [[validateCondition()]] to validate raw filter conditions.
191
public $conditionValidators = [
192
'AND' => 'validateConjunctionCondition',
193
'OR' => 'validateConjunctionCondition',
194
'NOT' => 'validateBlockCondition',
195
'<' => 'validateOperatorCondition',
196
'>' => 'validateOperatorCondition',
197
'<=' => 'validateOperatorCondition',
198
'>=' => 'validateOperatorCondition',
199
'=' => 'validateOperatorCondition',
200
'!=' => 'validateOperatorCondition',
201
'IN' => 'validateOperatorCondition',
202
'NOT IN' => 'validateOperatorCondition',
203
'LIKE' => 'validateOperatorCondition',
206
* @var array specifies the list of supported search attribute types per each operator.
207
* This field should be in format: 'operatorKeyword' => ['type1', 'type2' ...].
208
* Supported types list can be specified as `*`, which indicates that operator supports all types available.
209
* Any unspecified keyword will not be considered as a valid operator.
211
public $operatorTypes = [
212
'<' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME],
213
'>' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME],
214
'<=' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME],
215
'>=' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME],
220
'LIKE' => [self::TYPE_STRING],
223
* @var array list of operators keywords, which should accept multiple values.
225
public $multiValueOperators = [
230
* @var array actual attribute names to be used in searched condition, in format: [filterAttribute => actualAttribute].
231
* For example, in case of using table joins in the search query, attribute map may look like the following:
235
* 'authorName' => '{{author}}.[[name]]'
239
* Attribute map will be applied to filter condition in [[normalize()]] method.
241
public $attributeMap = [];
243
* @var string representation of `null` instead of literal `null` in case the latter cannot be used.
246
public $nullValue = 'NULL';
249
* @var array|\Closure list of error messages responding to invalid filter structure, in format: `[errorKey => message]`.
251
private $_errorMessages;
253
* @var mixed raw filter specification.
257
* @var Model|array|string|callable model to be used for filter attributes validation.
259
private $_searchModel;
261
* @var array list of search attribute types in format: attributeName => type
263
private $_searchAttributeTypes;
267
* @return mixed raw filter value.
269
public function getFilter()
271
return $this->_filter;
275
* @param mixed $filter raw filter value.
277
public function setFilter($filter)
279
$this->_filter = $filter;
283
* @return Model model instance.
284
* @throws InvalidConfigException on invalid configuration.
286
public function getSearchModel()
288
if (!is_object($this->_searchModel) || $this->_searchModel instanceof \Closure) {
289
$model = Yii::createObject($this->_searchModel);
290
if (!$model instanceof Model) {
291
throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
293
$this->_searchModel = $model;
295
return $this->_searchModel;
299
* @param Model|array|string|callable $model model instance or its DI compatible configuration.
300
* @throws InvalidConfigException on invalid configuration.
302
public function setSearchModel($model)
304
if (is_object($model) && !$model instanceof Model && !$model instanceof \Closure) {
305
throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
307
$this->_searchModel = $model;
311
* @return array search attribute type map.
313
public function getSearchAttributeTypes()
315
if ($this->_searchAttributeTypes === null) {
316
$this->_searchAttributeTypes = $this->detectSearchAttributeTypes();
318
return $this->_searchAttributeTypes;
322
* @param array|null $searchAttributeTypes search attribute type map.
324
public function setSearchAttributeTypes($searchAttributeTypes)
326
$this->_searchAttributeTypes = $searchAttributeTypes;
330
* Composes default value for [[searchAttributeTypes]] from the [[searchModel]] validation rules.
331
* @return array attribute type map.
333
protected function detectSearchAttributeTypes()
335
$model = $this->getSearchModel();
337
$attributeTypes = [];
338
foreach ($model->activeAttributes() as $attribute) {
339
$attributeTypes[$attribute] = self::TYPE_STRING;
342
foreach ($model->getValidators() as $validator) {
343
$type = $this->detectSearchAttributeType($validator);
345
if ($type !== null) {
346
foreach ((array) $validator->attributes as $attribute) {
347
$attributeTypes[$attribute] = $type;
352
return $attributeTypes;
356
* Detect attribute type from given validator.
358
* @param Validator $validator validator from which to detect attribute type.
359
* @return string|null detected attribute type.
362
protected function detectSearchAttributeType(Validator $validator)
364
if ($validator instanceof BooleanValidator) {
365
return self::TYPE_BOOLEAN;
368
if ($validator instanceof NumberValidator) {
369
return $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT;
372
if ($validator instanceof StringValidator) {
373
return self::TYPE_STRING;
376
if ($validator instanceof EachValidator) {
377
return self::TYPE_ARRAY;
380
if ($validator instanceof DateValidator) {
381
if ($validator->type == DateValidator::TYPE_DATETIME) {
382
return self::TYPE_DATETIME;
385
if ($validator->type == DateValidator::TYPE_TIME) {
386
return self::TYPE_TIME;
388
return self::TYPE_DATE;
393
* @return array error messages in format `[errorKey => message]`.
395
public function getErrorMessages()
397
if (!is_array($this->_errorMessages)) {
398
if ($this->_errorMessages === null) {
399
$this->_errorMessages = $this->defaultErrorMessages();
401
$this->_errorMessages = array_merge(
402
$this->defaultErrorMessages(),
403
call_user_func($this->_errorMessages)
407
return $this->_errorMessages;
411
* Sets the list of error messages responding to invalid filter structure, in format: `[errorKey => message]`.
412
* Message may contain placeholders that will be populated depending on the message context.
413
* For each message a `{filter}` placeholder is available referring to the label for [[filterAttributeName]] attribute.
414
* @param array|\Closure $errorMessages error messages in `[errorKey => message]` format, or a PHP callback returning them.
416
public function setErrorMessages($errorMessages)
418
if (is_array($errorMessages)) {
419
$errorMessages = array_merge($this->defaultErrorMessages(), $errorMessages);
421
$this->_errorMessages = $errorMessages;
425
* Returns default values for [[errorMessages]].
426
* @return array default error messages in `[errorKey => message]` format.
428
protected function defaultErrorMessages()
431
'invalidFilter' => Yii::t('yii', 'The format of {filter} is invalid.'),
432
'operatorRequireMultipleOperands' => Yii::t('yii', 'Operator "{operator}" requires multiple operands.'),
433
'unknownAttribute' => Yii::t('yii', 'Unknown filter attribute "{attribute}"'),
434
'invalidAttributeValueFormat' => Yii::t('yii', 'Condition for "{attribute}" should be either a value or valid operator specification.'),
435
'operatorRequireAttribute' => Yii::t('yii', 'Operator "{operator}" must be used with a search attribute.'),
436
'unsupportedOperatorType' => Yii::t('yii', '"{attribute}" does not support operator "{operator}".'),
441
* Parses content of the message from [[errorMessages]], specified by message key.
442
* @param string $messageKey message key.
443
* @param array $params params to be parsed into the message.
444
* @return string composed message string.
446
protected function parseErrorMessage($messageKey, $params = [])
448
$messages = $this->getErrorMessages();
449
if (isset($messages[$messageKey])) {
450
$message = $messages[$messageKey];
452
$message = Yii::t('yii', 'The format of {filter} is invalid.');
455
$params = array_merge(
457
'filter' => $this->getAttributeLabel($this->filterAttributeName),
462
return Yii::$app->getI18n()->format($message, $params, Yii::$app->language);
470
public function attributes()
473
$this->filterAttributeName,
480
public function formName()
488
public function rules()
491
[$this->filterAttributeName, 'validateFilter', 'skipOnEmpty' => false],
498
public function attributeLabels()
501
$this->filterAttributeName => $this->filterAttributeLabel,
508
* Validates filter attribute value to match filer condition specification.
510
public function validateFilter()
512
$value = $this->getFilter();
513
if ($value !== null) {
514
$this->validateCondition($value);
519
* Validates filter condition.
520
* @param mixed $condition raw filter condition.
522
protected function validateCondition($condition)
524
if (!is_array($condition)) {
525
$this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidFilter'));
529
if (empty($condition)) {
533
foreach ($condition as $key => $value) {
534
$method = 'validateAttributeCondition';
535
if (isset($this->filterControls[$key])) {
536
$controlKey = $this->filterControls[$key];
537
if (isset($this->conditionValidators[$controlKey])) {
538
$method = $this->conditionValidators[$controlKey];
541
$this->$method($key, $value);
546
* Validates conjunction condition that consists of multiple independent ones.
547
* This covers such operators as `and` and `or`.
548
* @param string $operator raw operator control keyword.
549
* @param mixed $condition raw condition.
551
protected function validateConjunctionCondition($operator, $condition)
553
if (!is_array($condition) || !ArrayHelper::isIndexed($condition)) {
554
$this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
558
foreach ($condition as $part) {
559
$this->validateCondition($part);
564
* Validates block condition that consists of a single condition.
565
* This covers such operators as `not`.
566
* @param string $operator raw operator control keyword.
567
* @param mixed $condition raw condition.
569
protected function validateBlockCondition($operator, $condition)
571
$this->validateCondition($condition);
575
* Validates search condition for a particular attribute.
576
* @param string $attribute search attribute name.
577
* @param mixed $condition search condition.
579
protected function validateAttributeCondition($attribute, $condition)
581
$attributeTypes = $this->getSearchAttributeTypes();
582
if (!isset($attributeTypes[$attribute])) {
583
$this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
587
if (is_array($condition)) {
589
foreach ($condition as $rawOperator => $value) {
590
if (isset($this->filterControls[$rawOperator])) {
591
$operator = $this->filterControls[$rawOperator];
592
if (isset($this->operatorTypes[$operator])) {
594
$this->validateOperatorCondition($rawOperator, $value, $attribute);
599
if ($operatorCount > 0) {
600
if ($operatorCount < count($condition)) {
601
$this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidAttributeValueFormat', ['attribute' => $attribute]));
604
// attribute may allow array value:
605
$this->validateAttributeValue($attribute, $condition);
608
$this->validateAttributeValue($attribute, $condition);
613
* Validates operator condition.
614
* @param string $operator raw operator control keyword.
615
* @param mixed $condition attribute condition.
616
* @param string|null $attribute attribute name.
618
protected function validateOperatorCondition($operator, $condition, $attribute = null)
620
if ($attribute === null) {
621
// absence of an attribute indicates that operator has been placed in a wrong position
622
$this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireAttribute', ['operator' => $operator]));
626
$internalOperator = $this->filterControls[$operator];
628
// check operator type :
629
$operatorTypes = $this->operatorTypes[$internalOperator];
630
if ($operatorTypes !== '*') {
631
$attributeTypes = $this->getSearchAttributeTypes();
632
$attributeType = $attributeTypes[$attribute];
633
if (!in_array($attributeType, $operatorTypes, true)) {
634
$this->addError($this->filterAttributeName, $this->parseErrorMessage('unsupportedOperatorType', ['attribute' => $attribute, 'operator' => $operator]));
639
if (in_array($internalOperator, $this->multiValueOperators, true)) {
640
// multi-value operator:
641
if (!is_array($condition)) {
642
$this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
644
foreach ($condition as $v) {
645
$this->validateAttributeValue($attribute, $v);
649
// single-value operator :
650
$this->validateAttributeValue($attribute, $condition);
655
* Validates attribute value in the scope of [[model]].
656
* @param string $attribute attribute name.
657
* @param mixed $value attribute value.
659
protected function validateAttributeValue($attribute, $value)
661
$model = $this->getSearchModel();
662
if (!$model->isAttributeSafe($attribute)) {
663
$this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
667
$model->{$attribute} = $value === $this->nullValue ? null : $value;
668
if (!$model->validate([$attribute])) {
669
$this->addError($this->filterAttributeName, $model->getFirstError($attribute));
675
* Validates attribute value in the scope of [[searchModel]], applying attribute value filters if any.
676
* @param string $attribute attribute name.
677
* @param mixed $value attribute value.
678
* @return mixed filtered attribute value.
680
protected function filterAttributeValue($attribute, $value)
682
$model = $this->getSearchModel();
683
if (!$model->isAttributeSafe($attribute)) {
684
$this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
687
$model->{$attribute} = $value;
688
if (!$model->validate([$attribute])) {
689
$this->addError($this->filterAttributeName, $model->getFirstError($attribute));
693
return $model->{$attribute};
699
* Builds actual filter specification form [[filter]] value.
700
* @param bool $runValidation whether to perform validation (calling [[validate()]])
701
* before building the filter. Defaults to `true`. If the validation fails, no filter will
702
* be built and this method will return `false`.
703
* @return mixed|false built actual filter value, or `false` if validation fails.
705
public function build($runValidation = true)
707
if ($runValidation && !$this->validate()) {
710
return $this->buildInternal();
714
* Performs actual filter build.
715
* By default this method returns result of [[normalize()]].
716
* The child class may override this method providing more specific implementation.
717
* @return mixed built actual filter value.
719
protected function buildInternal()
721
return $this->normalize(false);
725
* Normalizes filter value, replacing raw keys according to [[filterControls]] and [[attributeMap]].
726
* @param bool $runValidation whether to perform validation (calling [[validate()]])
727
* before normalizing the filter. Defaults to `true`. If the validation fails, no filter will
728
* be processed and this method will return `false`.
729
* @return array|bool normalized filter value, or `false` if validation fails.
731
public function normalize($runValidation = true)
733
if ($runValidation && !$this->validate()) {
737
$filter = $this->getFilter();
738
if (!is_array($filter) || empty($filter)) {
742
return $this->normalizeComplexFilter($filter);
746
* Normalizes complex filter recursively.
747
* @param array $filter raw filter.
748
* @return array normalized filter.
750
private function normalizeComplexFilter(array $filter)
753
foreach ($filter as $key => $value) {
754
if (isset($this->filterControls[$key])) {
755
$key = $this->filterControls[$key];
756
} elseif (isset($this->attributeMap[$key])) {
757
$key = $this->attributeMap[$key];
759
if (is_array($value)) {
760
$result[$key] = $this->normalizeComplexFilter($value);
761
} elseif ($value === $this->nullValue) {
762
$result[$key] = null;
764
$result[$key] = $value;
775
public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
777
if ($name === $this->filterAttributeName) {
780
return parent::canGetProperty($name, $checkVars, $checkBehaviors);
786
public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
788
if ($name === $this->filterAttributeName) {
791
return parent::canSetProperty($name, $checkVars, $checkBehaviors);
797
public function __get($name)
799
if ($name === $this->filterAttributeName) {
800
return $this->getFilter();
803
return parent::__get($name);
809
public function __set($name, $value)
811
if ($name === $this->filterAttributeName) {
812
$this->setFilter($value);
814
parent::__set($name, $value);
821
public function __isset($name)
823
if ($name === $this->filterAttributeName) {
824
return $this->getFilter() !== null;
827
return parent::__isset($name);
833
public function __unset($name)
835
if ($name === $this->filterAttributeName) {
836
$this->setFilter(null);
838
parent::__unset($name);