1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Filterer |
4
|
|
|
* |
5
|
|
|
* @package php-logical-filter |
6
|
|
|
* @author Jean Claveau |
7
|
|
|
*/ |
8
|
|
|
namespace JClaveau\LogicalFilter\Filterer; |
9
|
|
|
|
10
|
|
|
use JClaveau\LogicalFilter\Filterer\FiltererInterface; |
11
|
|
|
use JClaveau\LogicalFilter\LogicalFilter; |
12
|
|
|
|
13
|
|
|
use JClaveau\LogicalFilter\Rule\InRule; |
14
|
|
|
use JClaveau\LogicalFilter\Rule\NotInRule; |
15
|
|
|
use JClaveau\LogicalFilter\Rule\EqualRule; |
16
|
|
|
use JClaveau\LogicalFilter\Rule\BelowRule; |
17
|
|
|
use JClaveau\LogicalFilter\Rule\AboveRule; |
18
|
|
|
use JClaveau\LogicalFilter\Rule\NotEqualRule; |
19
|
|
|
use JClaveau\LogicalFilter\Rule\AbstractAtomicRule; |
20
|
|
|
use JClaveau\LogicalFilter\Rule\AbstractOperationRule; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* This filterer provides the tools and API to apply a LogicalFilter once it has |
24
|
|
|
* been simplified. |
25
|
|
|
*/ |
26
|
|
|
abstract class Filterer implements FiltererInterface |
27
|
|
|
{ |
28
|
|
|
const leaves_only = 'leaves_only'; |
29
|
|
|
const on_row_matches = 'on_row_matches'; |
30
|
|
|
const on_row_mismatches = 'on_row_mismatches'; |
31
|
|
|
|
32
|
|
|
/** @var array $custom_actions */ |
33
|
|
|
protected $custom_actions = [ |
34
|
|
|
// self::on_row_matches => null, |
35
|
|
|
// self::on_row_mismatches => null, |
36
|
|
|
]; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
*/ |
40
|
|
|
public function setCustomActions(array $custom_actions) |
41
|
|
|
{ |
42
|
|
|
$this->custom_actions = $custom_actions; |
43
|
|
|
return $this; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
*/ |
48
|
62 |
|
public function onRowMatches(&$row, $key, &$rows, $matching_case, $options) |
49
|
|
|
{ |
50
|
62 |
View Code Duplication |
if (isset($options[ self::on_row_matches ])) { |
|
|
|
|
51
|
18 |
|
$callback = $options[ self::on_row_matches ]; |
52
|
18 |
|
} |
53
|
45 |
|
elseif (isset($this->custom_actions[ self::on_row_matches ])) { |
54
|
|
|
$callback = $this->custom_actions[ self::on_row_matches ]; |
55
|
|
|
} |
56
|
|
|
else { |
57
|
45 |
|
return; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
$args = [ |
61
|
|
|
// &$row, |
62
|
18 |
|
$row, |
63
|
18 |
|
$key, |
64
|
18 |
|
&$rows, |
65
|
18 |
|
$matching_case, |
66
|
18 |
|
$options, |
67
|
18 |
|
]; |
68
|
|
|
|
69
|
18 |
|
call_user_func_array($callback, $args); |
70
|
17 |
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
*/ |
74
|
63 |
|
public function onRowMismatches(&$row, $key, &$rows, $matching_case, $options) |
75
|
|
|
{ |
76
|
63 |
|
if ( ! $this->custom_actions |
|
|
|
|
77
|
63 |
|
&& ! isset($options[self::on_row_mismatches]) |
78
|
63 |
|
&& ! isset($options[self::on_row_matches]) |
79
|
63 |
|
) { |
80
|
|
|
// Unset by default ONLY if NO custom action defined |
81
|
47 |
|
unset($rows[$key]); |
82
|
47 |
|
return; |
83
|
|
|
} |
84
|
|
|
|
85
|
17 |
View Code Duplication |
if (isset($options[ self::on_row_mismatches ])) { |
|
|
|
|
86
|
2 |
|
$callback = $options[ self::on_row_mismatches ]; |
87
|
2 |
|
} |
88
|
15 |
|
elseif (isset($this->custom_actions[ self::on_row_mismatches ])) { |
89
|
|
|
$callback = $this->custom_actions[ self::on_row_mismatches ]; |
90
|
|
|
} |
91
|
|
|
else { |
92
|
15 |
|
return; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
$args = [ |
96
|
|
|
// &$row, |
97
|
2 |
|
$row, |
98
|
2 |
|
$key, |
99
|
2 |
|
&$rows, |
100
|
2 |
|
$matching_case, |
101
|
2 |
|
$options, |
102
|
2 |
|
]; |
103
|
|
|
|
104
|
2 |
|
call_user_func_array($callback, $args); |
105
|
2 |
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @return array |
109
|
|
|
*/ |
110
|
25 |
|
public function getChildren($row) |
111
|
|
|
{ |
112
|
25 |
|
return []; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
*/ |
117
|
|
|
public function setChildren(&$row, $filtered_children) |
118
|
|
|
{ |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @param LogicalFilter $filter |
123
|
|
|
* @param Iterable $tree_to_filter |
124
|
|
|
* @param array $options |
125
|
|
|
*/ |
126
|
70 |
|
public function apply( LogicalFilter $filter, $tree_to_filter, $options=[] ) |
|
|
|
|
127
|
|
|
{ |
128
|
70 |
|
if (! $filter->hasSolution()) { |
129
|
|
|
return null; |
130
|
|
|
} |
131
|
|
|
|
132
|
70 |
|
if (! isset($options['recurse'])) { |
133
|
70 |
|
$options['recurse'] = 'before'; |
134
|
70 |
|
} |
135
|
|
|
elseif (! in_array($options['recurse'], ['before', 'after', null])) { |
136
|
|
|
throw new \InvalidArgumentException( |
137
|
|
|
"Invalid value for 'recurse' option: " |
138
|
|
|
.var_export($options['recurse'], true) |
139
|
|
|
."\nInstead of ['before', 'after', null]" |
140
|
|
|
); |
141
|
|
|
} |
142
|
|
|
|
143
|
70 |
|
return $this->foreachRow( |
144
|
70 |
|
!$filter->getRules() ? [] : $filter->addMinimalCase()->getRules()->getOperands(), |
145
|
70 |
|
$tree_to_filter, |
146
|
70 |
|
$path=[], |
147
|
|
|
$options |
148
|
70 |
|
); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
*/ |
153
|
70 |
|
protected function foreachRow(array $root_cases, $tree_to_filter, array $path, $options=[]) |
|
|
|
|
154
|
|
|
{ |
155
|
|
|
// Once the rules are prepared, we parse the data |
156
|
70 |
|
foreach ($tree_to_filter as $row_index => $row_to_filter) { |
157
|
70 |
|
array_push($path, $row_index); |
158
|
|
|
|
159
|
70 |
View Code Duplication |
if ('before' == $options['recurse']) { |
|
|
|
|
160
|
70 |
|
if ($children = $this->getChildren($row_to_filter)) { |
161
|
43 |
|
$filtered_children = $this->foreachRow( |
162
|
43 |
|
$root_cases, |
163
|
43 |
|
$children, |
164
|
43 |
|
$path, |
165
|
|
|
$options |
166
|
43 |
|
); |
167
|
|
|
|
168
|
39 |
|
$this->setChildren($row_to_filter, $filtered_children); |
169
|
39 |
|
} |
170
|
70 |
|
} |
171
|
|
|
|
172
|
70 |
|
$matching_case = $this->applyOnRow($root_cases, $row_to_filter, $path, $options); |
173
|
|
|
|
174
|
65 |
|
if ($matching_case) { |
175
|
62 |
|
$this->onRowMatches($row_to_filter, $row_index, $tree_to_filter, $matching_case, $options); |
176
|
61 |
|
} |
177
|
63 |
|
elseif (false === $matching_case) { |
178
|
|
|
// No case match the rule |
179
|
63 |
|
$this->onRowMismatches($row_to_filter, $row_index, $tree_to_filter, $matching_case, $options); |
180
|
63 |
|
} |
181
|
16 |
|
elseif (null === $matching_case) { |
182
|
|
|
// We simply avoid rules |
183
|
|
|
// row out of scope |
184
|
16 |
|
} |
185
|
|
|
|
186
|
64 |
View Code Duplication |
if ('after' == $options['recurse']) { |
|
|
|
|
187
|
|
|
if ($children = $this->getChildren($row_to_filter)) { |
188
|
|
|
$filtered_children = $this->foreachRow( |
189
|
|
|
$root_cases, |
190
|
|
|
$children, |
191
|
|
|
$path, |
192
|
|
|
$options |
193
|
|
|
); |
194
|
|
|
|
195
|
|
|
$this->setChildren($row_to_filter, $filtered_children); |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
64 |
|
array_pop($path); |
200
|
64 |
|
} |
201
|
|
|
|
202
|
64 |
|
return $tree_to_filter; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @param LogicalFilter $filter |
207
|
|
|
* @param Iterable $tree_to_filter |
|
|
|
|
208
|
|
|
* @param array $options |
209
|
|
|
* |
210
|
|
|
* @return bool |
211
|
|
|
*/ |
212
|
4 |
|
public function hasMatchingCase( LogicalFilter $filter, $row_to_check, $key_to_check, $options=[] ) |
213
|
|
|
{ |
214
|
4 |
|
if (! $filter->hasSolution()) { |
215
|
|
|
return null; |
216
|
|
|
} |
217
|
|
|
|
218
|
4 |
|
return $this->applyOnRow( |
219
|
4 |
|
!$filter->getRules() ? [] : $filter->addMinimalCase()->getRules()->getOperands(), |
220
|
4 |
|
$row_to_check, |
221
|
4 |
|
$path=[$key_to_check], |
222
|
|
|
$options |
223
|
4 |
|
); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
*/ |
228
|
74 |
|
protected function applyOnRow(array $root_cases, $row_to_filter, array $path, $options=[]) |
|
|
|
|
229
|
|
|
{ |
230
|
74 |
|
$operands_validation_row_cache = []; |
231
|
|
|
|
232
|
74 |
|
if (! $root_cases) { |
|
|
|
|
233
|
1 |
|
$matching_case = true; |
234
|
1 |
|
} |
235
|
|
|
else { |
236
|
73 |
|
$matching_case = null; |
237
|
73 |
|
foreach ($root_cases as $and_case_index => $and_case) { |
238
|
73 |
|
if (! empty($options['debug'])) { |
239
|
|
|
var_dump("Case $and_case_index: ".$and_case); |
240
|
|
|
} |
241
|
|
|
|
242
|
73 |
|
$case_is_good = null; |
243
|
73 |
|
foreach ($and_case->getOperands() as $i => $rule) { |
244
|
73 |
|
$class = get_class($rule); |
245
|
|
|
|
246
|
73 |
|
if (in_array($class, [OrRule::class, AndRule::class, ])) { |
247
|
|
|
$field = null; |
248
|
|
|
$value = $rule->getOperands(); |
249
|
|
|
} |
250
|
73 |
|
elseif ($rule instanceof AbstractAtomicRule || ! $rule->isNormalizationAllowed($options)) { |
251
|
73 |
|
$field = $rule->getField(); |
252
|
73 |
|
$value = $rule->getValues(); |
253
|
73 |
|
} |
254
|
|
|
else { |
255
|
|
|
throw new \LogicException( |
256
|
|
|
"Filtering with a rule which has not been simplified: $rule" |
257
|
|
|
); |
258
|
|
|
} |
259
|
|
|
|
260
|
73 |
|
$operator = $rule::operator; |
261
|
|
|
|
262
|
73 |
|
$cache_key = $and_case_index.'~|~'.$field.'~|~'.$operator; |
263
|
|
|
|
264
|
73 |
|
if (! empty($operands_validation_row_cache[ $cache_key ])) { |
265
|
|
|
$is_valid = $operands_validation_row_cache[ $cache_key ]; |
266
|
|
|
} |
267
|
|
|
else { |
268
|
73 |
|
$is_valid = $this->validateRule( |
269
|
73 |
|
$field, |
270
|
73 |
|
$operator, |
271
|
73 |
|
$value, |
272
|
73 |
|
$row_to_filter, |
273
|
73 |
|
$path, |
274
|
73 |
|
$root_cases, |
275
|
|
|
$options |
276
|
73 |
|
); |
277
|
|
|
|
278
|
68 |
|
$operands_validation_row_cache[ $cache_key ] = $is_valid; |
279
|
|
|
} |
280
|
|
|
|
281
|
68 |
|
if (false === $is_valid) { |
282
|
|
|
// one of the rules of the and_case do not validate |
283
|
|
|
// so all the and_case is invalid |
284
|
67 |
|
$case_is_good = false; |
285
|
67 |
|
break; |
286
|
|
|
} |
287
|
66 |
|
elseif (true === $is_valid) { |
288
|
|
|
// one of the rules of the and_case do not validate |
289
|
|
|
// so all the and_case is invalid |
290
|
65 |
|
$case_is_good = true; |
291
|
65 |
|
} |
292
|
68 |
|
} |
293
|
|
|
|
294
|
68 |
|
if (true === $case_is_good) { |
295
|
|
|
// at least one and_case works so we can stop here |
296
|
65 |
|
$matching_case = $and_case; |
297
|
65 |
|
break; |
298
|
|
|
} |
299
|
67 |
|
elseif (false === $case_is_good) { |
300
|
67 |
|
$matching_case = false; |
301
|
67 |
|
} |
302
|
17 |
|
elseif (null === $case_is_good) { |
303
|
|
|
// row out of scope |
304
|
17 |
|
} |
305
|
68 |
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
69 |
|
return $matching_case; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/**/ |
312
|
|
|
} |
313
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.