1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* LogicalFilter |
4
|
|
|
* |
5
|
|
|
* @package php-logical-filter |
6
|
|
|
* @author Jean Claveau |
7
|
|
|
*/ |
8
|
|
|
namespace JClaveau\LogicalFilter; |
9
|
|
|
|
10
|
|
|
use JClaveau\LogicalFilter\Rule\AbstractRule; |
11
|
|
|
use JClaveau\LogicalFilter\Rule\AbstractOperationRule; |
12
|
|
|
use JClaveau\LogicalFilter\Rule\AndRule; |
13
|
|
|
use JClaveau\LogicalFilter\Rule\OrRule; |
14
|
|
|
use JClaveau\LogicalFilter\Rule\NotRule; |
15
|
|
|
|
16
|
|
|
use JClaveau\LogicalFilter\Filterer\Filterer; |
17
|
|
|
use JClaveau\LogicalFilter\Filterer\PhpFilterer; |
18
|
|
|
use JClaveau\LogicalFilter\Filterer\CustomizableFilterer; |
19
|
|
|
use JClaveau\LogicalFilter\Filterer\RuleFilterer; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* LogicalFilter describes a set of logical rules structured by |
23
|
|
|
* conjunctions and disjunctions (AND and OR). |
24
|
|
|
* |
25
|
|
|
* It's able to simplify them in order to find contractories branches |
26
|
|
|
* of the tree rule and check if there is at least one set rules having |
27
|
|
|
* possibilities. |
28
|
|
|
*/ |
29
|
|
|
class LogicalFilter implements \JsonSerializable |
30
|
|
|
{ |
31
|
|
|
/** @var AndRule $rules */ |
32
|
|
|
protected $rules; |
33
|
|
|
|
34
|
|
|
/** @var Filterer $default_filterer */ |
35
|
|
|
protected $default_filterer; |
36
|
|
|
|
37
|
|
|
/** @var array $options */ |
38
|
|
|
protected $options = []; |
39
|
|
|
|
40
|
|
|
/** @var array $default_options */ |
41
|
|
|
protected static $default_options = [ |
42
|
|
|
'in.normalization_threshold' => 0, |
43
|
|
|
]; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Creates a filter. You can provide a description of rules as in |
47
|
|
|
* addRules() as paramater. |
48
|
|
|
* |
49
|
|
|
* @param array $rules |
50
|
|
|
* @param Filterer $default_filterer |
51
|
|
|
* |
52
|
|
|
* @see self::addRules |
53
|
|
|
*/ |
54
|
145 |
|
public function __construct($rules=[], Filterer $default_filterer=null, array $options=[]) |
55
|
|
|
{ |
56
|
145 |
|
if ($rules instanceof AbstractRule) { |
57
|
1 |
|
$rules = $rules->copy(); |
58
|
1 |
|
} |
59
|
145 |
|
elseif ( ! is_null($rules) && ! is_array($rules)) { |
60
|
|
|
throw new \InvalidArgumentException( |
61
|
|
|
"\$rules must be a rules description or an AbstractRule instead of" |
62
|
|
|
.var_export($rules, true) |
63
|
|
|
); |
64
|
|
|
} |
65
|
|
|
|
66
|
145 |
|
if ($default_filterer) { |
67
|
11 |
|
$this->default_filterer = $default_filterer; |
68
|
11 |
|
} |
69
|
|
|
|
70
|
145 |
|
if ($options) { |
|
|
|
|
71
|
4 |
|
$this->options = $options; |
72
|
4 |
|
} |
73
|
|
|
|
74
|
145 |
|
if ($rules) { |
75
|
125 |
|
$this->and_( $rules ); |
76
|
124 |
|
} |
77
|
144 |
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
*/ |
81
|
9 |
|
protected function getDefaultFilterer() |
82
|
|
|
{ |
83
|
9 |
|
if ( ! $this->default_filterer) { |
84
|
7 |
|
$this->default_filterer = new PhpFilterer(); |
85
|
7 |
|
} |
86
|
|
|
|
87
|
9 |
|
return $this->default_filterer; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
*/ |
92
|
1 |
|
public static function setDefaultOptions(array $options) |
93
|
|
|
{ |
94
|
1 |
|
foreach ($options as $name => $default_value) { |
95
|
1 |
|
self::$default_options[$name] = $default_value; |
96
|
1 |
|
} |
97
|
|
|
|
98
|
1 |
|
AbstractRule::flushStaticCache(); |
99
|
1 |
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @return array |
103
|
|
|
*/ |
104
|
51 |
|
public static function getDefaultOptions() |
105
|
|
|
{ |
106
|
51 |
|
return self::$default_options; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* @return array |
111
|
|
|
*/ |
112
|
135 |
|
public function getOptions() |
113
|
|
|
{ |
114
|
135 |
|
$options = self::$default_options; |
115
|
135 |
|
foreach ($this->options as $name => $value) { |
116
|
4 |
|
$options[$name] = $value; |
117
|
135 |
|
} |
118
|
|
|
|
119
|
135 |
|
return $options; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* This method parses different ways to define the rules of a LogicalFilter. |
124
|
|
|
* + You can add N already instanciated Rules. |
125
|
|
|
* + You can provide 3 arguments: $field, $operator, $value |
126
|
|
|
* + You can provide a tree of rules: |
127
|
|
|
* [ |
128
|
|
|
* 'or', |
129
|
|
|
* [ |
130
|
|
|
* 'and', |
131
|
|
|
* ['field_5', 'above', 'a'], |
132
|
|
|
* ['field_5', 'below', 'a'], |
133
|
|
|
* ], |
134
|
|
|
* ['field_6', 'equal', 'b'], |
135
|
|
|
* ] |
136
|
|
|
* |
137
|
|
|
* @param string $operation and | or |
138
|
|
|
* @param array $rules_description Rules description |
139
|
|
|
* @return LogicalFilter $this |
140
|
|
|
*/ |
141
|
141 |
|
protected function addRules( $operation, array $rules_description ) |
142
|
|
|
{ |
143
|
141 |
|
if ($rules_description == [null]) { |
144
|
|
|
// TODO this is due to the bad design of using "Null" instead of |
145
|
|
|
// TrueRule when a Filter "has no rule". So it's the equivalent of |
146
|
|
|
// "and true" or "or true". |
147
|
|
|
// Remove it while fixing https://github.com/jclaveau/php-logical-filter/issues/59 |
148
|
3 |
|
if (AndRule::operator == $operation) { |
149
|
|
|
// A && True <=> A |
150
|
2 |
|
return $this; |
151
|
|
|
} |
152
|
2 |
|
elseif (OrRule::operator == $operation) { |
153
|
|
|
// A || True <=> True |
154
|
2 |
|
$this->rules = null; |
155
|
2 |
|
return $this; |
156
|
|
|
} |
157
|
|
|
else { |
158
|
|
|
throw new InvalidArgumentException( |
159
|
|
|
"Unhandled operation '$operation'" |
160
|
|
|
); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
141 |
|
if ( 3 == count($rules_description) |
165
|
141 |
|
&& is_string($rules_description[0]) |
166
|
141 |
|
&& is_string($rules_description[1]) |
167
|
141 |
|
) { |
168
|
|
|
// Atomic rules |
169
|
6 |
|
$new_rule = AbstractRule::generateSimpleRule( |
170
|
6 |
|
$rules_description[0], // field |
171
|
6 |
|
$rules_description[1], // operator |
172
|
6 |
|
$rules_description[2], // value |
173
|
6 |
|
$this->getOptions() |
174
|
6 |
|
); |
175
|
|
|
|
176
|
6 |
|
$this->addRule($new_rule, $operation); |
177
|
5 |
|
} |
178
|
|
|
elseif (count($rules_description) == count(array_filter($rules_description, function($arg) { |
179
|
136 |
|
return $arg instanceof LogicalFilter; |
180
|
136 |
|
})) ) { |
181
|
|
|
// Already instanciated rules |
182
|
2 |
|
foreach ($rules_description as $i => $filter) { |
183
|
2 |
|
$rules = $filter->getRules(); |
184
|
2 |
|
if (null !== $rules) { |
185
|
1 |
|
$this->addRule( $rules, $operation); |
186
|
1 |
|
} |
187
|
2 |
|
} |
188
|
2 |
|
} |
189
|
|
|
elseif (count($rules_description) == count(array_filter($rules_description, function($arg) { |
190
|
135 |
|
return $arg instanceof AbstractRule; |
191
|
135 |
|
})) ) { |
192
|
|
|
// Already instanciated rules |
193
|
3 |
|
foreach ($rules_description as $i => $new_rule) { |
194
|
3 |
|
$this->addRule( $new_rule, $operation); |
195
|
3 |
|
} |
196
|
3 |
|
} |
197
|
134 |
|
elseif (1 == count($rules_description) && is_array($rules_description[0])) { |
198
|
|
|
if (count($rules_description[0]) == count(array_filter($rules_description[0], function($arg) { |
199
|
134 |
|
return $arg instanceof AbstractRule; |
200
|
134 |
|
})) ) { |
201
|
|
|
// Case of $filter->or_([AbstractRule, AbstractRule, AbstractRule, ...]) |
202
|
2 |
|
foreach ($rules_description[0] as $i => $new_rule) { |
203
|
2 |
|
$this->addRule( $new_rule, $operation ); |
204
|
2 |
|
} |
205
|
2 |
|
} |
206
|
|
|
else { |
207
|
132 |
|
$fake_root = new AndRule; |
208
|
|
|
|
209
|
132 |
|
$this->addCompositeRule_recursion( |
210
|
132 |
|
$rules_description[0], |
211
|
|
|
$fake_root |
212
|
132 |
|
); |
213
|
|
|
|
214
|
129 |
|
$this->addRule( $fake_root->getOperands()[0], $operation ); |
215
|
|
|
} |
216
|
131 |
|
} |
217
|
|
|
else { |
218
|
1 |
|
throw new \InvalidArgumentException( |
219
|
|
|
"Bad set of arguments provided for rules addition: " |
220
|
1 |
|
.var_export($rules_description, true) |
221
|
1 |
|
); |
222
|
|
|
} |
223
|
|
|
|
224
|
138 |
|
return $this; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Add one rule object to the filter |
229
|
|
|
* |
230
|
|
|
* @param AbstractRule $rule |
231
|
|
|
* @param string $operation |
232
|
|
|
* |
233
|
|
|
* @return $this |
234
|
|
|
*/ |
235
|
137 |
|
protected function addRule( AbstractRule $rule, $operation=AndRule::operator ) |
236
|
|
|
{ |
237
|
137 |
|
if ( $this->rules && in_array( get_class($this->rules), [AndRule::class, OrRule::class] ) |
238
|
137 |
|
&& ! $this->rules->getOperands() ) { |
239
|
1 |
|
throw new \LogicException( |
240
|
|
|
"You are trying to add rules to a LogicalFilter which had " |
241
|
|
|
."only contradictory rules that have already been simplified: " |
242
|
1 |
|
.$this->rules |
243
|
1 |
|
); |
244
|
|
|
} |
245
|
|
|
|
246
|
137 |
|
if (null === $this->rules) { |
247
|
137 |
|
$this->rules = $rule; |
|
|
|
|
248
|
137 |
|
} |
249
|
18 |
|
elseif (($tmp_rules = $this->rules) // $this->rules::operator not supported in PHP 5.6 |
250
|
18 |
|
&& ($tmp_rules::operator != $operation) |
251
|
18 |
|
) { |
252
|
17 |
|
if (AndRule::operator == $operation) { |
253
|
12 |
|
$this->rules = new AndRule([$this->rules, $rule]); |
254
|
12 |
|
} |
255
|
6 |
|
elseif (OrRule::operator == $operation) { |
256
|
5 |
|
$this->rules = new OrRule([$this->rules, $rule]); |
|
|
|
|
257
|
5 |
|
} |
258
|
|
|
else { |
259
|
1 |
|
throw new \InvalidArgumentException( |
260
|
1 |
|
"\$operation must be '".AndRule::operator."' or '".OrRule::operator |
261
|
1 |
|
."' instead of: ".var_export($operation, true) |
262
|
1 |
|
); |
263
|
|
|
} |
264
|
16 |
|
} |
265
|
|
|
else { |
266
|
7 |
|
$this->rules->addOperand($rule); |
267
|
|
|
} |
268
|
|
|
|
269
|
137 |
|
return $this; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Recursion auxiliary of addCompositeRule. |
274
|
|
|
* |
275
|
|
|
* @param array $rules_composition The description of the |
276
|
|
|
* rules to add. |
277
|
|
|
* @param AbstractOperationRule $recursion_position The position in the |
278
|
|
|
* tree where rules must |
279
|
|
|
* be added. |
280
|
|
|
* |
281
|
|
|
* @return $this |
282
|
|
|
*/ |
283
|
132 |
|
protected function addCompositeRule_recursion( |
284
|
|
|
array $rules_composition, |
285
|
|
|
AbstractOperationRule $recursion_position |
286
|
|
|
) { |
287
|
|
|
if ( ! array_filter($rules_composition, function ($rule_composition_part) { |
288
|
132 |
|
return is_string($rule_composition_part); |
289
|
132 |
|
})) { |
290
|
|
|
// at least one operator is required for operation rules |
291
|
1 |
|
throw new \InvalidArgumentException( |
292
|
|
|
"Please provide an operator for the operation: \n" |
293
|
1 |
|
.var_export($rules_composition, true) |
294
|
1 |
|
); |
295
|
|
|
} |
296
|
131 |
|
elseif ( 3 == count($rules_composition) |
297
|
131 |
|
&& ! in_array( AndRule::operator, $rules_composition, true ) |
298
|
131 |
|
&& ! in_array( OrRule::operator, $rules_composition, true ) |
299
|
131 |
|
&& ! in_array( NotRule::operator, $rules_composition, true ) |
300
|
131 |
|
&& ! in_array( AbstractRule::findSymbolicOperator( AndRule::operator ), $rules_composition, true ) |
301
|
131 |
|
&& ! in_array( AbstractRule::findSymbolicOperator( OrRule::operator ), $rules_composition, true ) |
302
|
131 |
|
&& ! in_array( AbstractRule::findSymbolicOperator( NotRule::operator ), $rules_composition, true ) |
303
|
131 |
|
) { |
304
|
|
|
// atomic or composit rules |
305
|
129 |
|
$operand_left = $rules_composition[0]; |
306
|
129 |
|
$operation = $rules_composition[1]; |
307
|
129 |
|
$operand_right = $rules_composition[2]; |
308
|
|
|
|
309
|
129 |
|
$rule = AbstractRule::generateSimpleRule( |
310
|
129 |
|
$operand_left, $operation, $operand_right, $this->getOptions() |
311
|
129 |
|
); |
312
|
129 |
|
$recursion_position->addOperand( $rule ); |
313
|
129 |
|
} |
314
|
|
|
else { |
315
|
|
|
// operations |
316
|
98 |
|
if ( NotRule::operator == $rules_composition[0] |
317
|
98 |
|
|| $rules_composition[0] == AbstractRule::findSymbolicOperator( NotRule::operator ) ) { |
318
|
17 |
|
$rule = new NotRule(); |
319
|
17 |
|
} |
320
|
93 |
View Code Duplication |
elseif (in_array( AndRule::operator, $rules_composition ) |
|
|
|
|
321
|
93 |
|
|| in_array( AbstractRule::findSymbolicOperator( AndRule::operator ), $rules_composition )) { |
322
|
82 |
|
$rule = new AndRule(); |
323
|
82 |
|
} |
324
|
58 |
View Code Duplication |
elseif (in_array( OrRule::operator, $rules_composition ) |
|
|
|
|
325
|
58 |
|
|| in_array( AbstractRule::findSymbolicOperator( OrRule::operator ), $rules_composition ) ) { |
326
|
57 |
|
$rule = new OrRule(); |
327
|
57 |
|
} |
328
|
|
|
else { |
329
|
1 |
|
throw new \InvalidArgumentException( |
330
|
|
|
"A rule description seems to be an operation but do " |
331
|
1 |
|
."not contains a valid operator: ".var_export($rules_composition, true) |
332
|
1 |
|
); |
333
|
|
|
} |
334
|
|
|
|
335
|
97 |
|
$operator = $rule::operator; |
336
|
|
|
|
337
|
97 |
|
$operands_descriptions = array_filter( |
338
|
97 |
|
$rules_composition, |
339
|
|
|
function ($operand) use ($operator) { |
340
|
97 |
|
return ! in_array($operand, [$operator, AbstractRule::findSymbolicOperator( $operator )]); |
341
|
|
|
} |
342
|
97 |
|
); |
343
|
|
|
|
344
|
97 |
|
$non_true_rule_descriptions = array_filter( |
345
|
97 |
|
$operands_descriptions, |
346
|
|
|
function($operand) { |
347
|
96 |
|
return null !== $operand // no rule <=> true |
348
|
96 |
|
|| true !== $operand |
349
|
96 |
|
; |
350
|
|
|
} |
351
|
97 |
|
); |
352
|
|
|
|
353
|
97 |
|
foreach ($operands_descriptions as $i => $operands_description) { |
354
|
96 |
|
if (false === $operands_description) { |
355
|
1 |
|
$operands_descriptions[ $i ] = ['and']; // FalseRule hack |
356
|
1 |
|
} |
357
|
96 |
|
elseif (null === $operands_description || true === $operands_description) { |
358
|
1 |
|
$operands_description = ['and']; |
359
|
1 |
|
if ( ! $non_true_rule_descriptions) { |
|
|
|
|
360
|
|
|
throw new \LogicException( |
361
|
|
|
"TrueRules are not implemented. Please add " |
362
|
|
|
."them to operations having other type of rules" |
363
|
|
|
); |
364
|
|
|
} |
365
|
|
|
|
366
|
1 |
|
unset($operands_descriptions[ $i ]); |
367
|
1 |
|
} |
368
|
97 |
|
} |
369
|
|
|
|
370
|
97 |
|
$remaining_operations = array_filter( |
371
|
97 |
|
$operands_descriptions, |
372
|
|
|
function($operand) { |
373
|
96 |
|
return ! is_array($operand) |
374
|
96 |
|
&& ! $operand instanceof AbstractRule |
375
|
96 |
|
&& ! $operand instanceof LogicalFilter |
376
|
96 |
|
; |
377
|
|
|
} |
378
|
97 |
|
); |
379
|
|
|
|
380
|
97 |
|
if ($remaining_operations) { |
|
|
|
|
381
|
1 |
|
throw new \InvalidArgumentException( |
382
|
|
|
"Mixing different operations in the same rule level not implemented: \n[" |
383
|
1 |
|
. implode(', ', $remaining_operations)."]\n" |
384
|
1 |
|
. 'in ' . var_export($rules_composition, true) |
385
|
1 |
|
); |
386
|
|
|
} |
387
|
|
|
|
388
|
97 |
|
if (NotRule::operator == $operator && 1 != count($operands_descriptions)) { |
389
|
1 |
|
throw new \InvalidArgumentException( |
390
|
|
|
"Negations can have only one operand: \n" |
391
|
1 |
|
.var_export($rules_composition, true) |
392
|
1 |
|
); |
393
|
|
|
} |
394
|
|
|
|
395
|
97 |
|
foreach ($operands_descriptions as $operands_description) { |
396
|
96 |
|
if ($operands_description instanceof AbstractRule) { |
397
|
1 |
|
$rule->addOperand($operands_description); |
398
|
1 |
|
} |
399
|
96 |
|
elseif ($operands_description instanceof LogicalFilter) { |
400
|
2 |
|
$rule->addOperand($operands_description->getRules()); |
401
|
2 |
|
} |
402
|
|
|
else { |
403
|
95 |
|
$this->addCompositeRule_recursion( |
404
|
95 |
|
$operands_description, |
405
|
|
|
$rule |
406
|
95 |
|
); |
407
|
|
|
} |
408
|
97 |
|
} |
409
|
|
|
|
410
|
96 |
|
$recursion_position->addOperand( $rule ); |
411
|
|
|
} |
412
|
|
|
|
413
|
130 |
|
return $this; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* This method parses different ways to define the rules of a LogicalFilter |
418
|
|
|
* and add them as a new And part of the filter. |
419
|
|
|
* + You can add N already instanciated Rules. |
420
|
|
|
* + You can provide 3 arguments: $field, $operator, $value |
421
|
|
|
* + You can provide a tree of rules: |
422
|
|
|
* [ |
423
|
|
|
* 'or', |
424
|
|
|
* [ |
425
|
|
|
* 'and', |
426
|
|
|
* ['field_5', 'above', 'a'], |
427
|
|
|
* ['field_5', 'below', 'a'], |
428
|
|
|
* ], |
429
|
|
|
* ['field_6', 'equal', 'b'], |
430
|
|
|
* ] |
431
|
|
|
* |
432
|
|
|
* @param mixed The descriptions of the rules to add |
433
|
|
|
* @return $this |
434
|
|
|
* |
435
|
|
|
* @todo remove the _ for PHP 7 |
436
|
|
|
*/ |
437
|
138 |
|
public function and_() |
438
|
|
|
{ |
439
|
138 |
|
$this->addRules( AndRule::operator, func_get_args()); |
440
|
135 |
|
return $this; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* This method parses different ways to define the rules of a LogicalFilter |
445
|
|
|
* and add them as a new Or part of the filter. |
446
|
|
|
* + You can add N already instanciated Rules. |
447
|
|
|
* + You can provide 3 arguments: $field, $operator, $value |
448
|
|
|
* + You can provide a tree of rules: |
449
|
|
|
* [ |
450
|
|
|
* 'or', |
451
|
|
|
* [ |
452
|
|
|
* 'and', |
453
|
|
|
* ['field_5', 'above', 'a'], |
454
|
|
|
* ['field_5', 'below', 'a'], |
455
|
|
|
* ], |
456
|
|
|
* ['field_6', 'equal', 'b'], |
457
|
|
|
* ] |
458
|
|
|
* |
459
|
|
|
* @param mixed The descriptions of the rules to add |
460
|
|
|
* @return $this |
461
|
|
|
* |
462
|
|
|
* @todo |
463
|
|
|
* @todo remove the _ for PHP 7 |
464
|
|
|
*/ |
465
|
7 |
|
public function or_() |
466
|
|
|
{ |
467
|
7 |
|
$this->addRules( OrRule::operator, func_get_args()); |
468
|
7 |
|
return $this; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* @deprecated |
473
|
|
|
*/ |
474
|
1 |
|
public function matches($rules_to_match) |
475
|
|
|
{ |
476
|
1 |
|
return $this->hasSolutionIf($rules_to_match); |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Checks that a filter matches another one. |
481
|
|
|
* |
482
|
|
|
* @param array|AbstractRule $rules_to_match |
483
|
|
|
* |
484
|
|
|
* @return bool Whether or not this combination of filters has |
485
|
|
|
* potential solutions |
486
|
|
|
*/ |
487
|
1 |
|
public function hasSolutionIf($rules_to_match) |
488
|
|
|
{ |
489
|
1 |
|
return $this |
490
|
1 |
|
->copy() |
491
|
1 |
|
->and_($rules_to_match) |
492
|
1 |
|
->hasSolution() |
493
|
1 |
|
; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* Retrieve all the rules. |
498
|
|
|
* |
499
|
|
|
* @param bool $copy By default copy the rule tree to avoid side effects. |
500
|
|
|
* |
501
|
|
|
* @return AbstractRule The tree of rules |
502
|
|
|
*/ |
503
|
56 |
|
public function getRules($copy = true) |
504
|
|
|
{ |
505
|
56 |
|
return $copy && $this->rules ? $this->rules->copy() : $this->rules; |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
/** |
509
|
|
|
* Remove any constraint being a duplicate of another one. |
510
|
|
|
* |
511
|
|
|
* @param array $options stop_after | stop_before | |
512
|
|
|
* @return $this |
513
|
|
|
*/ |
514
|
93 |
|
public function simplify($options=[]) |
515
|
|
|
{ |
516
|
93 |
|
if ($this->rules) { |
517
|
|
|
// AndRule added to make all Operation methods available |
518
|
90 |
|
$this->rules = (new AndRule([$this->rules])) |
519
|
90 |
|
->simplify( $options ) |
520
|
|
|
// ->dump(true, false) |
521
|
|
|
; |
522
|
90 |
|
} |
523
|
|
|
|
524
|
93 |
|
return $this; |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
/** |
528
|
|
|
* Checks if there is at least on set of conditions which is not |
529
|
|
|
* contradictory. |
530
|
|
|
* |
531
|
|
|
* Checking if a filter has solutions require to simplify it first. |
532
|
|
|
* To let the control on the balance between readability and |
533
|
|
|
* performances, the required simplification can be saved or not |
534
|
|
|
* depending on the $save_simplification parameter. |
535
|
|
|
* |
536
|
|
|
* @param $save_simplification |
537
|
|
|
* |
538
|
|
|
* @return bool |
539
|
|
|
*/ |
540
|
8 |
|
public function hasSolution($save_simplification=true) |
541
|
|
|
{ |
542
|
8 |
|
if ( ! $this->rules) { |
543
|
1 |
|
return true; |
544
|
|
|
} |
545
|
|
|
|
546
|
7 |
|
if ($save_simplification) { |
547
|
6 |
|
$this->simplify(); |
548
|
6 |
|
return $this->rules->hasSolution(); |
549
|
|
|
} |
550
|
|
|
|
551
|
2 |
|
return $this->copy()->simplify()->rules->hasSolution(); |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Returns an array describing the rule tree of the Filter. |
556
|
|
|
* |
557
|
|
|
* @param array $options |
558
|
|
|
* |
559
|
|
|
* @return array A description of the rules. |
560
|
|
|
*/ |
561
|
95 |
|
public function toArray(array $options=[]) |
562
|
|
|
{ |
563
|
95 |
|
return $this->rules ? $this->rules->toArray($options) : $this->rules; |
564
|
48 |
|
} |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* Returns an array describing the rule tree of the Filter. |
568
|
|
|
* |
569
|
|
|
* @param $debug Provides a source oriented dump. |
570
|
|
|
* |
571
|
|
|
* @return array A description of the rules. |
572
|
|
|
*/ |
573
|
4 |
|
public function toString(array $options=[]) |
574
|
|
|
{ |
575
|
4 |
|
return $this->rules ? $this->rules->toString($options) : $this->rules; |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
/** |
579
|
|
|
* Returns a unique id corresponding to the set of rules of the filter |
580
|
|
|
* |
581
|
|
|
* @return string The unique semantic id |
582
|
|
|
*/ |
583
|
1 |
|
public function getSemanticId() |
584
|
|
|
{ |
585
|
1 |
|
return $this->rules ? $this->rules->getSemanticId() : null; |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
/** |
589
|
|
|
* For implementing JsonSerializable interface. |
590
|
|
|
* |
591
|
|
|
* @see https://secure.php.net/manual/en/jsonserializable.jsonserialize.php |
592
|
|
|
*/ |
593
|
1 |
|
public function jsonSerialize() |
594
|
|
|
{ |
595
|
1 |
|
return $this->toArray(); |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* @return string |
600
|
|
|
*/ |
601
|
4 |
|
public function __toString() |
602
|
|
|
{ |
603
|
4 |
|
return $this->toString(); |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
/** |
607
|
|
|
* @see https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke |
608
|
|
|
* @param mixed $row |
609
|
|
|
* @return bool |
610
|
|
|
*/ |
611
|
3 |
|
public function __invoke($row, $key=null) |
612
|
|
|
{ |
613
|
3 |
|
return $this->validates($row, $key); |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
/** |
617
|
|
|
* Removes all the defined rules. |
618
|
|
|
* |
619
|
|
|
* @return $this |
620
|
|
|
*/ |
621
|
2 |
|
public function flushRules() |
622
|
|
|
{ |
623
|
2 |
|
$this->rules = null; |
624
|
2 |
|
return $this; |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
/** |
628
|
|
|
* @param array|callable Associative array of renamings or callable |
629
|
|
|
* that would rename the fields. |
630
|
|
|
* |
631
|
|
|
* @return string $this |
632
|
|
|
*/ |
633
|
1 |
|
public function renameFields($renamings) |
634
|
|
|
{ |
635
|
1 |
|
if (method_exists($this->rules, 'renameField')) { |
636
|
|
|
$this->rules->renameField($renamings); |
|
|
|
|
637
|
|
|
} |
638
|
1 |
|
elseif ($this->rules) { |
639
|
1 |
|
$this->rules->renameFields($renamings); |
640
|
1 |
|
} |
641
|
|
|
|
642
|
1 |
|
return $this; |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
/** |
646
|
|
|
* @param array|callable Associative array of renamings or callable |
647
|
|
|
* that would rename the fields. |
648
|
|
|
* |
649
|
|
|
* @return string $this |
650
|
|
|
*/ |
651
|
10 |
|
public function removeRules($filter) |
652
|
|
|
{ |
653
|
10 |
|
$cache_flush_required = false; |
654
|
|
|
|
655
|
10 |
|
$this->rules = (new RuleFilterer)->apply( |
656
|
10 |
|
new LogicalFilter($filter), |
657
|
10 |
|
$this->rules, |
658
|
|
|
[ |
659
|
|
|
Filterer::on_row_matches => function($rule, $key, &$rows, $matching_case) use (&$cache_flush_required) { |
|
|
|
|
660
|
|
|
// $rule->dump(); |
661
|
8 |
|
unset( $rows[$key] ); |
662
|
8 |
|
if ( ! $rows ) { |
663
|
1 |
|
throw new \Exception( |
664
|
1 |
|
"Removing the only rule $rule from the filter $this " |
665
|
|
|
."produces a case which has no possible solution due to missing " |
666
|
1 |
|
."implementation of TrueRule.\n" |
667
|
1 |
|
."Please see: https://github.com/jclaveau/php-logical-filter/issues/59" |
668
|
1 |
|
); |
669
|
|
|
} |
670
|
|
|
|
671
|
|
|
// $matching_case->dump(true); |
672
|
7 |
|
$cache_flush_required = true; |
673
|
10 |
|
}, |
674
|
|
|
// Filterer::on_row_mismatches => function($rule, $key, &$rows, $matching_case) { |
675
|
|
|
// $rule->dump(); |
676
|
|
|
// $matching_case && $matching_case->dump(true); |
677
|
|
|
// } |
678
|
|
|
] |
679
|
10 |
|
); |
680
|
|
|
|
681
|
7 |
|
if ($cache_flush_required) { |
682
|
7 |
|
$this->rules->flushCache(); |
683
|
7 |
|
} |
684
|
|
|
|
685
|
7 |
|
return $this; |
686
|
|
|
} |
687
|
|
|
|
688
|
|
|
/** |
689
|
|
|
* @param array|callable Associative array of renamings or callable |
690
|
|
|
* that would rename the fields. |
691
|
|
|
* |
692
|
|
|
* @return array The rules matching the filter |
693
|
|
|
* @return array $options debug | leaves_only | clean_empty_branches |
694
|
|
|
* |
695
|
|
|
* |
696
|
|
|
* @todo Merge with rules |
697
|
|
|
*/ |
698
|
4 |
|
public function keepLeafRulesMatching($filter=[], array $options=[]) |
699
|
|
|
{ |
700
|
4 |
|
$clean_empty_branches = ! isset($options['clean_empty_branches']) || $options['clean_empty_branches']; |
701
|
|
|
|
702
|
4 |
|
$filter = (new LogicalFilter($filter, new RuleFilterer)) |
703
|
|
|
// ->dump() |
704
|
4 |
|
; |
705
|
|
|
|
706
|
4 |
|
$options[ Filterer::leaves_only ] = true; |
707
|
|
|
|
708
|
4 |
|
$this->rules = (new RuleFilterer)->apply($filter, $this->rules, $options); |
709
|
|
|
// $this->rules->dump(true); |
710
|
|
|
|
711
|
|
|
|
712
|
|
|
// clean the remaining branches |
713
|
4 |
|
if ($clean_empty_branches) { |
714
|
4 |
|
$this->rules = (new RuleFilterer)->apply( |
715
|
4 |
|
new LogicalFilter(['and', |
716
|
4 |
|
['operator', 'in', ['or', 'and', 'not', '!in']], |
717
|
4 |
|
['children', '=', 0], |
718
|
4 |
|
]), |
719
|
4 |
|
$this->rules, |
720
|
|
|
[ |
721
|
|
|
Filterer::on_row_matches => function($rule, $key, &$rows) { |
722
|
2 |
|
unset( $rows[$key] ); |
723
|
4 |
|
}, |
724
|
|
|
Filterer::on_row_mismatches => function($rule, $key, &$rows) { |
|
|
|
|
725
|
4 |
|
}, |
726
|
|
|
] |
727
|
4 |
|
); |
728
|
|
|
|
729
|
|
|
// TODO replace it by a FalseRule |
730
|
4 |
|
if (false === $this->rules) { |
731
|
1 |
|
$this->rules = new AndRule; |
732
|
1 |
|
} |
733
|
4 |
|
} |
734
|
|
|
|
735
|
4 |
|
return $this; |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
/** |
739
|
|
|
* @param array|callable Associative array of renamings or callable |
740
|
|
|
* that would rename the fields. |
741
|
|
|
* |
742
|
|
|
* @return array The rules matching the filter |
743
|
|
|
* |
744
|
|
|
* |
745
|
|
|
* @todo Merge with rules |
746
|
|
|
*/ |
747
|
3 |
|
public function listLeafRulesMatching($filter=[]) |
748
|
|
|
{ |
749
|
3 |
|
$filter = (new LogicalFilter($filter, new RuleFilterer)) |
750
|
|
|
// ->dump() |
751
|
3 |
|
; |
752
|
|
|
|
753
|
3 |
|
if ( ! $this->rules) { |
754
|
1 |
|
return []; |
755
|
|
|
} |
756
|
|
|
|
757
|
2 |
|
$out = []; |
758
|
2 |
|
(new RuleFilterer)->apply( |
759
|
2 |
|
$filter, |
760
|
2 |
|
$this->rules, |
761
|
|
|
[ |
762
|
2 |
|
Filterer::on_row_matches => function( |
763
|
|
|
AbstractRule $matching_rule, |
764
|
|
|
$key, |
|
|
|
|
765
|
|
|
array $siblings |
|
|
|
|
766
|
|
|
) use (&$out) { |
767
|
|
|
if ( ! $matching_rule instanceof AndRule |
768
|
2 |
|
&& ! $matching_rule instanceof OrRule |
769
|
2 |
|
&& ! $matching_rule instanceof NotRule |
770
|
2 |
|
) { |
771
|
2 |
|
$out[] = $matching_rule; |
772
|
2 |
|
} |
773
|
2 |
|
}, |
774
|
2 |
|
Filterer::leaves_only => true, |
775
|
|
|
] |
776
|
2 |
|
); |
777
|
|
|
|
778
|
2 |
|
return $out; |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
/** |
782
|
|
|
* @param array|callable Associative array of renamings or callable |
783
|
|
|
* that would rename the fields. |
784
|
|
|
* |
785
|
|
|
* @return array The rules matching the filter |
786
|
|
|
* |
787
|
|
|
* |
788
|
|
|
* @todo Make it available on AbstractRule also |
789
|
|
|
*/ |
790
|
2 |
|
public function onEachRule($filter=[], $options) |
791
|
|
|
{ |
792
|
2 |
|
$filter = (new LogicalFilter($filter, new RuleFilterer)) |
793
|
|
|
// ->dump() |
794
|
2 |
|
; |
795
|
|
|
|
796
|
2 |
|
if ( ! $this->rules) { |
797
|
|
|
return []; |
798
|
|
|
} |
799
|
|
|
|
800
|
2 |
|
if (is_callable($options)) { |
801
|
|
|
$options = [ |
802
|
2 |
|
Filterer::on_row_matches => $options, |
803
|
2 |
|
]; |
804
|
2 |
|
} |
805
|
|
|
|
806
|
2 |
|
(new RuleFilterer)->apply( |
807
|
2 |
|
$filter, |
808
|
2 |
|
$this->rules, |
809
|
|
|
$options |
810
|
2 |
|
); |
811
|
|
|
|
812
|
2 |
|
return $this; |
813
|
|
|
} |
814
|
|
|
|
815
|
|
|
/** |
816
|
|
|
*/ |
817
|
1 |
|
public function onEachCase(callable $action) |
818
|
|
|
{ |
819
|
1 |
|
$this->simplify(['force_logical_core' => true]); |
820
|
|
|
|
821
|
1 |
|
if ( ! $this->rules) { |
822
|
|
|
return $this; |
823
|
|
|
} |
824
|
|
|
|
825
|
1 |
|
$operands = $this->rules->getOperands(); |
826
|
|
|
|
827
|
1 |
|
foreach ($operands as $i => &$and_case) { |
828
|
|
|
$arguments = [ |
829
|
1 |
|
&$and_case, |
830
|
1 |
|
]; |
831
|
1 |
|
call_user_func_array($action, $arguments); |
832
|
1 |
|
} |
833
|
|
|
|
834
|
|
|
// Debug::dumpJson($operands, true); |
835
|
1 |
|
$this->rules = new OrRule($operands); |
|
|
|
|
836
|
|
|
|
837
|
1 |
|
return $this; |
838
|
|
|
} |
839
|
|
|
|
840
|
|
|
/** |
841
|
|
|
* Clone the current object and its rules. |
842
|
|
|
* |
843
|
|
|
* @return LogicalFilter A copy of the current instance with a copied ruletree |
844
|
|
|
*/ |
845
|
7 |
|
public function copy() |
846
|
|
|
{ |
847
|
7 |
|
return clone $this; |
848
|
|
|
} |
849
|
|
|
|
850
|
|
|
/** |
851
|
|
|
* Make a deep copy of the rules |
852
|
|
|
*/ |
853
|
7 |
|
public function __clone() |
854
|
|
|
{ |
855
|
7 |
|
if ($this->rules) { |
856
|
7 |
|
$this->rules = $this->rules->copy(); |
857
|
7 |
|
} |
858
|
7 |
|
} |
859
|
|
|
|
860
|
|
|
/** |
861
|
|
|
* Copy the current instance into the variable given as parameter |
862
|
|
|
* and returns the copy. |
863
|
|
|
* |
864
|
|
|
* @return LogicalFilter |
865
|
|
|
*/ |
866
|
3 |
|
public function saveAs( &$variable ) |
867
|
|
|
{ |
868
|
3 |
|
return $variable = $this; |
869
|
|
|
} |
870
|
|
|
|
871
|
|
|
/** |
872
|
|
|
* Copy the current instance into the variable given as parameter |
873
|
|
|
* and returns the copied instance. |
874
|
|
|
* |
875
|
|
|
* @return LogicalFilter |
876
|
|
|
*/ |
877
|
1 |
|
public function saveCopyAs( &$copied_variable ) |
878
|
|
|
{ |
879
|
1 |
|
$copied_variable = $this->copy(); |
880
|
1 |
|
return $this; |
881
|
|
|
} |
882
|
|
|
|
883
|
|
|
/** |
884
|
|
|
* @param bool $exit=false |
|
|
|
|
885
|
|
|
* @param array $options + callstack_depth=2 The level of the caller to dump |
886
|
|
|
* + mode='string' in 'export' | 'dump' | 'string' |
887
|
|
|
* |
888
|
|
|
* @return $this |
889
|
|
|
*/ |
890
|
4 |
|
public function dump($exit=false, array $options=[]) |
891
|
|
|
{ |
892
|
|
|
$default_options = [ |
893
|
4 |
|
'callstack_depth' => 3, |
894
|
4 |
|
'mode' => 'string', |
895
|
4 |
|
]; |
896
|
4 |
|
foreach ($default_options as $default_option => &$default_value) { |
897
|
4 |
|
if ( ! isset($options[ $default_option ])) { |
898
|
4 |
|
$options[ $default_option ] = $default_value; |
899
|
4 |
|
} |
900
|
4 |
|
} |
901
|
4 |
|
extract($options); |
902
|
|
|
|
903
|
4 |
|
if ($this->rules) { |
904
|
4 |
|
$this->rules->dump($exit, $options); |
905
|
4 |
|
} |
906
|
|
|
else { |
907
|
|
|
// TODO dump a TrueRule |
908
|
|
|
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $callstack_depth); |
909
|
|
|
$caller = $bt[ $callstack_depth - 2 ]; |
910
|
|
|
|
911
|
|
|
// get line and file from the previous level of the caller |
912
|
|
|
// TODO go deeper if this case exist? |
913
|
|
View Code Duplication |
if ( ! isset($caller['file'])) { |
|
|
|
|
914
|
|
|
$caller['file'] = $bt[ $callstack_depth - 3 ]['file']; |
915
|
|
|
} |
916
|
|
View Code Duplication |
if ( ! isset($caller['line'])) { |
|
|
|
|
917
|
|
|
$caller['line'] = $bt[ $callstack_depth - 3 ]['line']; |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
try { |
921
|
|
|
echo "\n" . $caller['file'] . ':' . $caller['line'] . "\n"; |
922
|
|
|
var_export($this->toArray($options)); |
923
|
|
|
} |
924
|
|
|
catch (\Exception $e) { |
925
|
|
|
echo "\nError while dumping: " . $e->getMessage() . "\n"; |
926
|
|
|
var_export($caller); |
927
|
|
|
echo "\n\n"; |
928
|
|
|
var_export($bt); |
929
|
|
|
echo "\n\n"; |
930
|
|
|
var_export($this->toArray($options)); |
931
|
|
|
} |
932
|
|
|
echo "\n\n"; |
933
|
|
|
|
934
|
|
|
if ($exit) { |
935
|
|
|
exit; |
936
|
|
|
} |
937
|
|
|
} |
938
|
|
|
|
939
|
4 |
|
return $this; |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
/** |
943
|
|
|
* Applies the current instance to a set of data. |
944
|
|
|
* |
945
|
|
|
* @param mixed $data_to_filter |
946
|
|
|
* @param Filterer|callable|null $filterer |
947
|
|
|
* |
948
|
|
|
* @return mixed The filtered data |
949
|
|
|
*/ |
950
|
5 |
|
public function applyOn($data_to_filter, $action_on_matches=null, $filterer=null) |
|
|
|
|
951
|
|
|
{ |
952
|
5 |
View Code Duplication |
if ( ! $filterer) { |
|
|
|
|
953
|
5 |
|
$filterer = $this->getDefaultFilterer(); |
954
|
5 |
|
} |
955
|
|
|
elseif (is_callable($filterer)) { |
956
|
|
|
$filterer = new CustomizableFilterer($filterer); |
957
|
|
|
} |
958
|
|
|
elseif ( ! $filterer instanceof Filterer) { |
959
|
|
|
throw new \InvalidArgumentException( |
960
|
|
|
"The given \$filterer must be null or a callable or a instance " |
961
|
|
|
."of Filterer instead of: ".var_export($filterer, true) |
962
|
|
|
); |
963
|
|
|
} |
964
|
|
|
|
965
|
5 |
|
if ($data_to_filter instanceof LogicalFilter) { |
966
|
2 |
|
$filtered_rules = $filterer->apply( $this, $data_to_filter->getRules() ); |
|
|
|
|
967
|
2 |
|
return $data_to_filter->flushRules()->addRule( $filtered_rules ); |
968
|
|
|
} |
969
|
|
|
else { |
970
|
3 |
|
return $filterer->apply( $this, $data_to_filter ); |
971
|
|
|
} |
972
|
|
|
} |
973
|
|
|
|
974
|
|
|
/** |
975
|
|
|
* Applies the current instance to a value (and its index optionnally). |
976
|
|
|
* |
977
|
|
|
* @param mixed $value_to_check |
978
|
|
|
* @param scalar $index |
|
|
|
|
979
|
|
|
* @param Filterer|callable|null $filterer |
980
|
|
|
* |
981
|
|
|
* @return AbstractRule|false|true + False if the filter doesn't validates |
982
|
|
|
* + Null if the target has no sens (operation filtered by field for example) |
983
|
|
|
* + A rule tree containing the first matching case if there is one. |
984
|
|
|
*/ |
985
|
4 |
|
public function validates($value_to_check, $key_to_check=null, $filterer=null) |
986
|
|
|
{ |
987
|
4 |
View Code Duplication |
if ( ! $filterer) { |
|
|
|
|
988
|
4 |
|
$filterer = $this->getDefaultFilterer(); |
989
|
4 |
|
} |
990
|
|
|
elseif (is_callable($filterer)) { |
991
|
|
|
$filterer = new CustomizableFilterer($filterer); |
992
|
|
|
} |
993
|
|
|
elseif ( ! $filterer instanceof Filterer) { |
994
|
|
|
throw new \InvalidArgumentException( |
995
|
|
|
"The given \$filterer must be null or a callable or a instance " |
996
|
|
|
."of Filterer instead of: ".var_export($filterer, true) |
997
|
|
|
); |
998
|
|
|
} |
999
|
|
|
|
1000
|
4 |
|
return $filterer->hasMatchingCase( $this, $value_to_check, $key_to_check ); |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
/**/ |
1004
|
|
|
} |
1005
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.