Completed
Push — master ( 4bd39c...a2097d )
by Adrian
01:31
created

Filtrator::itemIsAllowed()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 9.8666
c 0
b 0
f 0
cc 4
nc 4
nop 1
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 = array();
21
22
    /**
23
     * @var array
24
     */
25
    protected $allowedSelectors;
26
27 22
    public function __construct(FilterFactory $filterFactory = null)
28
    {
29 22
        if (!$filterFactory) {
30
            $filterFactory = new FilterFactory();
31
        }
32 22
        $this->filterFactory = $filterFactory;
33 22
    }
34
35 1
    public function setAllowedSelectors(array $allowedSelectors = [])
36
    {
37 1
        foreach (array_values($allowedSelectors) as $selector) {
38 1
            while ($lastPart = strrpos($selector, '[')) {
39 1
                $parent = substr($selector, 0, $lastPart);
40 1
                if (!in_array($parent, $allowedSelectors)) {
41 1
                    $allowedSelectors[] = $parent;
42
                }
43 1
                $selector = $parent;
44
            }
45
        }
46 1
        $this->allowedSelectors = $allowedSelectors;
47 1
    }
48
49
    /**
50
     * Add a filter to the filters stack
51
     *
52
     * @example // normal callback
53
     *          $filtrator->add('title', '\strip_tags');
54
     *          // anonymous function
55
     *          $filtrator->add('title', function($value){ return $value . '!!!'; });
56
     *          // filter class from the library registered on the $filtersMap
57
     *          $filtrator->add('title', 'normalizedate', array('format' => 'm/d/Y'));
58
     *          // custom class
59
     *          $filtrator->add('title', '\MyApp\Filters\CustomFilter');
60
     *          // multiple filters as once with different ways to pass options
61
     *          $filtrator->add('title', array(
62
     *          array('truncate', 'limit=10', true, 10),
63
     *          array('censor', array('words' => array('faggy', 'idiot'))
64
     *          ));
65
     *          // multiple fitlers as a single string
66
     *          $filtrator->add('title', 'stringtrim(side=left)(true)(10) | truncate(limit=100)');
67
     * @param string|array $selector
68
     * @param mixed $callbackOrFilterName
69
     * @param array|null $options
70
     * @param bool $recursive
71
     * @param integer $priority
72
     * @throws \InvalidArgumentException
73
     * @internal param $ callable|filter class name|\Sirius\Filtration\Filter\AbstractFilter $callbackOrFilterName
74
     * @internal param array|string $params
75
     * @return self
76
     */
77 20
    public function add($selector, $callbackOrFilterName = null, $options = null, $recursive = false, $priority = 0)
78
    {
79
        /**
80
         * $selector is actually an array with filters
81
         *
82
         * @example $filtrator->add(array(
83
         *          'title' => array('trim', array('truncate', '{"limit":100}'))
84
         *          'description' => array('trim')
85
         *          ));
86
         */
87 20
        if (is_array($selector)) {
88 1
            foreach ($selector as $key => $filters) {
89 1
                $this->add($key, $filters);
90
            }
91 1
            return $this;
92
        }
93
94 20
        if (! is_string($selector)) {
95 1
            throw new \InvalidArgumentException('The data selector for filtering must be a string');
96
        }
97
98
99 19
        if (is_string($callbackOrFilterName)) {
100
            // rule was supplied like 'trim' or 'trim | nullify'
101 15
            if (strpos($callbackOrFilterName, ' | ') !== false) {
102 1
                return $this->add($selector, explode(' | ', $callbackOrFilterName));
103
            }
104
            // rule was supplied like this 'trim(limit=10)(true)(10)'
105 15
            if (strpos($callbackOrFilterName, '(') !== false) {
106 2
                list($callbackOrFilterName, $options, $recursive, $priority) = $this->parseRule($callbackOrFilterName);
107
            }
108
        }
109
110
        /**
111
         * The $callbackOrFilterName is an array of filters
112
         *
113
         * @example $filtrator->add('title', array(
114
         *          'trim',
115
         *          array('truncate', '{"limit":100}')
116
         *          ));
117
         */
118 19
        if (is_array($callbackOrFilterName) && ! is_callable($callbackOrFilterName)) {
119 3
            foreach ($callbackOrFilterName as $filter) {
120
                // $filter is something like array('truncate', '{"limit":100}')
121 3
                if (is_array($filter) && ! is_callable($filter)) {
122 1
                    $args = $filter;
123 1
                    array_unshift($args, $selector);
124 1
                    call_user_func_array(array(
125 1
                        $this,
126 1
                        'add'
127 1
                    ), $args);
128 2
                } elseif (is_string($filter) || is_callable($filter)) {
129 2
                    $this->add($selector, $filter);
130
                }
131
            }
132 3
            return $this;
133
        }
134
135 19
        $filter = $this->filterFactory->createFilter($callbackOrFilterName, $options, $recursive);
136 16
        if (! array_key_exists($selector, $this->filters)) {
137 16
            $this->filters[$selector] = new FilterSet();
138
        }
139
        /* @var $filterSet FilterSet */
140 16
        $filterSet = $this->filters[$selector];
141 16
        $filterSet->insert($filter, $priority);
142 16
        return $this;
143
    }
144
145
    /**
146
     * Converts a rule that was supplied as string into a set of options that define the rule
147
     *
148
     * @example 'minLength({"min":2})(true)(10)'
149
     *
150
     *          will be converted into
151
     *
152
     *          array(
153
     *          'minLength', // validator name
154
     *          array('min' => 2'), // validator options
155
     *          true, // recursive
156
     *          10 // priority
157
     *          )
158
     * @param string $ruleAsString
159
     * @return array
160
     */
161 2
    protected function parseRule($ruleAsString)
162
    {
163 2
        $ruleAsString = trim($ruleAsString);
164
165 2
        $options = array();
166 2
        $recursive = false;
167 2
        $priority = 0;
168
169 2
        $name = substr($ruleAsString, 0, strpos($ruleAsString, '('));
170 2
        $ruleAsString = substr($ruleAsString, strpos($ruleAsString, '('));
171 2
        $matches = array();
172 2
        preg_match_all('/\(([^\)]*)\)/', $ruleAsString, $matches);
173
174 2
        if (isset($matches[1])) {
175 2
            if (isset($matches[1][0]) && $matches[1][0]) {
176 2
                $options = $matches[1][0];
177
            }
178 2
            if (isset($matches[1][1]) && $matches[1][1]) {
179 2
                $recursive = (in_array($matches[1][1], array(true, 'TRUE', 'true', 1))) ? true : false;
180
            }
181 2
            if (isset($matches[1][2]) && $matches[1][2]) {
182 2
                $priority = (int)$matches[1][2];
183
            }
184
        }
185
186
        return array(
187 2
            $name,
188 2
            $options,
189 2
            $recursive,
190 2
            $priority
191
        );
192
    }
193
194
    /**
195
     * Remove a filter from the stack
196
     *
197
     * @param string $selector
198
     * @param bool|callable|string|TRUE $callbackOrName
199
     * @throws \InvalidArgumentException
200
     * @return \Sirius\Filtration\Filtrator
201
     */
202 3
    public function remove($selector, $callbackOrName = true)
203
    {
204 3
        if (array_key_exists($selector, $this->filters)) {
205 3
            if ($callbackOrName === true) {
206 1
                unset($this->filters[$selector]);
207
            } else {
208 2
                if (! is_object($callbackOrName)) {
209 1
                    $filter = $this->filterFactory->createFilter($callbackOrName);
0 ignored issues
show
Bug introduced by
It seems like $callbackOrName defined by parameter $callbackOrName on line 202 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...
210
                } else {
211 1
                    $filter = $callbackOrName;
212
                }
213
                /* @var $filterSet FilterSet */
214 2
                $filterSet = $this->filters[$selector];
215 2
                $filterSet->remove($filter);
216
            }
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
        // first apply the filters to the ROOT
243 15
        if (isset($this->filters[self::SELECTOR_ROOT])) {
244
            /* @var $rootFilters FilterSet */
245 1
            $rootFilters = $this->filters[self::SELECTOR_ROOT];
246 1
            $data = $rootFilters->applyFilters($data);
247
        }
248 15
        $result = [];
249 15
        foreach ($data as $key => $value) {
250 15
            if ($this->itemIsAllowed($key)) {
251 15
                $result[$key] = $this->filterItem($data, $key);
252
            }
253
        }
254 15
        return $result;
255
    }
256
257
    /**
258
     * Apply filters on a single item in the array
259
     *
260
     * @param array $data
261
     * @param string $valueIdentifier
262
     * @return mixed
263
     */
264 15
    public function filterItem($data, $valueIdentifier)
265
    {
266 15
        $value = Utils::arrayGetByPath($data, $valueIdentifier);
267 15
        $value = $this->applyFilters($value, $valueIdentifier, $data);
268 15
        if (is_array($value)) {
269 6
            $result = [];
270 6
            foreach (array_keys($value) as $k) {
271 6
                if ($this->itemIsAllowed("{$valueIdentifier}[{$k}]")) {
272 6
                    $result[$k] = $this->filterItem($data, "{$valueIdentifier}[{$k}]");
273
                }
274
            }
275 6
            return $result;
276
        }
277 15
        return $value;
278
    }
279
280
    /**
281
     * Apply filters to a single value
282
     *
283
     * @param mixed $value
284
     *            value of the item
285
     * @param string $valueIdentifier
286
     *            array element path (eg: 'key' or 'key[0][subkey]')
287
     * @param mixed $context
288
     * @return mixed
289
     */
290 15
    public function applyFilters($value, $valueIdentifier, $context)
291
    {
292 15
        foreach ($this->filters as $selector => $filterSet) {
293
            /* @var $filterSet FilterSet */
294 14
            if ($selector != self::SELECTOR_ROOT && Utils::itemMatchesSelector($valueIdentifier, $selector)) {
295 13
                $value = $filterSet->applyFilters($value, $valueIdentifier, $context);
296
            }
297
        }
298 15
        return $value;
299
    }
300
301 15
    private function itemIsAllowed($item)
302
    {
303 15
        if (empty($this->allowedSelectors)) {
304 14
            return true;
305
        }
306 1
        foreach ($this->allowedSelectors as $selector) {
307 1
            if (Utils::itemMatchesSelector($item, $selector)) {
308 1
                return true;
309
            }
310
        }
311 1
        return false;
312
    }
313
}
314