Filterer::setFilterAliases()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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