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 | use yii\validators\DateValidator; |
||
19 | use yii\validators\Validator; |
||
20 | |||
21 | /** |
||
22 | * DataFilter is a special [[Model]] for processing query filtering specification. |
||
23 | * It allows validating and building a filter condition passed via request. |
||
24 | * |
||
25 | * Filter example: |
||
26 | * |
||
27 | * ```json |
||
28 | * { |
||
29 | * "or": [ |
||
30 | * { |
||
31 | * "and": [ |
||
32 | * { |
||
33 | * "name": "some name", |
||
34 | * }, |
||
35 | * { |
||
36 | * "price": "25", |
||
37 | * } |
||
38 | * ] |
||
39 | * }, |
||
40 | * { |
||
41 | * "id": {"in": [2, 5, 9]}, |
||
42 | * "price": { |
||
43 | * "gt": 10, |
||
44 | * "lt": 50 |
||
45 | * } |
||
46 | * } |
||
47 | * ] |
||
48 | * } |
||
49 | * ``` |
||
50 | * |
||
51 | * In the request the filter should be specified using a key name equal to [[filterAttributeName]]. Thus actual HTTP request body |
||
52 | * will look like following: |
||
53 | * |
||
54 | * ```json |
||
55 | * { |
||
56 | * "filter": {"or": {...}}, |
||
57 | * "page": 2, |
||
58 | * ... |
||
59 | * } |
||
60 | * ``` |
||
61 | * |
||
62 | * Raw filter value should be assigned to [[filter]] property of the model. |
||
63 | * You may populate it from request data via [[load()]] method: |
||
64 | * |
||
65 | * ```php |
||
66 | * use yii\data\DataFilter; |
||
67 | * |
||
68 | * $dataFilter = new DataFilter(); |
||
69 | * $dataFilter->load(Yii::$app->request->getBodyParams()); |
||
70 | * ``` |
||
71 | * |
||
72 | * In order to function this class requires a search model specified via [[searchModel]]. This search model should declare |
||
73 | * all available search attributes and their validation rules. For example: |
||
74 | * |
||
75 | * ```php |
||
76 | * class SearchModel extends \yii\base\Model |
||
77 | * { |
||
78 | * public $id; |
||
79 | * public $name; |
||
80 | * |
||
81 | * public function rules() |
||
82 | * { |
||
83 | * return [ |
||
84 | * [['id', 'name'], 'trim'], |
||
85 | * ['id', 'integer'], |
||
86 | * ['name', 'string'], |
||
87 | * ]; |
||
88 | * } |
||
89 | * } |
||
90 | * ``` |
||
91 | * |
||
92 | * In order to reduce amount of classes, you may use [[\yii\base\DynamicModel]] instance as a [[searchModel]]. |
||
93 | * In this case you should specify [[searchModel]] using a PHP callable: |
||
94 | * |
||
95 | * ```php |
||
96 | * function () { |
||
97 | * return (new \yii\base\DynamicModel(['id' => null, 'name' => null])) |
||
98 | * ->addRule(['id', 'name'], 'trim') |
||
99 | * ->addRule('id', 'integer') |
||
100 | * ->addRule('name', 'string'); |
||
101 | * } |
||
102 | * ``` |
||
103 | * |
||
104 | * You can use [[validate()]] method to check if filter value is valid. If validation fails you can use |
||
105 | * [[getErrors()]] to get actual error messages. |
||
106 | * |
||
107 | * In order to acquire filter condition suitable for fetching data use [[build()]] method. |
||
108 | * |
||
109 | * > Note: This is a base class. Its implementation of [[build()]] simply returns normalized [[filter]] value. |
||
110 | * In order to convert filter to particular format you should use descendant of this class that implements |
||
111 | * [[buildInternal()]] method accordingly. |
||
112 | * |
||
113 | * @see ActiveDataFilter |
||
114 | * |
||
115 | * @property array $errorMessages Error messages in format `[errorKey => message]`. Note that the type of this |
||
116 | * property differs in getter and setter. See [[getErrorMessages()]] and [[setErrorMessages()]] for details. |
||
117 | * @property mixed $filter Raw filter value. |
||
118 | * @property array $searchAttributeTypes Search attribute type map. Note that the type of this property |
||
119 | * differs in getter and setter. See [[getSearchAttributeTypes()]] and [[setSearchAttributeTypes()]] for details. |
||
120 | * @property Model $searchModel Model instance. Note that the type of this property differs in getter and |
||
121 | * setter. See [[getSearchModel()]] and [[setSearchModel()]] for details. |
||
122 | * |
||
123 | * @author Paul Klimov <[email protected]> |
||
124 | * @since 2.0.13 |
||
125 | */ |
||
126 | class DataFilter extends Model |
||
127 | { |
||
128 | const TYPE_INTEGER = 'integer'; |
||
129 | const TYPE_FLOAT = 'float'; |
||
130 | const TYPE_BOOLEAN = 'boolean'; |
||
131 | const TYPE_STRING = 'string'; |
||
132 | const TYPE_ARRAY = 'array'; |
||
133 | const TYPE_DATETIME = 'datetime'; |
||
134 | const TYPE_DATE = 'date'; |
||
135 | const TYPE_TIME = 'time'; |
||
136 | |||
137 | /** |
||
138 | * @var string name of the attribute that handles filter value. |
||
139 | * The name is used to load data via [[load()]] method. |
||
140 | */ |
||
141 | public $filterAttributeName = 'filter'; |
||
142 | /** |
||
143 | * @var string label for the filter attribute specified via [[filterAttributeName]]. |
||
144 | * It will be used during error messages composition. |
||
145 | */ |
||
146 | public $filterAttributeLabel; |
||
147 | /** |
||
148 | * @var array keywords or expressions that could be used in a filter. |
||
149 | * Array keys are the expressions used in raw filter value obtained from user request. |
||
150 | * Array values are internal build keys used in this class methods. |
||
151 | * |
||
152 | * Any unspecified keyword will not be recognized as a filter control and will be treated as |
||
153 | * an attribute name. Thus you should avoid conflicts between control keywords and attribute names. |
||
154 | * For example: in case you have control keyword 'like' and an attribute named 'like', specifying condition |
||
155 | * for such attribute will be impossible. |
||
156 | * |
||
157 | * You may specify several keywords for the same filter build key, creating multiple aliases. For example: |
||
158 | * |
||
159 | * ```php |
||
160 | * [ |
||
161 | * 'eq' => '=', |
||
162 | * '=' => '=', |
||
163 | * '==' => '=', |
||
164 | * '===' => '=', |
||
165 | * // ... |
||
166 | * ] |
||
167 | * ``` |
||
168 | * |
||
169 | * > Note: while specifying filter controls take actual data exchange format, which your API uses, in mind. |
||
170 | * > Make sure each specified control keyword is valid for the format. For example, in XML tag name can start |
||
171 | * > only with a letter character, thus controls like `>`, '=' or `$gt` will break the XML schema. |
||
172 | */ |
||
173 | public $filterControls = [ |
||
174 | 'and' => 'AND', |
||
175 | 'or' => 'OR', |
||
176 | 'not' => 'NOT', |
||
177 | 'lt' => '<', |
||
178 | 'gt' => '>', |
||
179 | 'lte' => '<=', |
||
180 | 'gte' => '>=', |
||
181 | 'eq' => '=', |
||
182 | 'neq' => '!=', |
||
183 | 'in' => 'IN', |
||
184 | 'nin' => 'NOT IN', |
||
185 | 'like' => 'LIKE', |
||
186 | ]; |
||
187 | /** |
||
188 | * @var array maps filter condition keywords to validation methods. |
||
189 | * These methods are used by [[validateCondition()]] to validate raw filter conditions. |
||
190 | */ |
||
191 | public $conditionValidators = [ |
||
192 | 'AND' => 'validateConjunctionCondition', |
||
193 | 'OR' => 'validateConjunctionCondition', |
||
194 | 'NOT' => 'validateBlockCondition', |
||
195 | '<' => 'validateOperatorCondition', |
||
196 | '>' => 'validateOperatorCondition', |
||
197 | '<=' => 'validateOperatorCondition', |
||
198 | '>=' => 'validateOperatorCondition', |
||
199 | '=' => 'validateOperatorCondition', |
||
200 | '!=' => 'validateOperatorCondition', |
||
201 | 'IN' => 'validateOperatorCondition', |
||
202 | 'NOT IN' => 'validateOperatorCondition', |
||
203 | 'LIKE' => 'validateOperatorCondition', |
||
204 | ]; |
||
205 | /** |
||
206 | * @var array specifies the list of supported search attribute types per each operator. |
||
207 | * This field should be in format: 'operatorKeyword' => ['type1', 'type2' ...]. |
||
208 | * Supported types list can be specified as `*`, which indicates that operator supports all types available. |
||
209 | * Any unspecified keyword will not be considered as a valid operator. |
||
210 | */ |
||
211 | public $operatorTypes = [ |
||
212 | '<' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], |
||
213 | '>' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], |
||
214 | '<=' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], |
||
215 | '>=' => [self::TYPE_INTEGER, self::TYPE_FLOAT, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], |
||
216 | '=' => '*', |
||
217 | '!=' => '*', |
||
218 | 'IN' => '*', |
||
219 | 'NOT IN' => '*', |
||
220 | 'LIKE' => [self::TYPE_STRING], |
||
221 | ]; |
||
222 | /** |
||
223 | * @var array list of operators keywords, which should accept multiple values. |
||
224 | */ |
||
225 | public $multiValueOperators = [ |
||
226 | 'IN', |
||
227 | 'NOT IN', |
||
228 | ]; |
||
229 | /** |
||
230 | * @var array actual attribute names to be used in searched condition, in format: [filterAttribute => actualAttribute]. |
||
231 | * For example, in case of using table joins in the search query, attribute map may look like the following: |
||
232 | * |
||
233 | * ```php |
||
234 | * [ |
||
235 | * 'authorName' => '{{author}}.[[name]]' |
||
236 | * ] |
||
237 | * ``` |
||
238 | * |
||
239 | * Attribute map will be applied to filter condition in [[normalize()]] method. |
||
240 | */ |
||
241 | public $attributeMap = []; |
||
242 | /** |
||
243 | * @var string representation of `null` instead of literal `null` in case the latter cannot be used. |
||
244 | * @since 2.0.40 |
||
245 | */ |
||
246 | public $nullValue = 'NULL'; |
||
247 | |||
248 | /** |
||
249 | * @var array|\Closure list of error messages responding to invalid filter structure, in format: `[errorKey => message]`. |
||
250 | */ |
||
251 | private $_errorMessages; |
||
252 | /** |
||
253 | * @var mixed raw filter specification. |
||
254 | */ |
||
255 | private $_filter; |
||
256 | /** |
||
257 | * @var Model|array|string|callable model to be used for filter attributes validation. |
||
258 | */ |
||
259 | private $_searchModel; |
||
260 | /** |
||
261 | * @var array list of search attribute types in format: attributeName => type |
||
262 | */ |
||
263 | private $_searchAttributeTypes; |
||
264 | |||
265 | |||
266 | /** |
||
267 | * @return mixed raw filter value. |
||
268 | */ |
||
269 | 41 | public function getFilter() |
|
270 | { |
||
271 | 41 | return $this->_filter; |
|
272 | } |
||
273 | |||
274 | /** |
||
275 | * @param mixed $filter raw filter value. |
||
276 | */ |
||
277 | 41 | public function setFilter($filter) |
|
278 | { |
||
279 | 41 | $this->_filter = $filter; |
|
280 | 41 | } |
|
281 | |||
282 | /** |
||
283 | * @return Model model instance. |
||
284 | * @throws InvalidConfigException on invalid configuration. |
||
285 | */ |
||
286 | 23 | public function getSearchModel() |
|
287 | { |
||
288 | 23 | if (!is_object($this->_searchModel) || $this->_searchModel instanceof \Closure) { |
|
289 | 1 | $model = Yii::createObject($this->_searchModel); |
|
290 | 1 | if (!$model instanceof Model) { |
|
291 | throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.'); |
||
0 ignored issues
–
show
|
|||
292 | } |
||
293 | 1 | $this->_searchModel = $model; |
|
294 | } |
||
295 | 23 | return $this->_searchModel; |
|
296 | } |
||
297 | |||
298 | /** |
||
299 | * @param Model|array|string|callable $model model instance or its DI compatible configuration. |
||
300 | * @throws InvalidConfigException on invalid configuration. |
||
301 | */ |
||
302 | 41 | public function setSearchModel($model) |
|
303 | { |
||
304 | 41 | if (is_object($model) && !$model instanceof Model && !$model instanceof \Closure) { |
|
305 | 1 | throw new InvalidConfigException('`' . get_class($this) . '::$searchModel` should be an instance of `' . Model::className() . '` or its DI compatible configuration.'); |
|
306 | } |
||
307 | 41 | $this->_searchModel = $model; |
|
308 | 41 | } |
|
309 | |||
310 | /** |
||
311 | * @return array search attribute type map. |
||
312 | */ |
||
313 | 22 | public function getSearchAttributeTypes() |
|
314 | { |
||
315 | 22 | if ($this->_searchAttributeTypes === null) { |
|
316 | 22 | $this->_searchAttributeTypes = $this->detectSearchAttributeTypes(); |
|
317 | } |
||
318 | 22 | return $this->_searchAttributeTypes; |
|
319 | } |
||
320 | |||
321 | /** |
||
322 | * @param array|null $searchAttributeTypes search attribute type map. |
||
323 | */ |
||
324 | public function setSearchAttributeTypes($searchAttributeTypes) |
||
325 | { |
||
326 | $this->_searchAttributeTypes = $searchAttributeTypes; |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * Composes default value for [[searchAttributeTypes]] from the [[searchModel]] validation rules. |
||
331 | * @return array attribute type map. |
||
332 | */ |
||
333 | 22 | protected function detectSearchAttributeTypes() |
|
334 | { |
||
335 | 22 | $model = $this->getSearchModel(); |
|
336 | |||
337 | 22 | $attributeTypes = []; |
|
338 | 22 | foreach ($model->activeAttributes() as $attribute) { |
|
339 | 22 | $attributeTypes[$attribute] = self::TYPE_STRING; |
|
340 | } |
||
341 | |||
342 | 22 | foreach ($model->getValidators() as $validator) { |
|
343 | 22 | $type = $this->detectSearchAttributeType($validator); |
|
344 | |||
345 | 22 | if ($type !== null) { |
|
346 | 22 | foreach ((array) $validator->attributes as $attribute) { |
|
347 | 22 | $attributeTypes[$attribute] = $type; |
|
348 | } |
||
349 | } |
||
350 | } |
||
351 | |||
352 | 22 | return $attributeTypes; |
|
353 | } |
||
354 | |||
355 | /** |
||
356 | * Detect attribute type from given validator. |
||
357 | * |
||
358 | * @param Validator $validator validator from which to detect attribute type. |
||
359 | * @return string|null detected attribute type. |
||
360 | * @since 2.0.14 |
||
361 | */ |
||
362 | 22 | protected function detectSearchAttributeType(Validator $validator) |
|
363 | { |
||
364 | 22 | if ($validator instanceof BooleanValidator) { |
|
365 | return self::TYPE_BOOLEAN; |
||
366 | } |
||
367 | |||
368 | 22 | if ($validator instanceof NumberValidator) { |
|
369 | 21 | return $validator->integerOnly ? self::TYPE_INTEGER : self::TYPE_FLOAT; |
|
370 | } |
||
371 | |||
372 | 22 | if ($validator instanceof StringValidator) { |
|
373 | 22 | return self::TYPE_STRING; |
|
374 | } |
||
375 | |||
376 | 22 | if ($validator instanceof EachValidator) { |
|
377 | 21 | return self::TYPE_ARRAY; |
|
378 | } |
||
379 | |||
380 | 22 | if ($validator instanceof DateValidator) { |
|
381 | 13 | if ($validator->type == DateValidator::TYPE_DATETIME) { |
|
382 | 13 | return self::TYPE_DATETIME; |
|
383 | } |
||
384 | |||
385 | if ($validator->type == DateValidator::TYPE_TIME) { |
||
386 | return self::TYPE_TIME; |
||
387 | } |
||
388 | return self::TYPE_DATE; |
||
389 | } |
||
390 | 9 | } |
|
391 | |||
392 | /** |
||
393 | * @return array error messages in format `[errorKey => message]`. |
||
394 | */ |
||
395 | 5 | public function getErrorMessages() |
|
396 | { |
||
397 | 5 | if (!is_array($this->_errorMessages)) { |
|
398 | 5 | if ($this->_errorMessages === null) { |
|
399 | 4 | $this->_errorMessages = $this->defaultErrorMessages(); |
|
400 | } else { |
||
401 | 1 | $this->_errorMessages = array_merge( |
|
402 | 1 | $this->defaultErrorMessages(), |
|
403 | 1 | call_user_func($this->_errorMessages) |
|
404 | ); |
||
405 | } |
||
406 | } |
||
407 | 5 | return $this->_errorMessages; |
|
408 | } |
||
409 | |||
410 | /** |
||
411 | * Sets the list of error messages responding to invalid filter structure, in format: `[errorKey => message]`. |
||
412 | * Message may contain placeholders that will be populated depending on the message context. |
||
413 | * For each message a `{filter}` placeholder is available referring to the label for [[filterAttributeName]] attribute. |
||
414 | * @param array|\Closure $errorMessages error messages in `[errorKey => message]` format, or a PHP callback returning them. |
||
415 | */ |
||
416 | 1 | public function setErrorMessages($errorMessages) |
|
417 | { |
||
418 | 1 | if (is_array($errorMessages)) { |
|
419 | 1 | $errorMessages = array_merge($this->defaultErrorMessages(), $errorMessages); |
|
420 | } |
||
421 | 1 | $this->_errorMessages = $errorMessages; |
|
422 | 1 | } |
|
423 | |||
424 | /** |
||
425 | * Returns default values for [[errorMessages]]. |
||
426 | * @return array default error messages in `[errorKey => message]` format. |
||
427 | */ |
||
428 | 5 | protected function defaultErrorMessages() |
|
429 | { |
||
430 | return [ |
||
431 | 5 | 'invalidFilter' => Yii::t('yii', 'The format of {filter} is invalid.'), |
|
432 | 5 | 'operatorRequireMultipleOperands' => Yii::t('yii', 'Operator "{operator}" requires multiple operands.'), |
|
433 | 5 | 'unknownAttribute' => Yii::t('yii', 'Unknown filter attribute "{attribute}"'), |
|
434 | 5 | 'invalidAttributeValueFormat' => Yii::t('yii', 'Condition for "{attribute}" should be either a value or valid operator specification.'), |
|
435 | 5 | 'operatorRequireAttribute' => Yii::t('yii', 'Operator "{operator}" must be used with a search attribute.'), |
|
436 | 5 | 'unsupportedOperatorType' => Yii::t('yii', '"{attribute}" does not support operator "{operator}".'), |
|
437 | ]; |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * Parses content of the message from [[errorMessages]], specified by message key. |
||
442 | * @param string $messageKey message key. |
||
443 | * @param array $params params to be parsed into the message. |
||
444 | * @return string composed message string. |
||
445 | */ |
||
446 | 4 | protected function parseErrorMessage($messageKey, $params = []) |
|
447 | { |
||
448 | 4 | $messages = $this->getErrorMessages(); |
|
449 | 4 | if (isset($messages[$messageKey])) { |
|
450 | 4 | $message = $messages[$messageKey]; |
|
451 | } else { |
||
452 | $message = Yii::t('yii', 'The format of {filter} is invalid.'); |
||
453 | } |
||
454 | |||
455 | 4 | $params = array_merge( |
|
456 | [ |
||
457 | 4 | 'filter' => $this->getAttributeLabel($this->filterAttributeName), |
|
458 | ], |
||
459 | 4 | $params |
|
460 | ); |
||
461 | |||
462 | 4 | return Yii::$app->getI18n()->format($message, $params, Yii::$app->language); |
|
463 | } |
||
464 | |||
465 | // Model specific: |
||
466 | |||
467 | /** |
||
468 | * {@inheritdoc} |
||
469 | */ |
||
470 | public function attributes() |
||
471 | { |
||
472 | return [ |
||
473 | $this->filterAttributeName, |
||
474 | ]; |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * {@inheritdoc} |
||
479 | */ |
||
480 | 1 | public function formName() |
|
481 | { |
||
482 | 1 | return ''; |
|
483 | } |
||
484 | |||
485 | /** |
||
486 | * {@inheritdoc} |
||
487 | */ |
||
488 | 29 | public function rules() |
|
489 | { |
||
490 | return [ |
||
491 | 29 | [$this->filterAttributeName, 'validateFilter', 'skipOnEmpty' => false], |
|
492 | ]; |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * {@inheritdoc} |
||
497 | */ |
||
498 | 4 | public function attributeLabels() |
|
499 | { |
||
500 | return [ |
||
501 | 4 | $this->filterAttributeName => $this->filterAttributeLabel, |
|
502 | ]; |
||
503 | } |
||
504 | |||
505 | // Validation: |
||
506 | |||
507 | /** |
||
508 | * Validates filter attribute value to match filer condition specification. |
||
509 | */ |
||
510 | 28 | public function validateFilter() |
|
511 | { |
||
512 | 28 | $value = $this->getFilter(); |
|
513 | 28 | if ($value !== null) { |
|
514 | 27 | $this->validateCondition($value); |
|
515 | } |
||
516 | 28 | } |
|
517 | |||
518 | /** |
||
519 | * Validates filter condition. |
||
520 | * @param mixed $condition raw filter condition. |
||
521 | */ |
||
522 | 27 | protected function validateCondition($condition) |
|
523 | { |
||
524 | 27 | if (!is_array($condition)) { |
|
525 | 1 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidFilter')); |
|
526 | 1 | return; |
|
527 | } |
||
528 | |||
529 | 26 | if (empty($condition)) { |
|
530 | 2 | return; |
|
531 | } |
||
532 | |||
533 | 24 | foreach ($condition as $key => $value) { |
|
534 | 24 | $method = 'validateAttributeCondition'; |
|
535 | 24 | if (isset($this->filterControls[$key])) { |
|
536 | 9 | $controlKey = $this->filterControls[$key]; |
|
537 | 9 | if (isset($this->conditionValidators[$controlKey])) { |
|
538 | 9 | $method = $this->conditionValidators[$controlKey]; |
|
539 | } |
||
540 | } |
||
541 | 24 | $this->$method($key, $value); |
|
542 | } |
||
543 | 24 | } |
|
544 | |||
545 | /** |
||
546 | * Validates conjunction condition that consists of multiple independent ones. |
||
547 | * This covers such operators as `and` and `or`. |
||
548 | * @param string $operator raw operator control keyword. |
||
549 | * @param mixed $condition raw condition. |
||
550 | */ |
||
551 | 6 | protected function validateConjunctionCondition($operator, $condition) |
|
552 | { |
||
553 | 6 | if (!is_array($condition) || !ArrayHelper::isIndexed($condition)) { |
|
554 | 1 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator])); |
|
555 | 1 | return; |
|
556 | } |
||
557 | |||
558 | 5 | foreach ($condition as $part) { |
|
559 | 5 | $this->validateCondition($part); |
|
560 | } |
||
561 | 5 | } |
|
562 | |||
563 | /** |
||
564 | * Validates block condition that consists of a single condition. |
||
565 | * This covers such operators as `not`. |
||
566 | * @param string $operator raw operator control keyword. |
||
567 | * @param mixed $condition raw condition. |
||
568 | */ |
||
569 | 3 | protected function validateBlockCondition($operator, $condition) |
|
570 | { |
||
571 | 3 | $this->validateCondition($condition); |
|
572 | 3 | } |
|
573 | |||
574 | /** |
||
575 | * Validates search condition for a particular attribute. |
||
576 | * @param string $attribute search attribute name. |
||
577 | * @param mixed $condition search condition. |
||
578 | */ |
||
579 | 22 | protected function validateAttributeCondition($attribute, $condition) |
|
580 | { |
||
581 | 22 | $attributeTypes = $this->getSearchAttributeTypes(); |
|
582 | 22 | if (!isset($attributeTypes[$attribute])) { |
|
583 | 1 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute])); |
|
584 | 1 | return; |
|
585 | } |
||
586 | |||
587 | 22 | if (is_array($condition)) { |
|
588 | 14 | $operatorCount = 0; |
|
589 | 14 | foreach ($condition as $rawOperator => $value) { |
|
590 | 14 | if (isset($this->filterControls[$rawOperator])) { |
|
591 | 12 | $operator = $this->filterControls[$rawOperator]; |
|
592 | 12 | if (isset($this->operatorTypes[$operator])) { |
|
593 | 12 | $operatorCount++; |
|
594 | 14 | $this->validateOperatorCondition($rawOperator, $value, $attribute); |
|
595 | } |
||
596 | } |
||
597 | } |
||
598 | |||
599 | 14 | if ($operatorCount > 0) { |
|
600 | 12 | if ($operatorCount < count($condition)) { |
|
601 | 12 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('invalidAttributeValueFormat', ['attribute' => $attribute])); |
|
602 | } |
||
603 | } else { |
||
604 | // attribute may allow array value: |
||
605 | 14 | $this->validateAttributeValue($attribute, $condition); |
|
606 | } |
||
607 | } else { |
||
608 | 12 | $this->validateAttributeValue($attribute, $condition); |
|
609 | } |
||
610 | 22 | } |
|
611 | |||
612 | /** |
||
613 | * Validates operator condition. |
||
614 | * @param string $operator raw operator control keyword. |
||
615 | * @param mixed $condition attribute condition. |
||
616 | * @param string $attribute attribute name. |
||
617 | */ |
||
618 | 13 | protected function validateOperatorCondition($operator, $condition, $attribute = null) |
|
619 | { |
||
620 | 13 | if ($attribute === null) { |
|
621 | // absence of an attribute indicates that operator has been placed in a wrong position |
||
622 | 1 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireAttribute', ['operator' => $operator])); |
|
623 | 1 | return; |
|
624 | } |
||
625 | |||
626 | 12 | $internalOperator = $this->filterControls[$operator]; |
|
627 | |||
628 | // check operator type : |
||
629 | 12 | $operatorTypes = $this->operatorTypes[$internalOperator]; |
|
630 | 12 | if ($operatorTypes !== '*') { |
|
631 | 6 | $attributeTypes = $this->getSearchAttributeTypes(); |
|
632 | 6 | $attributeType = $attributeTypes[$attribute]; |
|
633 | 6 | if (!in_array($attributeType, $operatorTypes, true)) { |
|
634 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('unsupportedOperatorType', ['attribute' => $attribute, 'operator' => $operator])); |
||
635 | return; |
||
636 | } |
||
637 | } |
||
638 | |||
639 | 12 | if (in_array($internalOperator, $this->multiValueOperators, true)) { |
|
640 | // multi-value operator: |
||
641 | 3 | if (!is_array($condition)) { |
|
642 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('operatorRequireMultipleOperands', ['operator' => $operator])); |
||
643 | } else { |
||
644 | 3 | foreach ($condition as $v) { |
|
645 | 3 | $this->validateAttributeValue($attribute, $v); |
|
646 | } |
||
647 | } |
||
648 | } else { |
||
649 | // single-value operator : |
||
650 | 10 | $this->validateAttributeValue($attribute, $condition); |
|
651 | } |
||
652 | 12 | } |
|
653 | |||
654 | /** |
||
655 | * Validates attribute value in the scope of [[model]]. |
||
656 | * @param string $attribute attribute name. |
||
657 | * @param mixed $value attribute value. |
||
658 | */ |
||
659 | 22 | protected function validateAttributeValue($attribute, $value) |
|
660 | { |
||
661 | 22 | $model = $this->getSearchModel(); |
|
662 | 22 | if (!$model->isAttributeSafe($attribute)) { |
|
663 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute])); |
||
664 | return; |
||
665 | } |
||
666 | |||
667 | 22 | $model->{$attribute} = $value === $this->nullValue ? null : $value; |
|
668 | 22 | if (!$model->validate([$attribute])) { |
|
669 | 1 | $this->addError($this->filterAttributeName, $model->getFirstError($attribute)); |
|
670 | 1 | return; |
|
671 | } |
||
672 | 21 | } |
|
673 | |||
674 | /** |
||
675 | * Validates attribute value in the scope of [[searchModel]], applying attribute value filters if any. |
||
676 | * @param string $attribute attribute name. |
||
677 | * @param mixed $value attribute value. |
||
678 | * @return mixed filtered attribute value. |
||
679 | */ |
||
680 | 8 | protected function filterAttributeValue($attribute, $value) |
|
681 | { |
||
682 | 8 | $model = $this->getSearchModel(); |
|
683 | 8 | if (!$model->isAttributeSafe($attribute)) { |
|
684 | $this->addError($this->filterAttributeName, $this->parseErrorMessage('unknownAttribute', ['attribute' => $attribute])); |
||
685 | return $value; |
||
686 | } |
||
687 | 8 | $model->{$attribute} = $value; |
|
688 | 8 | if (!$model->validate([$attribute])) { |
|
689 | 1 | $this->addError($this->filterAttributeName, $model->getFirstError($attribute)); |
|
690 | 1 | return $value; |
|
691 | } |
||
692 | |||
693 | 7 | return $model->{$attribute}; |
|
694 | } |
||
695 | |||
696 | // Build: |
||
697 | |||
698 | /** |
||
699 | * Builds actual filter specification form [[filter]] value. |
||
700 | * @param bool $runValidation whether to perform validation (calling [[validate()]]) |
||
701 | * before building the filter. Defaults to `true`. If the validation fails, no filter will |
||
702 | * be built and this method will return `false`. |
||
703 | * @return mixed|false built actual filter value, or `false` if validation fails. |
||
704 | */ |
||
705 | 10 | public function build($runValidation = true) |
|
706 | { |
||
707 | 10 | if ($runValidation && !$this->validate()) { |
|
708 | return false; |
||
709 | } |
||
710 | 10 | return $this->buildInternal(); |
|
711 | } |
||
712 | |||
713 | /** |
||
714 | * Performs actual filter build. |
||
715 | * By default this method returns result of [[normalize()]]. |
||
716 | * The child class may override this method providing more specific implementation. |
||
717 | * @return mixed built actual filter value. |
||
718 | */ |
||
719 | protected function buildInternal() |
||
720 | { |
||
721 | return $this->normalize(false); |
||
722 | } |
||
723 | |||
724 | /** |
||
725 | * Normalizes filter value, replacing raw keys according to [[filterControls]] and [[attributeMap]]. |
||
726 | * @param bool $runValidation whether to perform validation (calling [[validate()]]) |
||
727 | * before normalizing the filter. Defaults to `true`. If the validation fails, no filter will |
||
728 | * be processed and this method will return `false`. |
||
729 | * @return array|bool normalized filter value, or `false` if validation fails. |
||
730 | */ |
||
731 | 22 | public function normalize($runValidation = true) |
|
732 | { |
||
733 | 22 | if ($runValidation && !$this->validate()) { |
|
734 | return false; |
||
735 | } |
||
736 | |||
737 | 22 | $filter = $this->getFilter(); |
|
738 | 22 | if (!is_array($filter) || empty($filter)) { |
|
739 | 4 | return []; |
|
740 | } |
||
741 | |||
742 | 18 | return $this->normalizeComplexFilter($filter); |
|
743 | } |
||
744 | |||
745 | /** |
||
746 | * Normalizes complex filter recursively. |
||
747 | * @param array $filter raw filter. |
||
748 | * @return array normalized filter. |
||
749 | */ |
||
750 | 18 | private function normalizeComplexFilter(array $filter) |
|
751 | { |
||
752 | 18 | $result = []; |
|
753 | 18 | foreach ($filter as $key => $value) { |
|
754 | 18 | if (isset($this->filterControls[$key])) { |
|
755 | 8 | $key = $this->filterControls[$key]; |
|
756 | 18 | } elseif (isset($this->attributeMap[$key])) { |
|
757 | 1 | $key = $this->attributeMap[$key]; |
|
758 | } |
||
759 | 18 | if (is_array($value)) { |
|
760 | 9 | $result[$key] = $this->normalizeComplexFilter($value); |
|
761 | 18 | } elseif ($value === $this->nullValue) { |
|
762 | 4 | $result[$key] = null; |
|
763 | } else { |
||
764 | 18 | $result[$key] = $value; |
|
765 | } |
||
766 | } |
||
767 | 18 | return $result; |
|
768 | } |
||
769 | |||
770 | // Property access: |
||
771 | |||
772 | /** |
||
773 | * {@inheritdoc} |
||
774 | */ |
||
775 | public function canGetProperty($name, $checkVars = true, $checkBehaviors = true) |
||
776 | { |
||
777 | if ($name === $this->filterAttributeName) { |
||
778 | return true; |
||
779 | } |
||
780 | return parent::canGetProperty($name, $checkVars, $checkBehaviors); |
||
781 | } |
||
782 | |||
783 | /** |
||
784 | * {@inheritdoc} |
||
785 | */ |
||
786 | public function canSetProperty($name, $checkVars = true, $checkBehaviors = true) |
||
787 | { |
||
788 | if ($name === $this->filterAttributeName) { |
||
789 | return true; |
||
790 | } |
||
791 | return parent::canSetProperty($name, $checkVars, $checkBehaviors); |
||
792 | } |
||
793 | |||
794 | /** |
||
795 | * {@inheritdoc} |
||
796 | */ |
||
797 | 28 | public function __get($name) |
|
798 | { |
||
799 | 28 | if ($name === $this->filterAttributeName) { |
|
800 | 28 | return $this->getFilter(); |
|
801 | } |
||
802 | |||
803 | return parent::__get($name); |
||
804 | } |
||
805 | |||
806 | /** |
||
807 | * {@inheritdoc} |
||
808 | */ |
||
809 | 41 | public function __set($name, $value) |
|
810 | { |
||
811 | 41 | if ($name === $this->filterAttributeName) { |
|
812 | 41 | $this->setFilter($value); |
|
813 | } else { |
||
814 | parent::__set($name, $value); |
||
815 | } |
||
816 | 41 | } |
|
817 | |||
818 | /** |
||
819 | * {@inheritdoc} |
||
820 | */ |
||
821 | public function __isset($name) |
||
822 | { |
||
823 | if ($name === $this->filterAttributeName) { |
||
824 | return $this->getFilter() !== null; |
||
825 | } |
||
826 | |||
827 | return parent::__isset($name); |
||
828 | } |
||
829 | |||
830 | /** |
||
831 | * {@inheritdoc} |
||
832 | */ |
||
833 | public function __unset($name) |
||
834 | { |
||
835 | if ($name === $this->filterAttributeName) { |
||
836 | $this->setFilter(null); |
||
837 | } else { |
||
838 | parent::__unset($name); |
||
839 | } |
||
840 | } |
||
841 | } |
||
842 |
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.