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