Issues (910)

framework/data/DataFilter.php (1 issue)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\data;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\base\Model;
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;
20
21
/**
22
 * DataFilter is a special [[Model]] for processing query filtering specification.
23
 * It allows validating and building a filter condition passed via request.
24
 *
25
 * Filter example:
26
 *
27
 * ```json
28
 * {
29
 *     "or": [
30
 *         {
31
 *             "and": [
32
 *                 {
33
 *                     "name": "some name",
34
 *                 },
35
 *                 {
36
 *                     "price": "25",
37
 *                 }
38
 *             ]
39
 *         },
40
 *         {
41
 *             "id": {"in": [2, 5, 9]},
42
 *             "price": {
43
 *                 "gt": 10,
44
 *                 "lt": 50
45
 *             }
46
 *         }
47
 *     ]
48
 * }
49
 * ```
50
 *
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:
53
 *
54
 * ```json
55
 * {
56
 *     "filter": {"or": {...}},
57
 *     "page": 2,
58
 *     ...
59
 * }
60
 * ```
61
 *
62
 * Raw filter value should be assigned to [[filter]] property of the model.
63
 * You may populate it from request data via [[load()]] method:
64
 *
65
 * ```php
66
 * use yii\data\DataFilter;
67
 *
68
 * $dataFilter = new DataFilter();
69
 * $dataFilter->load(Yii::$app->request->getBodyParams());
70
 * ```
71
 *
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:
74
 *
75
 * ```php
76
 * class SearchModel extends \yii\base\Model
77
 * {
78
 *     public $id;
79
 *     public $name;
80
 *
81
 *     public function rules()
82
 *     {
83
 *         return [
84
 *             [['id', 'name'], 'trim'],
85
 *             ['id', 'integer'],
86
 *             ['name', 'string'],
87
 *         ];
88
 *     }
89
 * }
90
 * ```
91
 *
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:
94
 *
95
 * ```php
96
 * function () {
97
 *     return (new \yii\base\DynamicModel(['id' => null, 'name' => null]))
98
 *         ->addRule(['id', 'name'], 'trim')
99
 *         ->addRule('id', 'integer')
100
 *         ->addRule('name', 'string');
101
 * }
102
 * ```
103
 *
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.
106
 *
107
 * In order to acquire filter condition suitable for fetching data use [[build()]] method.
108
 *
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.
112
 *
113
 * @see ActiveDataFilter
114
 *
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.
122
 *
123
 * @author Paul Klimov <[email protected]>
124
 * @since 2.0.13
125
 */
126
class DataFilter extends Model
127
{
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';
136
137
    /**
138
     * @var string name of the attribute that handles filter value.
139
     * The name is used to load data via [[load()]] method.
140
     */
141
    public $filterAttributeName = 'filter';
142
    /**
143
     * @var string label for the filter attribute specified via [[filterAttributeName]].
144
     * It will be used during error messages composition.
145
     */
146
    public $filterAttributeLabel;
147
    /**
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.
151
     *
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.
156
     *
157
     * You may specify several keywords for the same filter build key, creating multiple aliases. For example:
158
     *
159
     * ```php
160
     * [
161
     *     'eq' => '=',
162
     *     '=' => '=',
163
     *     '==' => '=',
164
     *     '===' => '=',
165
     *     // ...
166
     * ]
167
     * ```
168
     *
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.
172
     */
173
    public $filterControls = [
174
        'and' => 'AND',
175
        'or' => 'OR',
176
        'not' => 'NOT',
177
        'lt' => '<',
178
        'gt' => '>',
179
        'lte' => '<=',
180
        'gte' => '>=',
181
        'eq' => '=',
182
        'neq' => '!=',
183
        'in' => 'IN',
184
        'nin' => 'NOT IN',
185
        'like' => 'LIKE',
186
    ];
187
    /**
188
     * @var array maps filter condition keywords to validation methods.
189
     * These methods are used by [[validateCondition()]] to validate raw filter conditions.
190
     */
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',
204
    ];
205
    /**
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.
210
     */
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],
216
        '=' => '*',
217
        '!=' => '*',
218
        'IN' => '*',
219
        'NOT IN' => '*',
220
        'LIKE' => [self::TYPE_STRING],
221
    ];
222
    /**
223
     * @var array list of operators keywords, which should accept multiple values.
224
     */
225
    public $multiValueOperators = [
226
        'IN',
227
        'NOT IN',
228
    ];
229
    /**
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:
232
     *
233
     * ```php
234
     * [
235
     *     'authorName' => '{{author}}.[[name]]'
236
     * ]
237
     * ```
238
     *
239
     * Attribute map will be applied to filter condition in [[normalize()]] method.
240
     */
241
    public $attributeMap = [];
242
    /**
243
     * @var string representation of `null` instead of literal `null` in case the latter cannot be used.
244
     * @since 2.0.40
245
     */
246
    public $nullValue = 'NULL';
247
248
    /**
249
     * @var array|\Closure list of error messages responding to invalid filter structure, in format: `[errorKey => message]`.
250
     */
251
    private $_errorMessages;
252
    /**
253
     * @var mixed raw filter specification.
254
     */
255
    private $_filter;
256
    /**
257
     * @var Model|array|string|callable model to be used for filter attributes validation.
258
     */
259
    private $_searchModel;
260
    /**
261
     * @var array list of search attribute types in format: attributeName => type
262
     */
263
    private $_searchAttributeTypes;
264
265
266
    /**
267
     * @return mixed raw filter value.
268
     */
269 42
    public function getFilter()
270
    {
271 42
        return $this->_filter;
272
    }
273
274
    /**
275
     * @param mixed $filter raw filter value.
276
     */
277 42
    public function setFilter($filter)
278
    {
279 42
        $this->_filter = $filter;
280
    }
281
282
    /**
283
     * @return Model model instance.
284
     * @throws InvalidConfigException on invalid configuration.
285
     */
286 23
    public function getSearchModel()
287
    {
288 23
        if (!is_object($this->_searchModel) || $this->_searchModel instanceof \Closure) {
289 1
            $model = Yii::createObject($this->_searchModel);
290 1
            if (!$model instanceof Model) {
291
                throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

291
                throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . /** @scrutinizer ignore-deprecated */ Model::className() . '` or its DI compatible configuration.');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
292
            }
293 1
            $this->_searchModel = $model;
294
        }
295 23
        return $this->_searchModel;
296
    }
297
298
    /**
299
     * @param Model|array|string|callable $model model instance or its DI compatible configuration.
300
     * @throws InvalidConfigException on invalid configuration.
301
     */
302 42
    public function setSearchModel($model)
303
    {
304 42
        if (is_object($model) && !$model instanceof Model && !$model instanceof \Closure) {
305 1
            throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
306
        }
307 42
        $this->_searchModel = $model;
308
    }
309
310
    /**
311
     * @return array search attribute type map.
312
     */
313 22
    public function getSearchAttributeTypes()
314
    {
315 22
        if ($this->_searchAttributeTypes === null) {
316 22
            $this->_searchAttributeTypes = $this->detectSearchAttributeTypes();
317
        }
318 22
        return $this->_searchAttributeTypes;
319
    }
320
321
    /**
322
     * @param array|null $searchAttributeTypes search attribute type map.
323
     */
324
    public function setSearchAttributeTypes($searchAttributeTypes)
325
    {
326
        $this->_searchAttributeTypes = $searchAttributeTypes;
327
    }
328
329
    /**
330
     * Composes default value for [[searchAttributeTypes]] from the [[searchModel]] validation rules.
331
     * @return array attribute type map.
332
     */
333 22
    protected function detectSearchAttributeTypes()
334
    {
335 22
        $model = $this->getSearchModel();
336
337 22
        $attributeTypes = [];
338 22
        foreach ($model->activeAttributes() as $attribute) {
339 22
            $attributeTypes[$attribute] = self::TYPE_STRING;
340
        }
341
342 22
        foreach ($model->getValidators() as $validator) {
343 22
            $type = $this->detectSearchAttributeType($validator);
344
345 22
            if ($type !== null) {
346 22
                foreach ((array) $validator->attributes as $attribute) {
347 22
                    $attributeTypes[$attribute] = $type;
348
                }
349
            }
350
        }
351
352 22
        return $attributeTypes;
353
    }
354
355
    /**
356
     * Detect attribute type from given validator.
357
     *
358
     * @param Validator $validator validator from which to detect attribute type.
359
     * @return string|null detected attribute type.
360
     * @since 2.0.14
361
     */
362 22
    protected function detectSearchAttributeType(Validator $validator)
363
    {
364 22
        if ($validator instanceof BooleanValidator) {
365
            return self::TYPE_BOOLEAN;
366
        }
367
368 22
        if ($validator instanceof NumberValidator) {
369 21
            return $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT;
370
        }
371
372 22
        if ($validator instanceof StringValidator) {
373 22
            return self::TYPE_STRING;
374
        }
375
376 22
        if ($validator instanceof EachValidator) {
377 21
            return self::TYPE_ARRAY;
378
        }
379
380 22
        if ($validator instanceof DateValidator) {
381 13
            if ($validator->type == DateValidator::TYPE_DATETIME) {
382 13
                return self::TYPE_DATETIME;
383
            }
384
385
            if ($validator->type == DateValidator::TYPE_TIME) {
386
                return self::TYPE_TIME;
387
            }
388
            return self::TYPE_DATE;
389
        }
390
    }
391
392
    /**
393
     * @return array error messages in format `[errorKey => message]`.
394
     */
395 5
    public function getErrorMessages()
396
    {
397 5
        if (!is_array($this->_errorMessages)) {
398 5
            if ($this->_errorMessages === null) {
399 4
                $this->_errorMessages = $this->defaultErrorMessages();
400
            } else {
401 1
                $this->_errorMessages = array_merge(
402 1
                    $this->defaultErrorMessages(),
403 1
                    call_user_func($this->_errorMessages)
404 1
                );
405
            }
406
        }
407 5
        return $this->_errorMessages;
408
    }
409
410
    /**
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.
415
     */
416 1
    public function setErrorMessages($errorMessages)
417
    {
418 1
        if (is_array($errorMessages)) {
419 1
            $errorMessages = array_merge($this->defaultErrorMessages(), $errorMessages);
420
        }
421 1
        $this->_errorMessages = $errorMessages;
422
    }
423
424
    /**
425
     * Returns default values for [[errorMessages]].
426
     * @return array default error messages in `[errorKey => message]` format.
427
     */
428 5
    protected function defaultErrorMessages()
429
    {
430 5
        return [
431 5
            'invalidFilter' => Yii::t('yii', 'The format of {filter} is invalid.'),
432 5
            'operatorRequireMultipleOperands' => Yii::t('yii', 'Operator "{operator}" requires multiple operands.'),
433 5
            'unknownAttribute' => Yii::t('yii', 'Unknown filter attribute "{attribute}"'),
434 5
            'invalidAttributeValueFormat' => Yii::t('yii', 'Condition for "{attribute}" should be either a value or valid operator specification.'),
435 5
            'operatorRequireAttribute' => Yii::t('yii', 'Operator "{operator}" must be used with a search attribute.'),
436 5
            'unsupportedOperatorType' => Yii::t('yii', '"{attribute}" does not support operator "{operator}".'),
437 5
        ];
438
    }
439
440
    /**
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.
445
     */
446 4
    protected function parseErrorMessage($messageKey, $params = [])
447
    {
448 4
        $messages = $this->getErrorMessages();
449 4
        if (isset($messages[$messageKey])) {
450 4
            $message = $messages[$messageKey];
451
        } else {
452
            $message = Yii::t('yii', 'The format of {filter} is invalid.');
453
        }
454
455 4
        $params = array_merge(
456 4
            [
457 4
                'filter' => $this->getAttributeLabel($this->filterAttributeName),
458 4
            ],
459 4
            $params
460 4
        );
461
462 4
        return Yii::$app->getI18n()->format($message, $params, Yii::$app->language);
463
    }
464
465
    // Model specific:
466
467
    /**
468
     * {@inheritdoc}
469
     */
470
    public function attributes()
471
    {
472
        return [
473
            $this->filterAttributeName,
474
        ];
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480 1
    public function formName()
481
    {
482 1
        return '';
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488 29
    public function rules()
489
    {
490 29
        return [
491 29
            [$this->filterAttributeName, 'validateFilter', 'skipOnEmpty' => false],
492 29
        ];
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498 4
    public function attributeLabels()
499
    {
500 4
        return [
501 4
            $this->filterAttributeName => $this->filterAttributeLabel,
502 4
        ];
503
    }
504
505
    // Validation:
506
507
    /**
508
     * Validates filter attribute value to match filer condition specification.
509
     */
510 28
    public function validateFilter()
511
    {
512 28
        $value = $this->getFilter();
513 28
        if ($value !== null) {
514 27
            $this->validateCondition($value);
515
        }
516
    }
517
518
    /**
519
     * Validates filter condition.
520
     * @param mixed $condition raw filter condition.
521
     */
522 27
    protected function validateCondition($condition)
523
    {
524 27
        if (!is_array($condition)) {
525 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidFilter'));
526 1
            return;
527
        }
528
529 26
        if (empty($condition)) {
530 2
            return;
531
        }
532
533 24
        foreach ($condition as $key => $value) {
534 24
            $method = 'validateAttributeCondition';
535 24
            if (isset($this->filterControls[$key])) {
536 9
                $controlKey = $this->filterControls[$key];
537 9
                if (isset($this->conditionValidators[$controlKey])) {
538 9
                    $method = $this->conditionValidators[$controlKey];
539
                }
540
            }
541 24
            $this->$method($key, $value);
542
        }
543
    }
544
545
    /**
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.
550
     */
551 6
    protected function validateConjunctionCondition($operator, $condition)
552
    {
553 6
        if (!is_array($condition) || !ArrayHelper::isIndexed($condition)) {
554 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
555 1
            return;
556
        }
557
558 5
        foreach ($condition as $part) {
559 5
            $this->validateCondition($part);
560
        }
561
    }
562
563
    /**
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.
568
     */
569 3
    protected function validateBlockCondition($operator, $condition)
570
    {
571 3
        $this->validateCondition($condition);
572
    }
573
574
    /**
575
     * Validates search condition for a particular attribute.
576
     * @param string $attribute search attribute name.
577
     * @param mixed $condition search condition.
578
     */
579 22
    protected function validateAttributeCondition($attribute, $condition)
580
    {
581 22
        $attributeTypes = $this->getSearchAttributeTypes();
582 22
        if (!isset($attributeTypes[$attribute])) {
583 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
584 1
            return;
585
        }
586
587 22
        if (is_array($condition)) {
588 14
            $operatorCount = 0;
589 14
            foreach ($condition as $rawOperator => $value) {
590 14
                if (isset($this->filterControls[$rawOperator])) {
591 12
                    $operator = $this->filterControls[$rawOperator];
592 12
                    if (isset($this->operatorTypes[$operator])) {
593 12
                        $operatorCount++;
594 12
                        $this->validateOperatorCondition($rawOperator, $value, $attribute);
595
                    }
596
                }
597
            }
598
599 14
            if ($operatorCount > 0) {
600 12
                if ($operatorCount < count($condition)) {
601 12
                    $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidAttributeValueFormat', ['attribute' => $attribute]));
602
                }
603
            } else {
604
                // attribute may allow array value:
605 14
                $this->validateAttributeValue($attribute, $condition);
606
            }
607
        } else {
608 12
            $this->validateAttributeValue($attribute, $condition);
609
        }
610
    }
611
612
    /**
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.
617
     */
618 13
    protected function validateOperatorCondition($operator, $condition, $attribute = null)
619
    {
620 13
        if ($attribute === null) {
621
            // absence of an attribute indicates that operator has been placed in a wrong position
622 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireAttribute', ['operator' => $operator]));
623 1
            return;
624
        }
625
626 12
        $internalOperator = $this->filterControls[$operator];
627
628
        // check operator type :
629 12
        $operatorTypes = $this->operatorTypes[$internalOperator];
630 12
        if ($operatorTypes !== '*') {
631 6
            $attributeTypes = $this->getSearchAttributeTypes();
632 6
            $attributeType = $attributeTypes[$attribute];
633 6
            if (!in_array($attributeType, $operatorTypes, true)) {
634
                $this->addError($this->filterAttributeName, $this->parseErrorMessage('unsupportedOperatorType', ['attribute' => $attribute, 'operator' => $operator]));
635
                return;
636
            }
637
        }
638
639 12
        if (in_array($internalOperator, $this->multiValueOperators, true)) {
640
            // multi-value operator:
641 3
            if (!is_array($condition)) {
642
                $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
643
            } else {
644 3
                foreach ($condition as $v) {
645 3
                    $this->validateAttributeValue($attribute, $v);
646
                }
647
            }
648
        } else {
649
            // single-value operator :
650 10
            $this->validateAttributeValue($attribute, $condition);
651
        }
652
    }
653
654
    /**
655
     * Validates attribute value in the scope of [[model]].
656
     * @param string $attribute attribute name.
657
     * @param mixed $value attribute value.
658
     */
659 22
    protected function validateAttributeValue($attribute, $value)
660
    {
661 22
        $model = $this->getSearchModel();
662 22
        if (!$model->isAttributeSafe($attribute)) {
663
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
664
            return;
665
        }
666
667 22
        $model->{$attribute} = $value === $this->nullValue ? null : $value;
668 22
        if (!$model->validate([$attribute])) {
669 1
            $this->addError($this->filterAttributeName, $model->getFirstError($attribute));
670 1
            return;
671
        }
672
    }
673
674
    /**
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.
679
     */
680 8
    protected function filterAttributeValue($attribute, $value)
681
    {
682 8
        $model = $this->getSearchModel();
683 8
        if (!$model->isAttributeSafe($attribute)) {
684
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
685
            return $value;
686
        }
687 8
        $model->{$attribute} = $value;
688 8
        if (!$model->validate([$attribute])) {
689 1
            $this->addError($this->filterAttributeName, $model->getFirstError($attribute));
690 1
            return $value;
691
        }
692
693 7
        return $model->{$attribute};
694
    }
695
696
    // Build:
697
698
    /**
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.
704
     */
705 10
    public function build($runValidation = true)
706
    {
707 10
        if ($runValidation && !$this->validate()) {
708
            return false;
709
        }
710 10
        return $this->buildInternal();
711
    }
712
713
    /**
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.
718
     */
719
    protected function buildInternal()
720
    {
721
        return $this->normalize(false);
722
    }
723
724
    /**
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.
730
     */
731 23
    public function normalize($runValidation = true)
732
    {
733 23
        if ($runValidation && !$this->validate()) {
734
            return false;
735
        }
736
737 23
        $filter = $this->getFilter();
738 23
        if (!is_array($filter) || empty($filter)) {
739 4
            return [];
740
        }
741
742 19
        return $this->normalizeComplexFilter($filter);
743
    }
744
745
    /**
746
     * Normalizes complex filter recursively.
747
     * @param array $filter raw filter.
748
     * @return array normalized filter.
749
     */
750 19
    private function normalizeComplexFilter(array $filter)
751
    {
752 19
        $result = [];
753 19
        foreach ($filter as $key => $value) {
754 19
            if (isset($this->filterControls[$key])) {
755 9
                $key = $this->filterControls[$key];
756 19
            } elseif (isset($this->attributeMap[$key])) {
757 1
                $key = $this->attributeMap[$key];
758
            }
759 19
            if (is_array($value)) {
760 10
                $result[$key] = $this->normalizeComplexFilter($value);
761 19
            } elseif ($value === $this->nullValue) {
762 4
                $result[$key] = null;
763
            } else {
764 15
                $result[$key] = $value;
765
            }
766
        }
767 19
        return $result;
768
    }
769
770
    // Property access:
771
772
    /**
773
     * {@inheritdoc}
774
     */
775
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
776
    {
777
        if ($name === $this->filterAttributeName) {
778
            return true;
779
        }
780
        return parent::canGetProperty($name, $checkVars, $checkBehaviors);
781
    }
782
783
    /**
784
     * {@inheritdoc}
785
     */
786
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
787
    {
788
        if ($name === $this->filterAttributeName) {
789
            return true;
790
        }
791
        return parent::canSetProperty($name, $checkVars, $checkBehaviors);
792
    }
793
794
    /**
795
     * {@inheritdoc}
796
     */
797 28
    public function __get($name)
798
    {
799 28
        if ($name === $this->filterAttributeName) {
800 28
            return $this->getFilter();
801
        }
802
803
        return parent::__get($name);
804
    }
805
806
    /**
807
     * {@inheritdoc}
808
     */
809 42
    public function __set($name, $value)
810
    {
811 42
        if ($name === $this->filterAttributeName) {
812 42
            $this->setFilter($value);
813
        } else {
814
            parent::__set($name, $value);
815
        }
816
    }
817
818
    /**
819
     * {@inheritdoc}
820
     */
821
    public function __isset($name)
822
    {
823
        if ($name === $this->filterAttributeName) {
824
            return $this->getFilter() !== null;
825
        }
826
827
        return parent::__isset($name);
828
    }
829
830
    /**
831
     * {@inheritdoc}
832
     */
833
    public function __unset($name)
834
    {
835
        if ($name === $this->filterAttributeName) {
836
            $this->setFilter(null);
837
        } else {
838
            parent::__unset($name);
839
        }
840
    }
841
}
842