Completed
Push — master ( c9de61...744b87 )
by Alexander
41:24 queued 38:21
created

DataFilter::validateAttributeCondition()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 19
cts 19
cp 1
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 14
nop 2
crap 8
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\rest;
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
19
/**
20
 * DataFilter is a special [[Model]] for processing query filtering specification.
21
 * It allows validating and building a filter condition passed via request.
22
 *
23
 * Filter example:
24
 *
25
 * ```json
26
 * {
27
 *     "or": [
28
 *         {
29
 *             "and": [
30
 *                 {
31
 *                     "name": "some name",
32
 *                 },
33
 *                 {
34
 *                     "price": "25",
35
 *                 }
36
 *             ]
37
 *         },
38
 *         {
39
 *             "id": {"in": [2, 5, 9]},
40
 *             "price": {
41
 *                 "gt": 10,
42
 *                 "lt": 50
43
 *             }
44
 *         }
45
 *     ]
46
 * }
47
 * ```
48
 *
49
 * In the request the filter should be specified using a key name equal to [[filterAttributeName]]. Thus actual HTTP request body
50
 * will look like following:
51
 *
52
 * ```json
53
 * {
54
 *     "filter": {"or": {...}},
55
 *     "page": 2,
56
 *     ...
57
 * }
58
 * ```
59
 *
60
 * Raw filter value should be assigned to [[filter]] property of the model.
61
 * You may populate it from request data via [[load()]] method:
62
 *
63
 * ```php
64
 * use yii\rest\DataFilter;
65
 *
66
 * $dataFilter = new DataFilter();
67
 * $dataFilter->load(Yii::$app->request->getBodyParams());
68
 * ```
69
 *
70
 * In order to function this class requires a search model specified via [[searchModel]]. This search model should declare
71
 * all available search attributes and their validation rules. For example:
72
 *
73
 * ```php
74
 * class SearchModel extends \yii\base\Model
75
 * {
76
 *     public $id;
77
 *     public $name;
78
 *
79
 *     public function rules()
80
 *     {
81
 *         return [
82
 *             [['id', 'name'], 'trim'],
83
 *             ['id', 'integer'],
84
 *             ['name', 'string'],
85
 *         ];
86
 *     }
87
 * }
88
 * ```
89
 *
90
 * In order to reduce amount of classes, you may use [[\yii\base\DynamicModel]] instance as a [[searchModel]].
91
 * In this case you should specify [[searchModel]] using a PHP callable:
92
 *
93
 * ```php
94
 * function () {
95
 *     return (new \yii\base\DynamicModel(['id' => null, 'name' => null]))
96
 *         ->addRule(['id', 'name'], 'trim')
97
 *         ->addRule('id', 'integer')
98
 *         ->addRule('name', 'string');
99
 * }
100
 * ```
101
 *
102
 * You can use [[validate()]] method to check if filter value is valid. If validation fails you can use
103
 * [[getErrors()]] to get actual error messages.
104
 *
105
 * In order to acquire filter condition suitable for fetching data use [[build()]] method.
106
 *
107
 * > Note: This is a base class. Its implementation of [[build()]] simply returns normalized [[filter]] value.
108
 * In order to convert filter to particular format you should use descendant of this class that implements
109
 * [[buildInternal()]] method accordingly.
110
 *
111
 * @see ActiveDataFilter
112
 *
113
 * @property mixed $filter filter value.
114
 * @property Model $searchModel model to be used for filter attributes validation.
115
 * @property array $searchAttributeTypes search attribute type map.
116
 * @property array $errorMessages list of error messages responding to invalid filter structure,
117
 * in format: `[errorKey => message]`. Please refer to [[setErrorMessages()]] for details.
118
 *
119
 * @author Paul Klimov <[email protected]>
120
 * @since 2.0.13
121
 */
122
class DataFilter extends Model
123
{
124
    const TYPE_INTEGER = 'integer';
125
    const TYPE_FLOAT = 'float';
126
    const TYPE_BOOLEAN = 'boolean';
127
    const TYPE_STRING = 'string';
128
    const TYPE_ARRAY = 'array';
129
130
    /**
131
     * @var string name of the attribute that handles filter value.
132
     * The name is used to load data via [[load()]] method.
133
     */
134
    public $filterAttributeName = 'filter';
135
    /**
136
     * @var string label for the filter attribute specified via [[filterAttributeName]].
137
     * It will be used during error messages composition.
138
     */
139
    public $filterAttributeLabel;
140
    /**
141
     * @var array keywords or expressions that could be used in a filter.
142
     * Array keys are the expressions used in raw filter value obtained from user request.
143
     * Array values are internal build keys used in this class methods.
144
     *
145
     * Any unspecified keyword will not be recognized as a filter control and will be treated as
146
     * an attribute name. Thus you should avoid conflicts between control keywords and attribute names.
147
     * For example: in case you have control keyword 'like' and an attribute named 'like', specifying condition
148
     * for such attribute will be impossible.
149
     *
150
     * You may specify several keywords for the same filter build key, creating multiple aliases. For example:
151
     *
152
     * ```php
153
     * [
154
     *     'eq' => '=',
155
     *     '=' => '=',
156
     *     '==' => '=',
157
     *     '===' => '=',
158
     *     // ...
159
     * ]
160
     * ```
161
     *
162
     * > Note: while specifying filter controls take actual data exchange format, which your API uses, in mind.
163
     *   Make sure each specified control keyword is valid for the format. For example, in XML tag name can start
164
     *   only with a letter character, thus controls like `>`, '=' or `$gt` will break the XML schema.
165
     */
166
    public $filterControls = [
167
        'and' => 'AND',
168
        'or' => 'OR',
169
        'not' => 'NOT',
170
        'lt' => '<',
171
        'gt' => '>',
172
        'lte' => '<=',
173
        'gte' => '>=',
174
        'eq' => '=',
175
        'neq' => '!=',
176
        'in' => 'IN',
177
        'nin' => 'NOT IN',
178
        'like' => 'LIKE',
179
    ];
180
    /**
181
     * @var array maps filter condition keywords to validation methods.
182
     * These methods are used by [[validateCondition()]] to validate raw filter conditions.
183
     */
184
    public $conditionValidators = [
185
        'AND' => 'validateConjunctionCondition',
186
        'OR' => 'validateConjunctionCondition',
187
        'NOT' => 'validateBlockCondition',
188
        '<' => 'validateOperatorCondition',
189
        '>' => 'validateOperatorCondition',
190
        '<=' => 'validateOperatorCondition',
191
        '>=' => 'validateOperatorCondition',
192
        '=' => 'validateOperatorCondition',
193
        '!=' => 'validateOperatorCondition',
194
        'IN' => 'validateOperatorCondition',
195
        'NOT IN' => 'validateOperatorCondition',
196
        'LIKE' => 'validateOperatorCondition',
197
    ];
198
    /**
199
     * @var array specifies the list of supported search attribute types per each operator.
200
     * This field should be in format: 'operatorKeyword' => ['type1', 'type2' ...].
201
     * Supported types list can be specified as `*`, which indicates that operator supports all types available.
202
     * Any unspecified keyword will not be considered as a valid operator.
203
     */
204
    public $operatorTypes = [
205
        '<' => [self::TYPE_INTEGER, self::TYPE_FLOAT],
206
        '>' => [self::TYPE_INTEGER, self::TYPE_FLOAT],
207
        '<=' => [self::TYPE_INTEGER, self::TYPE_FLOAT],
208
        '>=' => [self::TYPE_INTEGER, self::TYPE_FLOAT],
209
        '=' => '*',
210
        '!=' => '*',
211
        'IN' => '*',
212
        'NOT IN' => '*',
213
        'LIKE' => [self::TYPE_STRING],
214
    ];
215
    /**
216
     * @var array list of operators keywords, which should accept multiple values.
217
     */
218
    public $multiValueOperators = [
219
        'IN',
220
        'NOT IN',
221
    ];
222
    /**
223
     * @var array actual attribute names to be used in searched condition, in format: [filterAttribute => actualAttribute].
224
     * For example, in case of using table joins in the search query, attribute map may look like the following:
225
     *
226
     * ```php
227
     * [
228
     *     'authorName' => '{{author}}.[[name]]'
229
     * ]
230
     * ```
231
     *
232
     * Attribute map will be applied to filter condition in [[normalize()]] method.
233
     */
234
    public $attributeMap = [];
235
236
    /**
237
     * @var array|\Closure list of error messages responding to invalid filter structure, in format: `[errorKey => message]`.
238
     */
239
    private $_errorMessages = [
240
        'invalidFilter' => 'The format of {filter} is invalid.',
241
        'operatorRequireMultipleOperands' => "Operator '{operator}' requires multiple operands.",
242
        'unknownAttribute' => "Unknown filter attribute '{attribute}'",
243
        'invalidAttributeValueFormat' => "Condition for '{attribute}' should be either a value or a valid operator specification.",
244
        'operatorRequireAttribute' => "Operator '{operator}' must be used with a search attribute.",
245
        'unsupportedOperatorType' => "'{attribute}' does not support '{operator}' operator.",
246
    ];
247
    /**
248
     * @var mixed raw filter specification.
249
     */
250
    private $_filter;
251
    /**
252
     * @var Model|array|string|callable model to be used for filter attributes validation.
253
     */
254
    private $_searchModel;
255
    /**
256
     * @var array list of search attribute types in format: attributeName => type
257
     */
258
    private $_searchAttributeTypes;
259
260
261
    /**
262
     * @return mixed raw filter value.
263
     */
264 28
    public function getFilter()
265
    {
266 28
        return $this->_filter;
267
    }
268
269
    /**
270
     * @param mixed $filter raw filter value.
271
     */
272 28
    public function setFilter($filter)
273
    {
274 28
        $this->_filter = $filter;
275 28
    }
276
277
    /**
278
     * @return Model model instance.
279
     * @throws InvalidConfigException on invalid configuration.
280
     */
281 15
    public function getSearchModel()
282
    {
283 15
        if (!is_object($this->_searchModel) || $this->_searchModel instanceof \Closure) {
284 1
            $model = Yii::createObject($this->_searchModel);
285 1
            if (!$model instanceof Model) {
286
                throw new InvalidConfigException('`' . get_class($this) . '::$model` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
287
            }
288 1
            $this->_searchModel = $model;
289
        }
290 15
        return $this->_searchModel;
291
    }
292
293
    /**
294
     * @param Model|array|string|callable $model model instance or its DI compatible configuration.
295
     * @throws InvalidConfigException on invalid configuration.
296
     */
297 28
    public function setSearchModel($model)
298
    {
299 28
        if (is_object($model) && !$model instanceof Model && !$model instanceof \Closure) {
300 1
            throw new InvalidConfigException('`' . get_class($this) . '::$model` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
301
        }
302 28
        $this->_searchModel = $model;
303 28
    }
304
305
    /**
306
     * @return array search attribute type map.
307
     */
308 14
    public function getSearchAttributeTypes()
309
    {
310 14
        if ($this->_searchAttributeTypes === null) {
311 14
            $this->_searchAttributeTypes = $this->detectSearchAttributeTypes();
312
        }
313 14
        return $this->_searchAttributeTypes;
314
    }
315
316
    /**
317
     * @param array|null $searchAttributeTypes search attribute type map.
318
     */
319
    public function setSearchAttributeTypes($searchAttributeTypes)
320
    {
321
        $this->_searchAttributeTypes = $searchAttributeTypes;
0 ignored issues
show
Documentation Bug introduced by
It seems like $searchAttributeTypes can be null. However, the property $_searchAttributeTypes is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
322
    }
323
324
    /**
325
     * Composes default value for [[searchAttributeTypes]] from the [[searchModel]] validation rules.
326
     * @return array attribute type map.
327
     */
328 14
    protected function detectSearchAttributeTypes()
329
    {
330 14
        $model = $this->getSearchModel();
331
332 14
        $attributeTypes = [];
333 14
        foreach ($model->activeAttributes() as $attribute) {
334 14
            $attributeTypes[$attribute] = self::TYPE_STRING;
335
        }
336
337 14
        foreach ($model->getValidators() as $validator) {
338 14
            $type = null;
339 14
            if ($validator instanceof BooleanValidator) {
340
                $type = self::TYPE_BOOLEAN;
341 14
            } elseif ($validator instanceof NumberValidator) {
342 13
                $type = $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT;
343 14
            } elseif ($validator instanceof StringValidator) {
344 14
                $type = self::TYPE_STRING;
345 14
            } elseif ($validator instanceof EachValidator) {
346 13
                $type = self::TYPE_ARRAY;
347
            }
348
349 14
            if ($type !== null) {
350 14
                foreach ((array)$validator->attributes as $attribute) {
351 14
                    $attributeTypes[$attribute] = $type;
352
                }
353
            }
354
        }
355
356 14
        return $attributeTypes;
357
    }
358
359
    /**
360
     * @return array error messages in format `[errorKey => message]`.
361
     */
362 5
    public function getErrorMessages()
363
    {
364 5
        if (!is_array($this->_errorMessages)) {
365 1
            if ($this->_errorMessages === null) {
366
                $this->_errorMessages = $this->defaultErrorMessages();
367
            } else {
368 1
                $this->_errorMessages = array_merge(
369 1
                    $this->defaultErrorMessages(),
370 1
                    call_user_func($this->_errorMessages)
371
                );
372
            }
373
        }
374 5
        return $this->_errorMessages;
375
    }
376
377
    /**
378
     * Sets the list of error messages responding to invalid filter structure, in format: `[errorKey => message]`.
379
     * Message may contain placeholders that will be populated depending on the message context.
380
     * For each message a `{filter}` placeholder is available referring to the label for [[filterAttributeName]] attribute.
381
     * @param array|\Closure $errorMessages error messages in `[errorKey => message]` format, or a PHP callback returning them.
382
     */
383 1
    public function setErrorMessages($errorMessages)
384
    {
385 1
        if (is_array($errorMessages)) {
386 1
            $errorMessages = array_merge($this->defaultErrorMessages(), $errorMessages);
387
        }
388 1
        $this->_errorMessages = $errorMessages;
389 1
    }
390
391
    /**
392
     * Returns default values for [[errorMessages]].
393
     * @return array default error messages in `[errorKey => message]` format.
394
     */
395 1
    protected function defaultErrorMessages()
396
    {
397
        return [
398 1
            'invalidFilter' => Yii::t('yii-rest', 'The format of {filter} is invalid.'),
399 1
            'operatorRequireMultipleOperands' => Yii::t('yii-rest', "Operator '{operator}' requires multiple operands."),
400 1
            'unknownAttribute' => Yii::t('yii-rest', "Unknown filter attribute '{attribute}'"),
401 1
            'invalidAttributeValueFormat' => Yii::t('yii-rest', "Condition for '{attribute}' should be either a value or valid operator specification."),
402 1
            'operatorRequireAttribute' => Yii::t('yii-rest', "Operator '{operator}' must be used with search attribute."),
403 1
            'unsupportedOperatorType' => Yii::t('yii-rest', "'{attribute}' does not support operator '{operator}'."),
404
        ];
405
    }
406
407
    /**
408
     * Parses content of the message from [[errorMessages]], specified by message key.
409
     * @param string $messageKey message key.
410
     * @param array $params params to be parsed into the message.
411
     * @return string composed message string.
412
     */
413 4
    protected function parseErrorMessage($messageKey, $params = [])
414
    {
415 4
        $messages = $this->getErrorMessages();
416 4
        if (isset($messages[$messageKey])) {
417 4
            $message = $messages[$messageKey];
418
        } else {
419
            $message = Yii::t('yii-rest', 'The format of {filter} is invalid.');
420
        }
421
422 4
        $params = array_merge(
423
            [
424 4
                'filter' => $this->getAttributeLabel($this->filterAttributeName)
425
            ],
426 4
            $params
427
        );
428
429 4
        return Yii::$app->getI18n()->format($message, $params, Yii::$app->language);
430
    }
431
432
    // Model specific:
433
434
    /**
435
     * @inheritdoc
436
     */
437
    public function attributes()
438
    {
439
        return [
440
            $this->filterAttributeName
441
        ];
442
    }
443
444
    /**
445
     * @inheritdoc
446
     */
447 1
    public function formName()
448
    {
449 1
        return '';
450
    }
451
452
    /**
453
     * @inheritdoc
454
     */
455 21
    public function rules()
456
    {
457
        return [
458 21
            [$this->filterAttributeName, 'validateFilter', 'skipOnEmpty' => false]
459
        ];
460
    }
461
462
    /**
463
     * @inheritdoc
464
     */
465 4
    public function attributeLabels()
466
    {
467
        return [
468 4
            $this->filterAttributeName => $this->filterAttributeLabel,
469
        ];
470
    }
471
472
    // Validation:
473
474
    /**
475
     * Validates filter attribute value to match filer condition specification.
476
     */
477 20
    public function validateFilter()
478
    {
479 20
        $value = $this->getFilter();
480 20
        if ($value !== null) {
481 19
            $this->validateCondition($value);
482
        }
483 20
    }
484
485
    /**
486
     * Validates filter condition.
487
     * @param mixed $condition raw filter condition.
488
     */
489 19
    protected function validateCondition($condition)
490
    {
491 19
        if (!is_array($condition)) {
492 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidFilter'));
493 1
            return;
494
        }
495
496 18
        if (empty($condition)) {
497 2
            return;
498
        }
499
500 16
        foreach ($condition as $key => $value) {
501 16
            $method = 'validateAttributeCondition';
502 16
            if (isset($this->filterControls[$key])) {
503 9
                $controlKey = $this->filterControls[$key];
504 9
                if (isset($this->conditionValidators[$controlKey])) {
505 9
                    $method = $this->conditionValidators[$controlKey];
506
                }
507
            }
508 16
            $this->$method($key, $value);
509
        }
510 16
    }
511
512
    /**
513
     * Validates conjunction condition that consists of multiple independent ones.
514
     * This covers such operators as `and` and `or`.
515
     * @param string $operator raw operator control keyword.
516
     * @param mixed $condition raw condition.
517
     */
518 6
    protected function validateConjunctionCondition($operator, $condition)
519
    {
520 6
        if (!is_array($condition) || !ArrayHelper::isIndexed($condition)) {
521 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
522 1
            return;
523
        }
524
525 5
        foreach ($condition as $part) {
526 5
            $this->validateCondition($part);
527
        }
528 5
    }
529
530
    /**
531
     * Validates block condition that consists of a single condition.
532
     * This covers such operators as `not`.
533
     * @param string $operator raw operator control keyword.
534
     * @param mixed $condition raw condition.
535
     */
536 3
    protected function validateBlockCondition($operator, $condition)
0 ignored issues
show
Unused Code introduced by
The parameter $operator is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
537
    {
538 3
        $this->validateCondition($condition);
539 3
    }
540
541
    /**
542
     * Validates search condition for a particular attribute.
543
     * @param string $attribute search attribute name.
544
     * @param mixed $condition search condition.
545
     */
546 14
    protected function validateAttributeCondition($attribute, $condition)
547
    {
548 14
        $attributeTypes = $this->getSearchAttributeTypes();
549 14
        if (!isset($attributeTypes[$attribute])) {
550 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
551 1
            return;
552
        }
553
554 14
        if (is_array($condition)) {
555 7
            $operatorCount = 0;
556 7
            foreach ($condition as $rawOperator => $value) {
557 7
                if (isset($this->filterControls[$rawOperator])) {
558 6
                    $operator = $this->filterControls[$rawOperator];
559 6
                    if (isset($this->operatorTypes[$operator])) {
560 6
                        $operatorCount++;
561 7
                        $this->validateOperatorCondition($rawOperator, $value, $attribute);
562
                    }
563
                }
564
            }
565
566 7
            if ($operatorCount > 0) {
567 6
                if ($operatorCount < count($condition)) {
568 6
                    $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidAttributeValueFormat', ['attribute' => $attribute]));
569
                }
570
            } else {
571
                // attribute may allow array value:
572 7
                $this->validateAttributeValue($attribute, $condition);
573
            }
574
        } else {
575 10
            $this->validateAttributeValue($attribute, $condition);
576
        }
577 14
    }
578
579
    /**
580
     * Validates operator condition.
581
     * @param string $operator raw operator control keyword.
582
     * @param mixed $condition attribute condition.
583
     * @param string $attribute attribute name.
584
     */
585 7
    protected function validateOperatorCondition($operator, $condition, $attribute = null)
586
    {
587 7
        if ($attribute === null) {
588
            // absence of an attribute indicates that operator has been placed in a wrong position
589 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireAttribute', ['operator' => $operator]));
590 1
            return;
591
        }
592
593 6
        $internalOperator = $this->filterControls[$operator];
594
595
        // check operator type :
596 6
        $operatorTypes = $this->operatorTypes[$internalOperator];
597 6
        if ($operatorTypes !== '*') {
598 3
            $attributeTypes = $this->getSearchAttributeTypes();
599 3
            $attributeType = $attributeTypes[$attribute];
600 3
            if (!in_array($attributeType, $operatorTypes, true)) {
601
                $this->addError($this->filterAttributeName, $this->parseErrorMessage('unsupportedOperatorType', ['attribute' => $attribute, 'operator' => $operator]));
602
                return;
603
            }
604
        }
605
606 6
        if (in_array($internalOperator, $this->multiValueOperators, true)) {
607
            // multi-value operator:
608 3
            if (!is_array($condition)) {
609
                $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
610
            } else {
611 3
                foreach ($condition as $v) {
612 3
                    $this->validateAttributeValue($attribute, $v);
613
                }
614
            }
615
        } else {
616
            // single-value operator :
617 4
            $this->validateAttributeValue($attribute, $condition);
618
        }
619 6
    }
620
621
    /**
622
     * Validates attribute value in the scope of [[model]].
623
     * @param string $attribute attribute name.
624
     * @param mixed $value attribute value.
625
     */
626 14
    protected function validateAttributeValue($attribute, $value)
627
    {
628 14
        $model = $this->getSearchModel();
629 14
        if (!$model->isAttributeSafe($attribute)) {
630
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
631
            return;
632
        }
633
634 14
        $model->{$attribute} = $value;
635 14
        if (!$model->validate([$attribute])) {
636 1
            $this->addError($this->filterAttributeName, $model->getFirstError($attribute));
637 1
            return;
638
        }
639 13
    }
640
641
    /**
642
     * Validates attribute value in the scope of [[searchModel]], applying attribute value filters if any.
643
     * @param string $attribute attribute name.
644
     * @param mixed $value attribute value.
645
     * @return mixed filtered attribute value.
646
     */
647 6
    protected function filterAttributeValue($attribute, $value)
648
    {
649 6
        $model = $this->getSearchModel();
650 6
        if (!$model->isAttributeSafe($attribute)) {
651
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
652
            return $value;
653
        }
654 6
        $model->{$attribute} = $value;
655 6
        if (!$model->validate([$attribute])) {
656 1
            $this->addError($this->filterAttributeName, $model->getFirstError($attribute));
657 1
            return $value;
658
        }
659
660 5
        return $model->{$attribute};
661
    }
662
663
    // Build:
664
665
    /**
666
     * Builds actual filter specification form [[filter]] value.
667
     * @param boolean $runValidation whether to perform validation (calling [[validate()]])
668
     * before building the filter. Defaults to `true`. If the validation fails, no filter will
669
     * be built and this method will return `false`.
670
     * @return mixed|false built actual filter value, or `false` if validation fails.
671
     */
672 8
    public function build($runValidation = true)
673
    {
674 8
        if ($runValidation && !$this->validate()) {
675
            return false;
676
        }
677 8
        return $this->buildInternal();
678
    }
679
680
    /**
681
     * Performs actual filter build.
682
     * By default this method returns result of [[normalize()]].
683
     * The child class may override this method providing more specific implementation.
684
     * @return mixed built actual filter value.
685
     */
686
    protected function buildInternal()
687
    {
688
        return $this->normalize(false);
689
    }
690
691
    /**
692
     * Normalizes filter value, replacing raw keys according to [[filterControls]] and [[attributeMap]].
693
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
694
     * before normalizing the filter. Defaults to `true`. If the validation fails, no filter will
695
     * be processed and this method will return `false`.
696
     * @return array|bool normalized filter value, or `false` if validation fails.
697
     */
698 15
    public function normalize($runValidation = true)
699
    {
700 15
        if ($runValidation && !$this->validate()) {
701
            return false;
702
        }
703
704 15
        $filter = $this->getFilter();
705 15
        if (!is_array($filter) || empty($filter)) {
706 4
            return [];
707
        }
708
709 11
        return $this->normalizeComplexFilter($filter);
710
    }
711
712
    /**
713
     * Normalizes complex filter recursively.
714
     * @param array $filter raw filter.
715
     * @return array normalized filter.
716
     */
717 11
    private function normalizeComplexFilter(array $filter)
718
    {
719 11
        $result = [];
720 11
        foreach ($filter as $key => $value) {
721 11
            if (isset($this->filterControls[$key])) {
722 7
                $key = $this->filterControls[$key];
723 11
            } elseif (isset($this->attributeMap[$key])) {
724 1
                $key = $this->attributeMap[$key];
725
            }
726 11
            if (is_array($value)) {
727 7
                $result[$key] = $this->normalizeComplexFilter($value);
728
            } else {
729 11
                $result[$key] = $value;
730
            }
731
        }
732 11
        return $result;
733
    }
734
735
    // Property access:
736
737
    /**
738
     * @inheritdoc
739
     */
740
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
741
    {
742
        if ($name === $this->filterAttributeName) {
743
            return true;
744
        }
745
        return parent::canGetProperty($name, $checkVars, $checkBehaviors);
746
    }
747
748
    /**
749
     * @inheritdoc
750
     */
751
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
752
    {
753
        if ($name === $this->filterAttributeName) {
754
            return true;
755
        }
756
        return parent::canSetProperty($name, $checkVars, $checkBehaviors);
757
    }
758
759
    /**
760
     * @inheritdoc
761
     */
762
    public function __get($name)
763
    {
764
        if ($name === $this->filterAttributeName) {
765
            return $this->getFilter();
766
        } else {
767
            return parent::__get($name);
768
        }
769
    }
770
771
    /**
772
     * @inheritdoc
773
     */
774 28
    public function __set($name, $value)
775
    {
776 28
        if ($name === $this->filterAttributeName) {
777 28
            $this->setFilter($value);
778
        } else {
779
            parent::__set($name, $value);
780
        }
781 28
    }
782
783
    /**
784
     * @inheritdoc
785
     */
786
    public function __isset($name)
787
    {
788
        if ($name === $this->filterAttributeName) {
789
            return $this->getFilter() !== null;
790
        } else {
791
            return parent::__isset($name);
792
        }
793
    }
794
795
    /**
796
     * @inheritdoc
797
     */
798
    public function __unset($name)
799
    {
800
        if ($name === $this->filterAttributeName) {
801
            $this->setFilter(null);
802
        } else {
803
            parent::__unset($name);
804
        }
805
    }
806
}
807