Completed
Push — master ( 9f9659...98edfc )
by Adrian
01:34
created

Filtrator::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

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