Failed Conditions
Pull Request — master (#75)
by
unknown
03:37
created

Filterer::filter()   F

Complexity

Conditions 29
Paths 537

Size

Total Lines 114
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 114
rs 2.6263
c 0
b 0
f 0
cc 29
eloc 66
nc 537
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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