GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( f5c98f...019932 )
by Robert
14:58
created

DataFilter::validateConjunctionCondition()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 2
crap 4
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