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.');
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) {
0 ignored issues
show
$model is always a sub-type of yii\base\Model.
Loading history...
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