Failed Conditions
Pull Request — master (#75)
by
unknown
01:29
created

Filterer   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 0
dl 0
loc 271
rs 8.2608
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
F filter() 0 114 29
A getFilterAliases() 0 4 1
A setFilterAliases() 0 13 3
B registerAlias() 0 16 7

How to fix   Complexity   

Complex Class

Complex classes like Filterer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Filterer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace TraderInteractive;
4
5
use Exception;
6
7
/**
8
 * Class to filter an array of input.
9
 */
10
final class Filterer
11
{
12
    private static $filterAliases = [
13
        'in' => '\TraderInteractive\Filter\Arrays::in',
14
        'array' => '\TraderInteractive\Filter\Arrays::filter',
15
        'bool' => '\TraderInteractive\Filter\Booleans::filter',
16
        'float' => '\TraderInteractive\Filter\Floats::filter',
17
        'int' => '\TraderInteractive\Filter\Ints::filter',
18
        'bool-convert' => '\TraderInteractive\Filter\Booleans::convert',
19
        'uint' => '\TraderInteractive\Filter\UnsignedInt::filter',
20
        'string' => '\TraderInteractive\Filter\Strings::filter',
21
        'ofScalars' => '\TraderInteractive\Filter\Arrays::ofScalars',
22
        'ofArrays' => '\TraderInteractive\Filter\Arrays::ofArrays',
23
        'ofArray' => '\TraderInteractive\Filter\Arrays::ofArray',
24
        'url' => '\TraderInteractive\Filter\Url::filter',
25
        'email' => '\TraderInteractive\Filter\Email::filter',
26
        'explode' => '\TraderInteractive\Filter\Strings::explode',
27
        'flatten' => '\TraderInteractive\Filter\Arrays::flatten',
28
        'date' => '\TraderInteractive\Filter\DateTime::filter',
29
        'date-format' => '\TraderInteractive\Filter\DateTime::format',
30
        'timezone' => '\TraderInteractive\Filter\DateTimeZone::filter',
31
    ];
32
33
    /**
34
     * Example:
35
     * <pre>
36
     * <?php
37
     * class AppendFilter
38
     * {
39
     *     public function filter($value, $extraArg)
40
     *     {
41
     *         return $value . $extraArg;
42
     *     }
43
     * }
44
     * $appendFilter = new AppendFilter();
45
     *
46
     * $trimFunc = function($val) { return trim($val); };
47
     *
48
     * list($status, $result, $error, $unknowns) = TraderInteractive\Filterer::filter(
49
     *     [
50
     *         'field one' => [[$trimFunc], ['substr', 0, 3], [[$appendFilter, 'filter'], 'boo']],
51
     *         'field two' => ['required' => true, ['floatval']],
52
     *         'field three' => ['required' => false, ['float']],
53
     *         'field four' => ['required' => true, 'default' => 1, ['uint']],
54
     *     ],
55
     *     ['field one' => ' abcd', 'field two' => '3.14']
56
     * );
57
     *
58
     * var_dump($status);
59
     * var_dump($result);
60
     * var_dump($error);
61
     * var_dump($unknowns);
62
     * </pre>
63
     * prints:
64
     * <pre>
65
     * bool(true)
66
     * array(3) {
67
     *   'field one' =>
68
     *   string(6) "abcboo"
69
     *   'field two' =>
70
     *   double(3.14)
71
     *   'field four' =>
72
     *   int(1)
73
     * }
74
     * NULL
75
     * array(0) {
76
     * }
77
     * </pre>
78
     *
79
     * @param array $spec the specification to apply to the $input. An array where each key is a known input field and
80
     *                    each value is an array of filters. Each filter should be an array with the first member being
81
     *                    anything that can pass is_callable() as well as accepting the value to filter as its first
82
     *                    argument. Two examples would be the string 'trim' or an object function specified like [$obj,
83
     *                    'filter'], see is_callable() documentation. The rest of the members are extra arguments to the
84
     *                    callable. The result of one filter will be the first argument to the next filter. In addition
85
     *                    to the filters, the specification values may contain a 'required' key (default false) that
86
     *                    controls the same behavior as the 'defaultRequired' option below but on a per field basis. A
87
     *                    'default' specification value may be used to substitute in a default to the $input when the
88
     *                    key is not present (whether 'required' is specified or not).
89
     * @param array $input the input the apply the $spec on.
90
     * @param array $options 'allowUnknowns' (default false) true to allow unknowns or false to treat as error,
91
     *                       'defaultRequired' (default false) true to make fields required by default and treat as
92
     *                       error on absence and false to allow their absence by default
93
     *
94
     * @return array on success [true, $input filtered, null, array of unknown fields]
95
     *     on error [false, null, 'error message', array of unknown fields]
96
     *
97
     * @throws Exception
98
     * @throws \InvalidArgumentException if 'allowUnknowns' option was not a bool
99
     * @throws \InvalidArgumentException if 'defaultRequired' option was not a bool
100
     * @throws \InvalidArgumentException if filters for a field was not a array
101
     * @throws \InvalidArgumentException if a filter for a field was not a array
102
     * @throws \InvalidArgumentException if 'required' for a field was not a bool
103
     */
104
    public static function filter(array $spec, array $input, array $options = []) : array
105
    {
106
        $options += ['allowUnknowns' => false, 'defaultRequired' => false];
107
108
        $allowUnknowns = $options['allowUnknowns'];
109
        $defaultRequired = $options['defaultRequired'];
110
111
        if ($allowUnknowns !== false && $allowUnknowns !== true) {
112
            throw new \InvalidArgumentException("'allowUnknowns' option was not a bool");
113
        }
114
115
        if ($defaultRequired !== false && $defaultRequired !== true) {
116
            throw new \InvalidArgumentException("'defaultRequired' option was not a bool");
117
        }
118
119
        $inputToFilter = array_intersect_key($input, $spec);
120
        $leftOverSpec = array_diff_key($spec, $input);
121
        $leftOverInput = array_diff_key($input, $spec);
122
123
        $errors = [];
124
        foreach ($inputToFilter as $field => $value) {
125
            $filters = $spec[$field];
126
127
            if (!is_array($filters)) {
128
                throw new \InvalidArgumentException("filters for field '{$field}' was not a array");
129
            }
130
131
            $customError = null;
132
            if (array_key_exists('error', $filters)) {
133
                $customError = $filters['error'];
134
                if (!is_string($customError) || trim($customError) === '') {
135
                    throw new \InvalidArgumentException("error for field '{$field}' was not a non-empty string");
136
                }
137
138
                unset($filters['error']);//unset so its not used as a filter
139
            }
140
141
            unset($filters['required']);//doesn't matter if required since we have this one
142
            unset($filters['default']);//doesn't matter if there is a default since we have a value
143
            foreach ($filters as $filter) {
144
                if (!is_array($filter)) {
145
                    throw new \InvalidArgumentException("filter for field '{$field}' was not a array");
146
                }
147
148
                if (empty($filter)) {
149
                    continue;
150
                }
151
152
                $function = array_shift($filter);
153
                if ((is_string($function) || is_int($function)) && array_key_exists($function, self::$filterAliases)) {
154
                    $function = self::$filterAliases[$function];
155
                }
156
157
                if (!is_callable($function)) {
158
                    throw new Exception(
159
                        "Function '" . trim(var_export($function, true), "'") . "' for field '{$field}' is not callable"
160
                    );
161
                }
162
163
                array_unshift($filter, $value);
164
                try {
165
                    $value = call_user_func_array($function, $filter);
166
                } catch (Exception $e) {
167
                    $error = $customError;
168
                    if ($error === null) {
169
                        $error = sprintf(
170
                            "Field '%s' with value '%s' failed filtering, message '%s'",
171
                            $field,
172
                            trim(var_export($value, true), "'"),
173
                            $e->getMessage()
174
                        );
175
                    }
176
177
                    $errors[] = $error;
178
                    continue 2;//next field
179
                }
180
            }
181
182
            $inputToFilter[$field] = $value;
183
        }
184
185
        foreach ($leftOverSpec as $field => $filters) {
186
            if (!is_array($filters)) {
187
                throw new \InvalidArgumentException("filters for field '{$field}' was not a array");
188
            }
189
190
            $required = isset($filters['required']) ? $filters['required'] : $defaultRequired;
191
192
            if ($required !== false && $required !== true) {
193
                throw new \InvalidArgumentException("'required' for field '{$field}' was not a bool");
194
            }
195
196
            if (array_key_exists('default', $filters)) {
197
                $inputToFilter[$field] = $filters['default'];
198
                continue;
199
            }
200
201
            if ($required) {
202
                $errors[] = "Field '{$field}' was required and not present";
203
            }
204
        }
205
206
        if (!$allowUnknowns) {
207
            foreach ($leftOverInput as $field => $value) {
208
                $errors[] = "Field '{$field}' with value '" . trim(var_export($value, true), "'") . "' is unknown";
209
            }
210
        }
211
212
        if (empty($errors)) {
213
            return [true, $inputToFilter, null, $leftOverInput];
214
        }
215
216
        return [false, null, implode("\n", $errors), $leftOverInput];
217
    }
218
219
    /**
220
     * Return the filter aliases.
221
     *
222
     * @return array array where keys are aliases and values pass is_callable().
223
     */
224
    public static function getFilterAliases() : array
225
    {
226
        return self::$filterAliases;
227
    }
228
229
    /**
230
     * Set the filter aliases.
231
     *
232
     * @param array $aliases array where keys are aliases and values pass is_callable().
233
     * @return void
234
     *
235
     * @throws Exception Thrown if any of the given $aliases is not valid. @see registerAlias()
236
     */
237
    public static function setFilterAliases(array $aliases)
238
    {
239
        $originalAliases = self::$filterAliases;
240
        self::$filterAliases = [];
241
        try {
242
            foreach ($aliases as $alias => $callback) {
243
                self::registerAlias($alias, $callback);
244
            }
245
        } catch (Exception $e) {
246
            self::$filterAliases = $originalAliases;
247
            throw $e;
248
        }
249
    }
250
251
    /**
252
     * Register a new alias with the Filterer
253
     *
254
     * @param string|int $alias the alias to register
255
     * @param callable $filter the aliased callable filter
256
     * @param bool $overwrite Flag to overwrite existing alias if it exists
257
     *
258
     * @return void
259
     *
260
     * @throws \InvalidArgumentException if $alias was not a string or int
261
     * @throws \InvalidArgumentException if $overwrite was not a bool
262
     * @throws Exception if $overwrite is false and $alias exists
263
     */
264
    public static function registerAlias($alias, callable $filter, bool $overwrite = false)
265
    {
266
        if (!is_string($alias) && !is_int($alias)) {
267
            throw new \InvalidArgumentException('$alias was not a string or int');
268
        }
269
270
        if ($overwrite !== false && $overwrite !== true) {
271
            throw new \InvalidArgumentException('$overwrite was not a bool');
272
        }
273
274
        if (array_key_exists($alias, self::$filterAliases) && !$overwrite) {
275
            throw new Exception("Alias '{$alias}' exists");
276
        }
277
278
        self::$filterAliases[$alias] = $filter;
279
    }
280
}
281