Completed
Push — master ( b7a96a...376006 )
by Alexander
40:44 queued 34:50
created

DataFilter::normalize()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.0729

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 6
cts 7
cp 0.8571
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 7
nc 3
nop 1
crap 5.0729
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\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
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\data\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
    /**
241
     * @var mixed raw filter specification.
242
     */
243
    private $_filter;
244
    /**
245
     * @var Model|array|string|callable model to be used for filter attributes validation.
246
     */
247
    private $_searchModel;
248
    /**
249
     * @var array list of search attribute types in format: attributeName => type
250
     */
251
    private $_searchAttributeTypes;
252
253
254
    /**
255
     * @return mixed raw filter value.
256
     */
257 28
    public function getFilter()
258
    {
259 28
        return $this->_filter;
260
    }
261
262
    /**
263
     * @param mixed $filter raw filter value.
264
     */
265 28
    public function setFilter($filter)
266
    {
267 28
        $this->_filter = $filter;
268 28
    }
269
270
    /**
271
     * @return Model model instance.
272
     * @throws InvalidConfigException on invalid configuration.
273
     */
274 15
    public function getSearchModel()
275
    {
276 15
        if (!is_object($this->_searchModel) || $this->_searchModel instanceof \Closure) {
277 1
            $model = Yii::createObject($this->_searchModel);
278 1
            if (!$model instanceof Model) {
279
                throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
280
            }
281 1
            $this->_searchModel = $model;
282
        }
283 15
        return $this->_searchModel;
284
    }
285
286
    /**
287
     * @param Model|array|string|callable $model model instance or its DI compatible configuration.
288
     * @throws InvalidConfigException on invalid configuration.
289
     */
290 28
    public function setSearchModel($model)
291
    {
292 28
        if (is_object($model) && !$model instanceof Model && !$model instanceof \Closure) {
293 1
            throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.');
294
        }
295 28
        $this->_searchModel = $model;
296 28
    }
297
298
    /**
299
     * @return array search attribute type map.
300
     */
301 14
    public function getSearchAttributeTypes()
302
    {
303 14
        if ($this->_searchAttributeTypes === null) {
304 14
            $this->_searchAttributeTypes = $this->detectSearchAttributeTypes();
305
        }
306 14
        return $this->_searchAttributeTypes;
307
    }
308
309
    /**
310
     * @param array|null $searchAttributeTypes search attribute type map.
311
     */
312
    public function setSearchAttributeTypes($searchAttributeTypes)
313
    {
314
        $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...
315
    }
316
317
    /**
318
     * Composes default value for [[searchAttributeTypes]] from the [[searchModel]] validation rules.
319
     * @return array attribute type map.
320
     */
321 14
    protected function detectSearchAttributeTypes()
322
    {
323 14
        $model = $this->getSearchModel();
324
325 14
        $attributeTypes = [];
326 14
        foreach ($model->activeAttributes() as $attribute) {
327 14
            $attributeTypes[$attribute] = self::TYPE_STRING;
328
        }
329
330 14
        foreach ($model->getValidators() as $validator) {
331 14
            $type = null;
332 14
            if ($validator instanceof BooleanValidator) {
333
                $type = self::TYPE_BOOLEAN;
334 14
            } elseif ($validator instanceof NumberValidator) {
335 13
                $type = $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT;
336 14
            } elseif ($validator instanceof StringValidator) {
337 14
                $type = self::TYPE_STRING;
338 14
            } elseif ($validator instanceof EachValidator) {
339 13
                $type = self::TYPE_ARRAY;
340
            }
341
342 14
            if ($type !== null) {
343 14
                foreach ((array)$validator->attributes as $attribute) {
344 14
                    $attributeTypes[$attribute] = $type;
345
                }
346
            }
347
        }
348
349 14
        return $attributeTypes;
350
    }
351
352
    /**
353
     * @return array error messages in format `[errorKey => message]`.
354
     */
355 5
    public function getErrorMessages()
356
    {
357 5
        if (!is_array($this->_errorMessages)) {
358 5
            if ($this->_errorMessages === null) {
359 4
                $this->_errorMessages = $this->defaultErrorMessages();
360
            } else {
361 1
                $this->_errorMessages = array_merge(
362 1
                    $this->defaultErrorMessages(),
363 1
                    call_user_func($this->_errorMessages)
364
                );
365
            }
366
        }
367 5
        return $this->_errorMessages;
368
    }
369
370
    /**
371
     * Sets the list of error messages responding to invalid filter structure, in format: `[errorKey => message]`.
372
     * Message may contain placeholders that will be populated depending on the message context.
373
     * For each message a `{filter}` placeholder is available referring to the label for [[filterAttributeName]] attribute.
374
     * @param array|\Closure $errorMessages error messages in `[errorKey => message]` format, or a PHP callback returning them.
375
     */
376 1
    public function setErrorMessages($errorMessages)
377
    {
378 1
        if (is_array($errorMessages)) {
379 1
            $errorMessages = array_merge($this->defaultErrorMessages(), $errorMessages);
380
        }
381 1
        $this->_errorMessages = $errorMessages;
382 1
    }
383
384
    /**
385
     * Returns default values for [[errorMessages]].
386
     * @return array default error messages in `[errorKey => message]` format.
387
     */
388 5
    protected function defaultErrorMessages()
389
    {
390
        return [
391 5
            'invalidFilter' => Yii::t('yii', 'The format of {filter} is invalid.'),
392 5
            'operatorRequireMultipleOperands' => Yii::t('yii', "Operator '{operator}' requires multiple operands."),
393 5
            'unknownAttribute' => Yii::t('yii', "Unknown filter attribute '{attribute}'"),
394 5
            'invalidAttributeValueFormat' => Yii::t('yii', "Condition for '{attribute}' should be either a value or valid operator specification."),
395 5
            'operatorRequireAttribute' => Yii::t('yii', "Operator '{operator}' must be used with a search attribute."),
396 5
            'unsupportedOperatorType' => Yii::t('yii', "'{attribute}' does not support operator '{operator}'."),
397
        ];
398
    }
399
400
    /**
401
     * Parses content of the message from [[errorMessages]], specified by message key.
402
     * @param string $messageKey message key.
403
     * @param array $params params to be parsed into the message.
404
     * @return string composed message string.
405
     */
406 4
    protected function parseErrorMessage($messageKey, $params = [])
407
    {
408 4
        $messages = $this->getErrorMessages();
409 4
        if (isset($messages[$messageKey])) {
410 4
            $message = $messages[$messageKey];
411
        } else {
412
            $message = Yii::t('yii', 'The format of {filter} is invalid.');
413
        }
414
415 4
        $params = array_merge(
416
            [
417 4
                'filter' => $this->getAttributeLabel($this->filterAttributeName)
418
            ],
419 4
            $params
420
        );
421
422 4
        return Yii::$app->getI18n()->format($message, $params, Yii::$app->language);
423
    }
424
425
    // Model specific:
426
427
    /**
428
     * @inheritdoc
429
     */
430
    public function attributes()
431
    {
432
        return [
433
            $this->filterAttributeName
434
        ];
435
    }
436
437
    /**
438
     * @inheritdoc
439
     */
440 1
    public function formName()
441
    {
442 1
        return '';
443
    }
444
445
    /**
446
     * @inheritdoc
447
     */
448 21
    public function rules()
449
    {
450
        return [
451 21
            [$this->filterAttributeName, 'validateFilter', 'skipOnEmpty' => false]
452
        ];
453
    }
454
455
    /**
456
     * @inheritdoc
457
     */
458 4
    public function attributeLabels()
459
    {
460
        return [
461 4
            $this->filterAttributeName => $this->filterAttributeLabel,
462
        ];
463
    }
464
465
    // Validation:
466
467
    /**
468
     * Validates filter attribute value to match filer condition specification.
469
     */
470 20
    public function validateFilter()
471
    {
472 20
        $value = $this->getFilter();
473 20
        if ($value !== null) {
474 19
            $this->validateCondition($value);
475
        }
476 20
    }
477
478
    /**
479
     * Validates filter condition.
480
     * @param mixed $condition raw filter condition.
481
     */
482 19
    protected function validateCondition($condition)
483
    {
484 19
        if (!is_array($condition)) {
485 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidFilter'));
486 1
            return;
487
        }
488
489 18
        if (empty($condition)) {
490 2
            return;
491
        }
492
493 16
        foreach ($condition as $key => $value) {
494 16
            $method = 'validateAttributeCondition';
495 16
            if (isset($this->filterControls[$key])) {
496 9
                $controlKey = $this->filterControls[$key];
497 9
                if (isset($this->conditionValidators[$controlKey])) {
498 9
                    $method = $this->conditionValidators[$controlKey];
499
                }
500
            }
501 16
            $this->$method($key, $value);
502
        }
503 16
    }
504
505
    /**
506
     * Validates conjunction condition that consists of multiple independent ones.
507
     * This covers such operators as `and` and `or`.
508
     * @param string $operator raw operator control keyword.
509
     * @param mixed $condition raw condition.
510
     */
511 6
    protected function validateConjunctionCondition($operator, $condition)
512
    {
513 6
        if (!is_array($condition) || !ArrayHelper::isIndexed($condition)) {
514 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
515 1
            return;
516
        }
517
518 5
        foreach ($condition as $part) {
519 5
            $this->validateCondition($part);
520
        }
521 5
    }
522
523
    /**
524
     * Validates block condition that consists of a single condition.
525
     * This covers such operators as `not`.
526
     * @param string $operator raw operator control keyword.
527
     * @param mixed $condition raw condition.
528
     */
529 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...
530
    {
531 3
        $this->validateCondition($condition);
532 3
    }
533
534
    /**
535
     * Validates search condition for a particular attribute.
536
     * @param string $attribute search attribute name.
537
     * @param mixed $condition search condition.
538
     */
539 14
    protected function validateAttributeCondition($attribute, $condition)
540
    {
541 14
        $attributeTypes = $this->getSearchAttributeTypes();
542 14
        if (!isset($attributeTypes[$attribute])) {
543 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
544 1
            return;
545
        }
546
547 14
        if (is_array($condition)) {
548 7
            $operatorCount = 0;
549 7
            foreach ($condition as $rawOperator => $value) {
550 7
                if (isset($this->filterControls[$rawOperator])) {
551 6
                    $operator = $this->filterControls[$rawOperator];
552 6
                    if (isset($this->operatorTypes[$operator])) {
553 6
                        $operatorCount++;
554 7
                        $this->validateOperatorCondition($rawOperator, $value, $attribute);
555
                    }
556
                }
557
            }
558
559 7
            if ($operatorCount > 0) {
560 6
                if ($operatorCount < count($condition)) {
561 6
                    $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidAttributeValueFormat', ['attribute' => $attribute]));
562
                }
563
            } else {
564
                // attribute may allow array value:
565 7
                $this->validateAttributeValue($attribute, $condition);
566
            }
567
        } else {
568 10
            $this->validateAttributeValue($attribute, $condition);
569
        }
570 14
    }
571
572
    /**
573
     * Validates operator condition.
574
     * @param string $operator raw operator control keyword.
575
     * @param mixed $condition attribute condition.
576
     * @param string $attribute attribute name.
577
     */
578 7
    protected function validateOperatorCondition($operator, $condition, $attribute = null)
579
    {
580 7
        if ($attribute === null) {
581
            // absence of an attribute indicates that operator has been placed in a wrong position
582 1
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireAttribute', ['operator' => $operator]));
583 1
            return;
584
        }
585
586 6
        $internalOperator = $this->filterControls[$operator];
587
588
        // check operator type :
589 6
        $operatorTypes = $this->operatorTypes[$internalOperator];
590 6
        if ($operatorTypes !== '*') {
591 3
            $attributeTypes = $this->getSearchAttributeTypes();
592 3
            $attributeType = $attributeTypes[$attribute];
593 3
            if (!in_array($attributeType, $operatorTypes, true)) {
594
                $this->addError($this->filterAttributeName, $this->parseErrorMessage('unsupportedOperatorType', ['attribute' => $attribute, 'operator' => $operator]));
595
                return;
596
            }
597
        }
598
599 6
        if (in_array($internalOperator, $this->multiValueOperators, true)) {
600
            // multi-value operator:
601 3
            if (!is_array($condition)) {
602
                $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator]));
603
            } else {
604 3
                foreach ($condition as $v) {
605 3
                    $this->validateAttributeValue($attribute, $v);
606
                }
607
            }
608
        } else {
609
            // single-value operator :
610 4
            $this->validateAttributeValue($attribute, $condition);
611
        }
612 6
    }
613
614
    /**
615
     * Validates attribute value in the scope of [[model]].
616
     * @param string $attribute attribute name.
617
     * @param mixed $value attribute value.
618
     */
619 14
    protected function validateAttributeValue($attribute, $value)
620
    {
621 14
        $model = $this->getSearchModel();
622 14
        if (!$model->isAttributeSafe($attribute)) {
623
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
624
            return;
625
        }
626
627 14
        $model->{$attribute} = $value;
628 14
        if (!$model->validate([$attribute])) {
629 1
            $this->addError($this->filterAttributeName, $model->getFirstError($attribute));
630 1
            return;
631
        }
632 13
    }
633
634
    /**
635
     * Validates attribute value in the scope of [[searchModel]], applying attribute value filters if any.
636
     * @param string $attribute attribute name.
637
     * @param mixed $value attribute value.
638
     * @return mixed filtered attribute value.
639
     */
640 6
    protected function filterAttributeValue($attribute, $value)
641
    {
642 6
        $model = $this->getSearchModel();
643 6
        if (!$model->isAttributeSafe($attribute)) {
644
            $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute]));
645
            return $value;
646
        }
647 6
        $model->{$attribute} = $value;
648 6
        if (!$model->validate([$attribute])) {
649 1
            $this->addError($this->filterAttributeName, $model->getFirstError($attribute));
650 1
            return $value;
651
        }
652
653 5
        return $model->{$attribute};
654
    }
655
656
    // Build:
657
658
    /**
659
     * Builds actual filter specification form [[filter]] value.
660
     * @param boolean $runValidation whether to perform validation (calling [[validate()]])
661
     * before building the filter. Defaults to `true`. If the validation fails, no filter will
662
     * be built and this method will return `false`.
663
     * @return mixed|false built actual filter value, or `false` if validation fails.
664
     */
665 8
    public function build($runValidation = true)
666
    {
667 8
        if ($runValidation && !$this->validate()) {
668
            return false;
669
        }
670 8
        return $this->buildInternal();
671
    }
672
673
    /**
674
     * Performs actual filter build.
675
     * By default this method returns result of [[normalize()]].
676
     * The child class may override this method providing more specific implementation.
677
     * @return mixed built actual filter value.
678
     */
679
    protected function buildInternal()
680
    {
681
        return $this->normalize(false);
682
    }
683
684
    /**
685
     * Normalizes filter value, replacing raw keys according to [[filterControls]] and [[attributeMap]].
686
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
687
     * before normalizing the filter. Defaults to `true`. If the validation fails, no filter will
688
     * be processed and this method will return `false`.
689
     * @return array|bool normalized filter value, or `false` if validation fails.
690
     */
691 15
    public function normalize($runValidation = true)
692
    {
693 15
        if ($runValidation && !$this->validate()) {
694
            return false;
695
        }
696
697 15
        $filter = $this->getFilter();
698 15
        if (!is_array($filter) || empty($filter)) {
699 4
            return [];
700
        }
701
702 11
        return $this->normalizeComplexFilter($filter);
703
    }
704
705
    /**
706
     * Normalizes complex filter recursively.
707
     * @param array $filter raw filter.
708
     * @return array normalized filter.
709
     */
710 11
    private function normalizeComplexFilter(array $filter)
711
    {
712 11
        $result = [];
713 11
        foreach ($filter as $key => $value) {
714 11
            if (isset($this->filterControls[$key])) {
715 7
                $key = $this->filterControls[$key];
716 11
            } elseif (isset($this->attributeMap[$key])) {
717 1
                $key = $this->attributeMap[$key];
718
            }
719 11
            if (is_array($value)) {
720 7
                $result[$key] = $this->normalizeComplexFilter($value);
721
            } else {
722 11
                $result[$key] = $value;
723
            }
724
        }
725 11
        return $result;
726
    }
727
728
    // Property access:
729
730
    /**
731
     * @inheritdoc
732
     */
733
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
734
    {
735
        if ($name === $this->filterAttributeName) {
736
            return true;
737
        }
738
        return parent::canGetProperty($name, $checkVars, $checkBehaviors);
739
    }
740
741
    /**
742
     * @inheritdoc
743
     */
744
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
745
    {
746
        if ($name === $this->filterAttributeName) {
747
            return true;
748
        }
749
        return parent::canSetProperty($name, $checkVars, $checkBehaviors);
750
    }
751
752
    /**
753
     * @inheritdoc
754
     */
755
    public function __get($name)
756
    {
757
        if ($name === $this->filterAttributeName) {
758
            return $this->getFilter();
759
        }
760
761
        return parent::__get($name);
762
    }
763
764
    /**
765
     * @inheritdoc
766
     */
767 28
    public function __set($name, $value)
768
    {
769 28
        if ($name === $this->filterAttributeName) {
770 28
            $this->setFilter($value);
771
        } else {
772
            parent::__set($name, $value);
773
        }
774 28
    }
775
776
    /**
777
     * @inheritdoc
778
     */
779
    public function __isset($name)
780
    {
781
        if ($name === $this->filterAttributeName) {
782
            return $this->getFilter() !== null;
783
        }
784
785
        return parent::__isset($name);
786
    }
787
788
    /**
789
     * @inheritdoc
790
     */
791
    public function __unset($name)
792
    {
793
        if ($name === $this->filterAttributeName) {
794
            $this->setFilter(null);
795
        } else {
796
            parent::__unset($name);
797
        }
798
    }
799
}
800