1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
namespace Sirius\Filtration; |
4
|
|
|
|
5
|
|
|
class Filtrator implements FiltratorInterface |
6
|
|
|
{ |
7
|
|
|
// selector to specify that the filter is applied to the entire data set |
8
|
|
|
const SELECTOR_ROOT = '/'; |
9
|
|
|
|
10
|
|
|
// selector to specify that the filter is applied to all ITEMS of a set |
11
|
|
|
const SELECTOR_ANY = '*'; |
12
|
|
|
|
13
|
|
|
protected $filterFactory; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* The list of filters available in the filtrator |
17
|
|
|
* |
18
|
|
|
* @var array |
19
|
|
|
*/ |
20
|
|
|
protected $filters = []; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var array |
24
|
|
|
*/ |
25
|
|
|
protected $allowedSelectors = []; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var array |
29
|
|
|
*/ |
30
|
|
|
protected $compiledAllowedSelectors = []; |
31
|
|
|
|
32
|
20 |
|
public function __construct(FilterFactory $filterFactory = null) |
33
|
|
|
{ |
34
|
20 |
|
if (!$filterFactory) { |
35
|
|
|
$filterFactory = new FilterFactory(); |
36
|
|
|
} |
37
|
20 |
|
$this->filterFactory = $filterFactory; |
38
|
20 |
|
} |
39
|
|
|
|
40
|
1 |
|
public function setAllowed(array $allowedSelectors = []) |
41
|
|
|
{ |
42
|
1 |
|
$this->allowedSelectors = $allowedSelectors; |
43
|
1 |
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Add a filter to the filters stack |
47
|
|
|
* |
48
|
|
|
* @example // normal callback |
49
|
|
|
* $filtrator->add('title', '\strip_tags'); |
50
|
|
|
* // anonymous function |
51
|
|
|
* $filtrator->add('title', function($value){ return $value . '!!!'; }); |
52
|
|
|
* // filter class from the library registered on the $filtersMap |
53
|
|
|
* $filtrator->add('title', 'normalizedate', ['format' => 'm/d/Y']); |
54
|
|
|
* // custom class |
55
|
|
|
* $filtrator->add('title', '\MyApp\Filters\CustomFilter'); |
56
|
|
|
* // multiple filters as once with different ways to pass options |
57
|
|
|
* $filtrator->add('title', [ |
58
|
|
|
* ['truncate', 'limit=10', true, 10], |
59
|
|
|
* ['censor', ['words' => ['idiot']] |
60
|
|
|
* ]); |
61
|
|
|
* // multiple fitlers as a single string |
62
|
|
|
* $filtrator->add('title', 'stringtrim(side=left)(true)(10) | truncate(limit=100)'); |
63
|
|
|
* @param string|array $selector |
64
|
|
|
* @param mixed $callbackOrFilterName |
65
|
|
|
* @param array|null $options |
66
|
|
|
* @param bool $recursive |
67
|
|
|
* @param integer $priority |
68
|
|
|
* @throws \InvalidArgumentException |
69
|
|
|
* @internal param $ callable|filter class name|\Sirius\Filtration\Filter\AbstractFilter $callbackOrFilterName |
70
|
|
|
* @internal param array|string $params |
71
|
|
|
* @return self |
72
|
|
|
*/ |
73
|
20 |
|
public function add($selector, $callbackOrFilterName = null, $options = null, $recursive = false, $priority = 0) |
74
|
|
|
{ |
75
|
|
|
/** |
76
|
|
|
* $selector is actually an array with filters |
77
|
|
|
* |
78
|
|
|
* @example $filtrator->add([ |
79
|
|
|
* 'title' => ['trim', ['truncate', '{"limit":100}']] |
80
|
|
|
* 'description' => ['trim'] |
81
|
|
|
* ]); |
82
|
|
|
*/ |
83
|
20 |
|
if (is_array($selector)) { |
84
|
1 |
|
foreach ($selector as $key => $filters) { |
85
|
1 |
|
$this->add($key, $filters); |
86
|
|
|
} |
87
|
1 |
|
return $this; |
88
|
|
|
} |
89
|
|
|
|
90
|
20 |
|
if (! is_string($selector)) { |
91
|
1 |
|
throw new \InvalidArgumentException('The data selector for filtering must be a string'); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
|
95
|
19 |
|
if (is_string($callbackOrFilterName)) { |
96
|
|
|
// rule was supplied like 'trim' or 'trim | nullify' |
97
|
15 |
|
if (strpos($callbackOrFilterName, ' | ') !== false) { |
98
|
1 |
|
return $this->add($selector, explode(' | ', $callbackOrFilterName)); |
99
|
|
|
} |
100
|
|
|
// rule was supplied like this 'trim(limit=10)(true)(10)' |
101
|
15 |
|
if (strpos($callbackOrFilterName, '(') !== false) { |
102
|
2 |
|
list($callbackOrFilterName, $options, $recursive, $priority) = $this->parseRule($callbackOrFilterName); |
103
|
|
|
} |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* The $callbackOrFilterName is an array of filters |
108
|
|
|
* |
109
|
|
|
* @example $filtrator->add('title', [ |
110
|
|
|
* 'trim', |
111
|
|
|
* ['truncate', '{"limit":100}'] |
112
|
|
|
* ]); |
113
|
|
|
*/ |
114
|
19 |
|
if (is_array($callbackOrFilterName) && ! is_callable($callbackOrFilterName)) { |
115
|
3 |
|
foreach ($callbackOrFilterName as $filter) { |
116
|
|
|
// $filter is something like ['truncate', '{"limit":100}'] |
117
|
3 |
|
if (is_array($filter) && ! is_callable($filter)) { |
118
|
1 |
|
$this->add($selector, ...$filter); |
119
|
2 |
|
} elseif (is_string($filter) || is_callable($filter)) { |
120
|
2 |
|
$this->add($selector, $filter); |
121
|
|
|
} |
122
|
|
|
} |
123
|
3 |
|
return $this; |
124
|
|
|
} |
125
|
|
|
|
126
|
19 |
|
$filter = $this->filterFactory->createFilter($callbackOrFilterName, $options, $recursive); |
127
|
16 |
|
if (! array_key_exists($selector, $this->filters)) { |
128
|
16 |
|
$this->filters[$selector] = new FilterSet(); |
129
|
|
|
} |
130
|
|
|
/* @var $filterSet FilterSet */ |
131
|
16 |
|
$filterSet = $this->filters[$selector]; |
132
|
16 |
|
$filterSet->insert($filter, $priority); |
133
|
16 |
|
$this->compiledAllowedSelectors = []; |
134
|
|
|
|
135
|
16 |
|
return $this; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Converts a rule that was supplied as string into a set of options that define the rule |
140
|
|
|
* |
141
|
|
|
* @example 'minLength({"min":2})(true)(10)' |
142
|
|
|
* |
143
|
|
|
* will be converted into |
144
|
|
|
* |
145
|
|
|
* [ |
146
|
|
|
* 'minLength', // validator name |
147
|
|
|
* ['min' => 2'], // validator options |
148
|
|
|
* true, // recursive |
149
|
|
|
* 10 // priority |
150
|
|
|
* ] |
151
|
|
|
* @param string $ruleAsString |
152
|
|
|
* @return array |
153
|
|
|
*/ |
154
|
2 |
|
protected function parseRule($ruleAsString) |
155
|
|
|
{ |
156
|
2 |
|
$ruleAsString = trim($ruleAsString); |
157
|
|
|
|
158
|
2 |
|
$options = []; |
159
|
2 |
|
$recursive = false; |
160
|
2 |
|
$priority = 0; |
161
|
|
|
|
162
|
2 |
|
$name = substr($ruleAsString, 0, strpos($ruleAsString, '(')); |
163
|
2 |
|
$ruleAsString = substr($ruleAsString, strpos($ruleAsString, '(')); |
164
|
2 |
|
$matches = []; |
165
|
2 |
|
preg_match_all('/\(([^\)]*)\)/', $ruleAsString, $matches); |
166
|
|
|
|
167
|
2 |
|
if (isset($matches[1])) { |
168
|
2 |
|
if (isset($matches[1][0]) && $matches[1][0]) { |
169
|
2 |
|
$options = $matches[1][0]; |
170
|
|
|
} |
171
|
2 |
|
if (isset($matches[1][1]) && $matches[1][1]) { |
172
|
2 |
|
$recursive = (in_array($matches[1][1], array(true, 'TRUE', 'true', 1))) ? true : false; |
173
|
|
|
} |
174
|
2 |
|
if (isset($matches[1][2]) && $matches[1][2]) { |
175
|
2 |
|
$priority = (int)$matches[1][2]; |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
return [ |
180
|
2 |
|
$name, |
181
|
2 |
|
$options, |
182
|
2 |
|
$recursive, |
183
|
2 |
|
$priority |
184
|
|
|
]; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Remove a filter from the stack |
189
|
|
|
* |
190
|
|
|
* @param string $selector |
191
|
|
|
* @param bool|callable|string|TRUE $callbackOrName |
192
|
|
|
* @throws \InvalidArgumentException |
193
|
|
|
* @return \Sirius\Filtration\Filtrator |
194
|
|
|
*/ |
195
|
3 |
|
public function remove($selector, $callbackOrName = true) |
196
|
|
|
{ |
197
|
3 |
|
if (array_key_exists($selector, $this->filters)) { |
198
|
3 |
|
if ($callbackOrName === true) { |
199
|
1 |
|
unset($this->filters[$selector]); |
200
|
|
|
} else { |
201
|
2 |
|
if (! is_object($callbackOrName)) { |
202
|
1 |
|
$filter = $this->filterFactory->createFilter($callbackOrName); |
|
|
|
|
203
|
|
|
} else { |
204
|
1 |
|
$filter = $callbackOrName; |
205
|
|
|
} |
206
|
|
|
/* @var $filterSet FilterSet */ |
207
|
2 |
|
$filterSet = $this->filters[$selector]; |
208
|
2 |
|
$filterSet->remove($filter); |
209
|
|
|
} |
210
|
|
|
} |
211
|
3 |
|
$this->compiledAllowedSelectors = []; |
212
|
|
|
|
213
|
3 |
|
return $this; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Retrieve all filters stack |
218
|
|
|
* |
219
|
|
|
* @return array |
220
|
|
|
*/ |
221
|
1 |
|
public function getFilters() |
222
|
|
|
{ |
223
|
1 |
|
return $this->filters; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* Apply filters to an array |
228
|
|
|
* |
229
|
|
|
* @param array $data |
230
|
|
|
* @return array |
231
|
|
|
*/ |
232
|
15 |
|
public function filter(array $data = []) |
233
|
|
|
{ |
234
|
|
|
// first apply the filters to the ROOT |
235
|
15 |
|
if (isset($this->filters[self::SELECTOR_ROOT])) { |
236
|
|
|
/* @var $rootFilters FilterSet */ |
237
|
1 |
|
$rootFilters = $this->filters[self::SELECTOR_ROOT]; |
238
|
1 |
|
$data = $rootFilters->applyFilters($data); |
239
|
|
|
} |
240
|
|
|
|
241
|
15 |
|
$this->compileAllowedSelectors(); |
242
|
|
|
|
243
|
15 |
|
$result = []; |
244
|
15 |
|
foreach ($data as $key => $value) { |
245
|
15 |
|
if ($this->itemIsAllowed($key)) { |
246
|
15 |
|
$result[$key] = $this->filterItem($data, $key); |
247
|
|
|
} |
248
|
|
|
} |
249
|
15 |
|
return $result; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Apply filters on a single item in the array |
254
|
|
|
* |
255
|
|
|
* @param array $data |
256
|
|
|
* @param string $valueIdentifier |
257
|
|
|
* @return mixed |
258
|
|
|
*/ |
259
|
15 |
|
public function filterItem($data, $valueIdentifier) |
260
|
|
|
{ |
261
|
15 |
|
$value = Utils::arrayGetByPath($data, $valueIdentifier); |
262
|
15 |
|
$value = $this->applyFilters($value, $valueIdentifier, $data); |
263
|
15 |
|
if (is_array($value)) { |
264
|
5 |
|
$result = []; |
265
|
5 |
|
foreach (array_keys($value) as $k) { |
266
|
5 |
|
if ($this->itemIsAllowed("{$valueIdentifier}[{$k}]")) { |
267
|
5 |
|
$result[$k] = $this->filterItem($data, "{$valueIdentifier}[{$k}]"); |
268
|
|
|
} |
269
|
|
|
} |
270
|
5 |
|
return $result; |
271
|
|
|
} |
272
|
15 |
|
return $value; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Apply filters to a single value |
277
|
|
|
* |
278
|
|
|
* @param mixed $value |
279
|
|
|
* value of the item |
280
|
|
|
* @param string $valueIdentifier |
281
|
|
|
* array element path (eg: 'key' or 'key[0][subkey]') |
282
|
|
|
* @param mixed $context |
283
|
|
|
* @return mixed |
284
|
|
|
*/ |
285
|
15 |
|
public function applyFilters($value, $valueIdentifier, $context) |
286
|
|
|
{ |
287
|
15 |
|
foreach ($this->filters as $selector => $filterSet) { |
288
|
|
|
/* @var $filterSet FilterSet */ |
289
|
15 |
|
if ($selector != self::SELECTOR_ROOT && Utils::itemMatchesSelector($valueIdentifier, $selector)) { |
290
|
14 |
|
$value = $filterSet->applyFilters($value, $valueIdentifier, $context); |
291
|
|
|
} |
292
|
|
|
} |
293
|
15 |
|
return $value; |
294
|
|
|
} |
295
|
|
|
|
296
|
15 |
|
private function itemIsAllowed($item) |
297
|
|
|
{ |
298
|
15 |
|
if (empty($this->compiledAllowedSelectors)) { |
299
|
4 |
|
return true; |
300
|
|
|
} |
301
|
11 |
|
foreach ($this->compiledAllowedSelectors as $selector) { |
302
|
11 |
|
if (Utils::itemMatchesSelector($item, $selector)) { |
303
|
11 |
|
return true; |
304
|
|
|
} |
305
|
|
|
} |
306
|
3 |
|
return false; |
307
|
|
|
} |
308
|
|
|
|
309
|
15 |
|
private function compileAllowedSelectors() |
310
|
|
|
{ |
311
|
15 |
|
if (!empty($this->compiledAllowedSelectors)) { |
312
|
|
|
return; |
313
|
|
|
} |
314
|
|
|
|
315
|
15 |
|
$selectors = array_unique(array_merge( |
316
|
15 |
|
array_values($this->allowedSelectors), |
317
|
15 |
|
array_keys($this->filters) |
318
|
|
|
)); |
319
|
|
|
|
320
|
15 |
|
$compiled = []; |
321
|
|
|
|
322
|
15 |
|
foreach ($selectors as $selector) { |
323
|
15 |
|
if ($selector == '/' || $selector == '*') { |
324
|
4 |
|
continue; |
325
|
|
|
} |
326
|
11 |
|
$compiled[] = $selector; |
327
|
11 |
|
while ($lastPart = strrpos($selector, '[')) { |
328
|
2 |
|
$parent = substr($selector, 0, $lastPart); |
329
|
2 |
|
if (!in_array($parent, $compiled)) { |
330
|
2 |
|
$compiled[] = $parent; |
331
|
|
|
} |
332
|
2 |
|
$selector = $parent; |
333
|
|
|
} |
334
|
|
|
} |
335
|
|
|
|
336
|
15 |
|
$this->compiledAllowedSelectors = $compiled; |
337
|
15 |
|
} |
338
|
|
|
} |
339
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.