Completed
Push — master ( 882b0e...431e4c )
by Adrian
01:23
created

Filtrator::setAllowed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 = array();
21
22
    /**
23
     * @var array
24
     */
25
    protected $allowedSelectors = [];
26
27
    /**
28
     * @var array
29
     */
30
    protected $compiledAllowedSelectors = [];
31
32 22
    public function __construct(FilterFactory $filterFactory = null)
33
    {
34 22
        if (!$filterFactory) {
35
            $filterFactory = new FilterFactory();
36
        }
37 22
        $this->filterFactory = $filterFactory;
38 22
    }
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', array('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', array(
58
     *          array('truncate', 'limit=10', true, 10),
59
     *          array('censor', array('words' => array('faggy', '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 21
    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(array(
79
         *          'title' => array('trim', array('truncate', '{"limit":100}'))
80
         *          'description' => array('trim')
81
         *          ));
82
         */
83 21
        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 21
        if (! is_string($selector)) {
91 1
            throw new \InvalidArgumentException('The data selector for filtering must be a string');
92
        }
93
94
95 20
        if (is_string($callbackOrFilterName)) {
96
            // rule was supplied like 'trim' or 'trim | nullify'
97 16
            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 16
            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', array(
110
         *          'trim',
111
         *          array('truncate', '{"limit":100}')
112
         *          ));
113
         */
114 20
        if (is_array($callbackOrFilterName) && ! is_callable($callbackOrFilterName)) {
115 3
            foreach ($callbackOrFilterName as $filter) {
116
                // $filter is something like array('truncate', '{"limit":100}')
117 3
                if (is_array($filter) && ! is_callable($filter)) {
118 1
                    $args = $filter;
119 1
                    array_unshift($args, $selector);
120 1
                    call_user_func_array(array(
121 1
                        $this,
122 1
                        'add'
123 1
                    ), $args);
124 2
                } elseif (is_string($filter) || is_callable($filter)) {
125 2
                    $this->add($selector, $filter);
126
                }
127
            }
128 3
            return $this;
129
        }
130
131 20
        $filter = $this->filterFactory->createFilter($callbackOrFilterName, $options, $recursive);
132 17
        if (! array_key_exists($selector, $this->filters)) {
133 17
            $this->filters[$selector] = new FilterSet();
134
        }
135
        /* @var $filterSet FilterSet */
136 17
        $filterSet = $this->filters[$selector];
137 17
        $filterSet->insert($filter, $priority);
138 17
        $this->compiledAllowedSelectors = [];
139
140 17
        return $this;
141
    }
142
143
    /**
144
     * Converts a rule that was supplied as string into a set of options that define the rule
145
     *
146
     * @example 'minLength({"min":2})(true)(10)'
147
     *
148
     *          will be converted into
149
     *
150
     *          array(
151
     *          'minLength', // validator name
152
     *          array('min' => 2'), // validator options
153
     *          true, // recursive
154
     *          10 // priority
155
     *          )
156
     * @param string $ruleAsString
157
     * @return array
158
     */
159 2
    protected function parseRule($ruleAsString)
160
    {
161 2
        $ruleAsString = trim($ruleAsString);
162
163 2
        $options = array();
164 2
        $recursive = false;
165 2
        $priority = 0;
166
167 2
        $name = substr($ruleAsString, 0, strpos($ruleAsString, '('));
168 2
        $ruleAsString = substr($ruleAsString, strpos($ruleAsString, '('));
169 2
        $matches = array();
170 2
        preg_match_all('/\(([^\)]*)\)/', $ruleAsString, $matches);
171
172 2
        if (isset($matches[1])) {
173 2
            if (isset($matches[1][0]) && $matches[1][0]) {
174 2
                $options = $matches[1][0];
175
            }
176 2
            if (isset($matches[1][1]) && $matches[1][1]) {
177 2
                $recursive = (in_array($matches[1][1], array(true, 'TRUE', 'true', 1))) ? true : false;
178
            }
179 2
            if (isset($matches[1][2]) && $matches[1][2]) {
180 2
                $priority = (int)$matches[1][2];
181
            }
182
        }
183
184
        return array(
185 2
            $name,
186 2
            $options,
187 2
            $recursive,
188 2
            $priority
189
        );
190
    }
191
192
    /**
193
     * Remove a filter from the stack
194
     *
195
     * @param string $selector
196
     * @param bool|callable|string|TRUE $callbackOrName
197
     * @throws \InvalidArgumentException
198
     * @return \Sirius\Filtration\Filtrator
199
     */
200 3
    public function remove($selector, $callbackOrName = true)
201
    {
202 3
        if (array_key_exists($selector, $this->filters)) {
203 3
            if ($callbackOrName === true) {
204 1
                unset($this->filters[$selector]);
205
            } else {
206 2
                if (! is_object($callbackOrName)) {
207 1
                    $filter = $this->filterFactory->createFilter($callbackOrName);
0 ignored issues
show
Bug introduced by
It seems like $callbackOrName defined by parameter $callbackOrName on line 200 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...
208
                } else {
209 1
                    $filter = $callbackOrName;
210
                }
211
                /* @var $filterSet FilterSet */
212 2
                $filterSet = $this->filters[$selector];
213 2
                $filterSet->remove($filter);
214
            }
215
        }
216 3
        $this->compiledAllowedSelectors = [];
217
218 3
        return $this;
219
    }
220
221
    /**
222
     * Retrieve all filters stack
223
     *
224
     * @return array
225
     */
226 2
    public function getFilters()
227
    {
228 2
        return $this->filters;
229
    }
230
231
    /**
232
     * Apply filters to an array
233
     *
234
     * @param array $data
235
     * @return array
236
     */
237 16
    public function filter($data = array())
238
    {
239 16
        if (! is_array($data)) {
240 1
            return $data;
241
        }
242
243
        // first apply the filters to the ROOT
244 15
        if (isset($this->filters[self::SELECTOR_ROOT])) {
245
            /* @var $rootFilters FilterSet */
246 1
            $rootFilters = $this->filters[self::SELECTOR_ROOT];
247 1
            $data = $rootFilters->applyFilters($data);
248
        }
249
250 15
        $this->compileAllowedSelectors();
251
252 15
        $result = [];
253 15
        foreach ($data as $key => $value) {
254 15
            if ($this->itemIsAllowed($key)) {
255 15
                $result[$key] = $this->filterItem($data, $key);
256
            }
257
        }
258 15
        return $result;
259
    }
260
261
    /**
262
     * Apply filters on a single item in the array
263
     *
264
     * @param array $data
265
     * @param string $valueIdentifier
266
     * @return mixed
267
     */
268 15
    public function filterItem($data, $valueIdentifier)
269
    {
270 15
        $value = Utils::arrayGetByPath($data, $valueIdentifier);
271 15
        $value = $this->applyFilters($value, $valueIdentifier, $data);
272 15
        if (is_array($value)) {
273 5
            $result = [];
274 5
            foreach (array_keys($value) as $k) {
275 5
                if ($this->itemIsAllowed("{$valueIdentifier}[{$k}]")) {
276 5
                    $result[$k] = $this->filterItem($data, "{$valueIdentifier}[{$k}]");
277
                }
278
            }
279 5
            return $result;
280
        }
281 15
        return $value;
282
    }
283
284
    /**
285
     * Apply filters to a single value
286
     *
287
     * @param mixed $value
288
     *            value of the item
289
     * @param string $valueIdentifier
290
     *            array element path (eg: 'key' or 'key[0][subkey]')
291
     * @param mixed $context
292
     * @return mixed
293
     */
294 15
    public function applyFilters($value, $valueIdentifier, $context)
295
    {
296 15
        foreach ($this->filters as $selector => $filterSet) {
297
            /* @var $filterSet FilterSet */
298 15
            if ($selector != self::SELECTOR_ROOT && Utils::itemMatchesSelector($valueIdentifier, $selector)) {
299 14
                $value = $filterSet->applyFilters($value, $valueIdentifier, $context);
300
            }
301
        }
302 15
        return $value;
303
    }
304
305 15
    private function itemIsAllowed($item)
306
    {
307 15
        if (empty($this->compiledAllowedSelectors)) {
308 4
            return true;
309
        }
310 11
        foreach ($this->compiledAllowedSelectors as $selector) {
311 11
            if (Utils::itemMatchesSelector($item, $selector)) {
312 11
                return true;
313
            }
314
        }
315 3
        return false;
316
    }
317
318 15
    private function compileAllowedSelectors()
319
    {
320 15
        if (!empty($this->compiledAllowedSelectors)) {
321
            return;
322
        }
323
324 15
        $selectors = array_unique(array_merge(
325 15
            array_values($this->allowedSelectors),
326 15
            array_keys($this->filters)
327
        ));
328
329 15
        $compiled = [];
330
331 15
        foreach ($selectors as $selector) {
332 15
            if ($selector == '/' || $selector == '*') {
333 4
                continue;
334
            }
335 11
            $compiled[] = $selector;
336 11
            while ($lastPart = strrpos($selector, '[')) {
337 2
                $parent = substr($selector, 0, $lastPart);
338 2
                if (!in_array($parent, $compiled)) {
339 2
                    $compiled[] = $parent;
340
                }
341 2
                $selector = $parent;
342
            }
343
        }
344
345 15
        $this->compiledAllowedSelectors = $compiled;
346 15
    }
347
}
348