Filtrator::applyFilters()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.9332
c 0
b 0
f 0
cc 4
nc 3
nop 3
crap 4
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);
0 ignored issues
show
Bug introduced by
It seems like $callbackOrName defined by parameter $callbackOrName on line 195 can also be of type boolean; however, Sirius\Filtration\FilterFactory::createFilter() does only seem to accept callable, maybe add an additional type check?

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.

Loading history...
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