Filterer   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 728
Duplicated Lines 0 %

Importance

Changes 14
Bugs 0 Features 0
Metric Value
wmc 96
eloc 254
c 14
b 0
f 0
dl 0
loc 728
rs 2

35 Methods

Rating   Name   Duplication   Size   Complexity  
A addUsedInputToFilter() 0 10 3
A getFilterAliases() 0 3 1
A getAllowUnknowns() 0 8 3
A ofArrays() 0 24 5
A handleFilterAliases() 0 7 4
A assertFiltersIsAnArray() 0 4 2
A registerAlias() 0 5 1
A checkForUnknowns() 0 7 2
A validateThrowOnError() 0 16 4
A __construct() 0 8 1
A validateCustomError() 0 15 4
A getAliases() 0 3 1
A assertIfAliasExists() 0 4 3
A extractUses() 0 5 2
A withSpecification() 0 3 1
A getOptions() 0 5 1
A assertFilterIsArray() 0 4 2
A getSpecification() 0 3 1
C execute() 0 79 12
A withAliases() 0 3 1
A handleAllowUnknowns() 0 7 2
A setFilterAliases() 0 11 3
A handleConflicts() 0 15 5
A getDefaultRequired() 0 10 3
A handleRequiredFields() 0 6 2
A extractConflicts() 0 15 3
A ofScalars() 0 13 3
A validateReturnOnNull() 0 16 4
A filter() 0 9 1
A assertFunctionIsCallable() 0 5 2
A ofArray() 0 8 2
A generateFilterResponse() 0 16 4
A assertIfStringOrInt() 0 4 3
A handleCustomError() 0 15 2
A getRequired() 0 10 3

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.

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
use InvalidArgumentException;
7
use Throwable;
8
use TraderInteractive\Exceptions\FilterException;
9
use TraderInteractive\Filter\Arrays;
10
use TraderInteractive\Filter\Json;
11
use TraderInteractive\Filter\PhoneFilter;
12
use TraderInteractive\Filter\TimeOfDayFilter;
13
use TraderInteractive\Filter\XmlFilter;
14
15
/**
16
 * Class to filter an array of input.
17
 */
18
final class Filterer implements FiltererInterface
19
{
20
    /**
21
     * @var array
22
     */
23
    const DEFAULT_FILTER_ALIASES = [
24
        'array' => '\\TraderInteractive\\Filter\\Arrays::filter',
25
        'array-copy' => Arrays::class . '::copy',
26
        'array-copy-each' => Arrays::class . '::copyEach',
27
        'array-pad' => Arrays::class . '::pad',
28
        'arrayize' => '\\TraderInteractive\\Filter\\Arrays::arrayize',
29
        'bool' => '\\TraderInteractive\\Filter\\Booleans::filter',
30
        'bool-convert' => '\\TraderInteractive\\Filter\\Booleans::convert',
31
        'compress-string' => '\\TraderInteractive\\Filter\\Strings::compress',
32
        'concat' => '\\TraderInteractive\\Filter\\Strings::concat',
33
        'date' => '\\TraderInteractive\\Filter\\DateTime::filter',
34
        'date-format' => '\\TraderInteractive\\Filter\\DateTime::format',
35
        'email' => '\\TraderInteractive\\Filter\\Email::filter',
36
        'explode' => '\\TraderInteractive\\Filter\\Strings::explode',
37
        'flatten' => '\\TraderInteractive\\Filter\\Arrays::flatten',
38
        'float' => '\\TraderInteractive\\Filter\\Floats::filter',
39
        'implode' => Arrays::class . '::implode',
40
        'in' => '\\TraderInteractive\\Filter\\Arrays::in',
41
        'int' => '\\TraderInteractive\\Filter\\Ints::filter',
42
        'json' => Json::class . '::validate',
43
        'json-decode' => Json::class . '::parse',
44
        'ofArray' => '\\TraderInteractive\\Filterer::ofArray',
45
        'ofArrays' => '\\TraderInteractive\\Filterer::ofArrays',
46
        'ofScalars' => '\\TraderInteractive\\Filterer::ofScalars',
47
        'phone' => PhoneFilter::class . '::filter',
48
        'redact' => '\\TraderInteractive\\Filter\\Strings::redact',
49
        'string' => '\\TraderInteractive\\Filter\\Strings::filter',
50
        'strip-tags' => '\\TraderInteractive\\Filter\\Strings::stripTags',
51
        'time-of-day' => TimeOfDayFilter::class . '::filter',
52
        'timezone' => '\\TraderInteractive\\Filter\\DateTimeZone::filter',
53
        'translate' => '\\TraderInteractive\\Filter\\Strings::translate',
54
        'uint' => '\\TraderInteractive\\Filter\\UnsignedInt::filter',
55
        'url' => '\\TraderInteractive\\Filter\\Url::filter',
56
        'xml' => XmlFilter::class . '::filter',
57
        'xml-extract' => XmlFilter::class . '::extract',
58
        'xml-validate' => XmlFilter::class . '::validate',
59
    ];
60
61
    /**
62
     * @var array
63
     */
64
    const DEFAULT_OPTIONS = [
65
        FiltererOptions::ALLOW_UNKNOWNS => false,
66
        FiltererOptions::DEFAULT_REQUIRED => false,
67
        FiltererOptions::RESPONSE_TYPE => self::RESPONSE_TYPE_ARRAY,
68
    ];
69
70
    /**
71
     * @var string
72
     */
73
    const RESPONSE_TYPE_ARRAY = 'array';
74
75
    /**
76
     * @var string
77
     */
78
    const RESPONSE_TYPE_FILTER = FilterResponse::class;
79
80
    /**
81
     * @var string
82
     */
83
    const INVALID_BOOLEAN_FILTER_OPTION = "%s for field '%s' was not a boolean value";
84
85
    /**
86
     * @var array
87
     */
88
    private static $registeredFilterAliases = self::DEFAULT_FILTER_ALIASES;
89
90
    /**
91
     * @var array|null
92
     */
93
    private $filterAliases;
94
95
    /**
96
     * @var array
97
     */
98
    private $specification;
99
100
    /**
101
     * @var bool
102
     */
103
    private $allowUnknowns;
104
105
    /**
106
     * @var bool
107
     */
108
    private $defaultRequired;
109
110
    /**
111
     * @param array      $specification The specification to apply to the value.
112
     * @param array      $options       The options apply during filtering.
113
     *                                  'allowUnknowns' (default false) true to allow or false to treat as error.
114
     *                                  'defaultRequired' (default false) true to make fields required by default.
115
     * @param array|null $filterAliases The filter aliases to accept.
116
     *
117
     * @throws InvalidArgumentException if 'allowUnknowns' option was not a bool
118
     * @throws InvalidArgumentException if 'defaultRequired' option was not a bool
119
     */
120
    public function __construct(array $specification, array $options = [], array $filterAliases = null)
121
    {
122
        $options += self::DEFAULT_OPTIONS;
123
124
        $this->specification = $specification;
125
        $this->filterAliases = $filterAliases;
126
        $this->allowUnknowns = self::getAllowUnknowns($options);
127
        $this->defaultRequired = self::getDefaultRequired($options);
128
    }
129
130
    /**
131
     * @param mixed $input The input to filter.
132
     *
133
     * @return FilterResponse
134
     *
135
     * @throws InvalidArgumentException Thrown if the filters for a field were not an array.
136
     * @throws InvalidArgumentException Thrown if any one filter for a field was not an array.
137
     * @throws InvalidArgumentException Thrown if the 'required' value for a field was not a bool.
138
     */
139
    public function execute(array $input) : FilterResponse
140
    {
141
        $filterAliases = $this->getAliases();
142
        $inputToFilter = [];
143
        $leftOverSpec = [];
144
145
        foreach ($this->specification as $field => $specification) {
146
            if (array_key_exists($field, $input)) {
147
                $inputToFilter[$field] = $input[$field];
148
                continue;
149
            }
150
151
            $leftOverSpec[$field] = $specification;
152
        }
153
154
        $leftOverInput = array_diff_key($input, $inputToFilter);
155
156
        $filteredInput = [];
157
        $errors = [];
158
        $conflicts = [];
159
        foreach ($inputToFilter as $field => $input) {
160
            $filters = $this->specification[$field];
161
            self::assertFiltersIsAnArray($filters, $field);
162
            $customError = self::validateCustomError($filters, $field);
163
            $throwOnError = self::validateThrowOnError($filters, $field);
164
            $returnOnNull = self::validateReturnOnNull($filters, $field);
165
            unset($filters[FilterOptions::IS_REQUIRED]);//doesn't matter if required since we have this one
166
            unset($filters[FilterOptions::DEFAULT_VALUE]);//doesn't matter if there is a default since we have a value
167
            $conflicts = self::extractConflicts($filters, $field, $conflicts);
168
169
            foreach ($filters as $filter) {
170
                self::assertFilterIsArray($filter, $field);
171
172
                if (empty($filter)) {
173
                    continue;
174
                }
175
176
                $uses = self::extractUses($filter);
177
178
                $function = array_shift($filter);
179
                $function = self::handleFilterAliases($function, $filterAliases);
180
181
                self::assertFunctionIsCallable($function, $field);
182
183
                array_unshift($filter, $input);
184
                try {
185
                    $this->addUsedInputToFilter($uses, $filteredInput, $field, $filter);
186
                    $input = call_user_func_array($function, $filter);
187
                    if ($input === null && $returnOnNull) {
188
                        break;
189
                    }
190
                } catch (Exception $exception) {
191
                    if ($throwOnError) {
192
                        throw $exception;
193
                    }
194
195
                    $errors = self::handleCustomError($field, $input, $exception, $errors, $customError);
196
                    continue 2;//next field
197
                }
198
            }
199
200
            $filteredInput[$field] = $input;
201
        }
202
203
        foreach ($leftOverSpec as $field => $filters) {
204
            self::assertFiltersIsAnArray($filters, $field);
205
            $required = self::getRequired($filters, $this->defaultRequired, $field);
206
            if (array_key_exists(FilterOptions::DEFAULT_VALUE, $filters)) {
207
                $filteredInput[$field] = $filters[FilterOptions::DEFAULT_VALUE];
208
                continue;
209
            }
210
211
            $errors = self::handleRequiredFields($required, $field, $errors);
212
        }
213
214
        $errors = self::handleAllowUnknowns($this->allowUnknowns, $leftOverInput, $errors);
215
        $errors = self::handleConflicts($filteredInput, $conflicts, $errors);
216
217
        return new FilterResponse($filteredInput, $errors, $leftOverInput);
218
    }
219
220
    /**
221
     * @return array
222
     *
223
     * @see FiltererInterface::getAliases
224
     */
225
    public function getAliases() : array
226
    {
227
        return $this->filterAliases ?? self::$registeredFilterAliases;
228
    }
229
230
    private static function extractConflicts(array &$filters, string $field, array $conflicts) : array
231
    {
232
        $conflictsWith = $filters[FilterOptions::CONFLICTS_WITH] ?? null;
233
        unset($filters[FilterOptions::CONFLICTS_WITH]);
234
        if ($conflictsWith === null) {
235
            return $conflicts;
236
        }
237
238
        if (!is_array($conflictsWith)) {
239
            $conflictsWith = [$conflictsWith];
240
        }
241
242
        $conflicts[$field] = $conflictsWith;
243
244
        return $conflicts;
245
    }
246
247
    private static function handleConflicts(array $inputToFilter, array $conflicts, array $errors)
248
    {
249
        foreach (array_keys($inputToFilter) as $field) {
250
            if (!array_key_exists($field, $conflicts)) {
251
                continue;
252
            }
253
254
            foreach ($conflicts[$field] as $conflictsWith) {
255
                if (array_key_exists($conflictsWith, $inputToFilter)) {
256
                    $errors[] = "Field '{$field}' cannot be given if field '{$conflictsWith}' is present.";
257
                }
258
            }
259
        }
260
261
        return $errors;
262
    }
263
264
    private static function extractUses(&$filters)
265
    {
266
        $uses = $filters[FilterOptions::USES] ?? [];
267
        unset($filters[FilterOptions::USES]);
268
        return is_array($uses) ? $uses : [$uses];
269
    }
270
271
    /**
272
     * @return array
273
     *
274
     * @see FiltererInterface::getSpecification
275
     */
276
    public function getSpecification() : array
277
    {
278
        return $this->specification;
279
    }
280
281
    /**
282
     * @param array $filterAliases
283
     *
284
     * @return FiltererInterface
285
     *
286
     * @see FiltererInterface::withAliases
287
     */
288
    public function withAliases(array $filterAliases) : FiltererInterface
289
    {
290
        return new Filterer($this->specification, $this->getOptions(), $filterAliases);
291
    }
292
293
    /**
294
     * @param array $specification
295
     *
296
     * @return FiltererInterface
297
     *
298
     * @see FiltererInterface::withSpecification
299
     */
300
    public function withSpecification(array $specification) : FiltererInterface
301
    {
302
        return new Filterer($specification, $this->getOptions(), $this->filterAliases);
303
    }
304
305
    /**
306
     * @return array
307
     */
308
    private function getOptions() : array
309
    {
310
        return [
311
            FiltererOptions::DEFAULT_REQUIRED => $this->defaultRequired,
312
            FiltererOptions::ALLOW_UNKNOWNS => $this->allowUnknowns,
313
        ];
314
    }
315
316
    /**
317
     * Example:
318
     * <pre>
319
     * <?php
320
     * class AppendFilter
321
     * {
322
     *     public function filter($value, $extraArg)
323
     *     {
324
     *         return $value . $extraArg;
325
     *     }
326
     * }
327
     * $appendFilter = new AppendFilter();
328
     *
329
     * $trimFunc = function($val) { return trim($val); };
330
     *
331
     * list($status, $result, $error, $unknowns) = TraderInteractive\Filterer::filter(
332
     *     [
333
     *         'field one' => [[$trimFunc], ['substr', 0, 3], [[$appendFilter, 'filter'], 'boo']],
334
     *         'field two' => ['required' => true, ['floatval']],
335
     *         'field three' => ['required' => false, ['float']],
336
     *         'field four' => ['required' => true, 'default' => 1, ['uint']],
337
     *     ],
338
     *     ['field one' => ' abcd', 'field two' => '3.14']
339
     * );
340
     *
341
     * var_dump($status);
342
     * var_dump($result);
343
     * var_dump($error);
344
     * var_dump($unknowns);
345
     * </pre>
346
     * prints:
347
     * <pre>
348
     * bool(true)
349
     * array(3) {
350
     *   'field one' =>
351
     *   string(6) "abcboo"
352
     *   'field two' =>
353
     *   double(3.14)
354
     *   'field four' =>
355
     *   int(1)
356
     * }
357
     * NULL
358
     * array(0) {
359
     * }
360
     * </pre>
361
     *
362
     * @param array $specification The specification to apply to the input.
363
     * @param array $input          The input the apply the specification to.
364
     * @param array $options        The options apply during filtering.
365
     *                              'allowUnknowns' (default false) true to allow or false to treat as error.
366
     *                              'defaultRequired' (default false) true to make fields required by default.
367
     *                              'responseType' (default RESPONSE_TYPE_ARRAY)
368
     *                                  Determines the return type, as described in the return section.
369
     *
370
     * @return array|FilterResponse If 'responseType' option is RESPONSE_TYPE_ARRAY:
371
     *                                  On success: [true, $input filtered, null, array of unknown fields]
372
     *                                  On error: [false, null, 'error message', array of unknown fields]
373
     *                              If 'responseType' option is RESPONSE_TYPE_FILTER: a FilterResponse instance
374
     *
375
     * @throws Exception
376
     * @throws InvalidArgumentException Thrown if the 'allowUnknowns' option was not a bool
377
     * @throws InvalidArgumentException Thrown if the 'defaultRequired' option was not a bool
378
     * @throws InvalidArgumentException Thrown if the 'responseType' option was not a recognized type.
379
     * @throws InvalidArgumentException Thrown if the filters for a field were not an array.
380
     * @throws InvalidArgumentException Thrown if any one filter for a field was not an array.
381
     * @throws InvalidArgumentException Thrown if the 'required' value for a field was not a bool.
382
     *
383
     * @see FiltererInterface::getSpecification For more information on specifications.
384
     */
385
    public static function filter(array $specification, array $input, array $options = [])
386
    {
387
        $options += self::DEFAULT_OPTIONS;
388
        $responseType = $options[FiltererOptions::RESPONSE_TYPE];
389
390
        $filterer = new Filterer($specification, $options);
391
        $filterResponse = $filterer->execute($input);
392
393
        return self::generateFilterResponse($responseType, $filterResponse);
394
    }
395
396
    /**
397
     * Return the filter aliases.
398
     *
399
     * @return array array where keys are aliases and values pass is_callable().
400
     */
401
    public static function getFilterAliases() : array
402
    {
403
        return self::$registeredFilterAliases;
404
    }
405
406
    /**
407
     * Set the filter aliases.
408
     *
409
     * @param array $aliases array where keys are aliases and values pass is_callable().
410
     * @return void
411
     *
412
     * @throws Exception Thrown if any of the given $aliases is not valid. @see registerAlias()
413
     */
414
    public static function setFilterAliases(array $aliases)
415
    {
416
        $originalAliases = self::$registeredFilterAliases;
417
        self::$registeredFilterAliases = [];
418
        try {
419
            foreach ($aliases as $alias => $callback) {
420
                self::registerAlias($alias, $callback);
421
            }
422
        } catch (Throwable $throwable) {
423
            self::$registeredFilterAliases = $originalAliases;
424
            throw $throwable;
425
        }
426
    }
427
428
    /**
429
     * Register a new alias with the Filterer
430
     *
431
     * @param string|int $alias the alias to register
432
     * @param callable $filter the aliased callable filter
433
     * @param bool $overwrite Flag to overwrite existing alias if it exists
434
     *
435
     * @return void
436
     *
437
     * @throws \InvalidArgumentException if $alias was not a string or int
438
     * @throws Exception if $overwrite is false and $alias exists
439
     */
440
    public static function registerAlias($alias, callable $filter, bool $overwrite = false)
441
    {
442
        self::assertIfStringOrInt($alias);
443
        self::assertIfAliasExists($alias, $overwrite);
444
        self::$registeredFilterAliases[$alias] = $filter;
445
    }
446
447
    /**
448
     * Filter an array by applying filters to each member
449
     *
450
     * @param array $values an array to be filtered. Use the Arrays::filter() before this method to ensure counts when
451
     *                      you pass into Filterer
452
     * @param array $filters filters with each specified the same as in @see self::filter.
453
     *                       Eg [['string', false, 2], ['uint']]
454
     *
455
     * @return array the filtered $values
456
     *
457
     * @throws FilterException if any member of $values fails filtering
458
     */
459
    public static function ofScalars(array $values, array $filters) : array
460
    {
461
        $wrappedFilters = [];
462
        foreach ($values as $key => $item) {
463
            $wrappedFilters[$key] = $filters;
464
        }
465
466
        list($status, $result, $error) = self::filter($wrappedFilters, $values);
467
        if (!$status) {
468
            throw new FilterException($error);
469
        }
470
471
        return $result;
472
    }
473
474
    /**
475
     * Filter an array by applying filters to each member
476
     *
477
     * @param array $values as array to be filtered. Use the Arrays::filter() before this method to ensure counts when
478
     *                      you pass into Filterer
479
     * @param array $spec spec to apply to each $values member, specified the same as in @see self::filter.
480
     *     Eg ['key' => ['required' => true, ['string', false], ['unit']], 'key2' => ...]
481
     *
482
     * @return array the filtered $values
483
     *
484
     * @throws Exception if any member of $values fails filtering
485
     */
486
    public static function ofArrays(array $values, array $spec) : array
487
    {
488
        $results = [];
489
        $errors = [];
490
        foreach ($values as $key => $item) {
491
            if (!is_array($item)) {
492
                $errors[] = "Value at position '{$key}' was not an array";
493
                continue;
494
            }
495
496
            list($status, $result, $error) = self::filter($spec, $item);
497
            if (!$status) {
498
                $errors[] = $error;
499
                continue;
500
            }
501
502
            $results[$key] = $result;
503
        }
504
505
        if (!empty($errors)) {
506
            throw new FilterException(implode("\n", $errors));
507
        }
508
509
        return $results;
510
    }
511
512
    /**
513
     * Filter $value by using a Filterer $spec and Filterer's default options.
514
     *
515
     * @param array $value array to be filtered. Use the Arrays::filter() before this method to ensure counts when you
516
     *                     pass into Filterer
517
     * @param array $spec spec to apply to $value, specified the same as in @see self::filter.
518
     *     Eg ['key' => ['required' => true, ['string', false], ['unit']], 'key2' => ...]
519
     *
520
     * @return array the filtered $value
521
     *
522
     * @throws FilterException if $value fails filtering
523
     */
524
    public static function ofArray(array $value, array $spec) : array
525
    {
526
        list($status, $result, $error) = self::filter($spec, $value);
527
        if (!$status) {
528
            throw new FilterException($error);
529
        }
530
531
        return $result;
532
    }
533
534
    private static function assertIfStringOrInt($alias)
535
    {
536
        if (!is_string($alias) && !is_int($alias)) {
537
            throw new InvalidArgumentException('$alias was not a string or int');
538
        }
539
    }
540
541
    private static function assertIfAliasExists($alias, bool $overwrite)
542
    {
543
        if (array_key_exists($alias, self::$registeredFilterAliases) && !$overwrite) {
544
            throw new Exception("Alias '{$alias}' exists");
545
        }
546
    }
547
548
    private static function checkForUnknowns(array $leftOverInput, array $errors) : array
549
    {
550
        foreach ($leftOverInput as $field => $value) {
551
            $errors[$field] = "Field '{$field}' with value '" . trim(var_export($value, true), "'") . "' is unknown";
552
        }
553
554
        return $errors;
555
    }
556
557
    private static function handleAllowUnknowns(bool $allowUnknowns, array $leftOverInput, array $errors) : array
558
    {
559
        if (!$allowUnknowns) {
560
            $errors = self::checkForUnknowns($leftOverInput, $errors);
561
        }
562
563
        return $errors;
564
    }
565
566
    private static function handleRequiredFields(bool $required, string $field, array $errors) : array
567
    {
568
        if ($required) {
569
            $errors[$field] = "Field '{$field}' was required and not present";
570
        }
571
        return $errors;
572
    }
573
574
    private static function getRequired($filters, $defaultRequired, $field) : bool
575
    {
576
        $required = $filters[FilterOptions::IS_REQUIRED] ?? $defaultRequired;
577
        if ($required !== false && $required !== true) {
578
            throw new InvalidArgumentException(
579
                sprintf("'%s' for field '%s' was not a bool", FilterOptions::IS_REQUIRED, $field)
580
            );
581
        }
582
583
        return $required;
584
    }
585
586
    private static function assertFiltersIsAnArray($filters, string $field)
587
    {
588
        if (!is_array($filters)) {
589
            throw new InvalidArgumentException("filters for field '{$field}' was not a array");
590
        }
591
    }
592
593
    private static function handleCustomError(
594
        string $field,
595
        $value,
596
        Throwable $e,
597
        array $errors,
598
        string $customError = null
599
    ) : array {
600
        $error = $customError;
601
        if ($error === null) {
602
            $errorFormat = "Field '%s' with value '{value}' failed filtering, message '%s'";
603
            $error = sprintf($errorFormat, $field, $e->getMessage());
604
        }
605
606
        $errors[$field] = str_replace('{value}', trim(var_export($value, true), "'"), $error);
607
        return $errors;
608
    }
609
610
    private static function assertFunctionIsCallable($function, string $field)
611
    {
612
        if (!is_callable($function)) {
613
            throw new Exception(
614
                "Function '" . trim(var_export($function, true), "'") . "' for field '{$field}' is not callable"
615
            );
616
        }
617
    }
618
619
    private static function handleFilterAliases($function, $filterAliases)
620
    {
621
        if ((is_string($function) || is_int($function)) && array_key_exists($function, $filterAliases)) {
622
            $function = $filterAliases[$function];
623
        }
624
625
        return $function;
626
    }
627
628
    private static function assertFilterIsArray($filter, string $field)
629
    {
630
        if (!is_array($filter)) {
631
            throw new InvalidArgumentException("filter for field '{$field}' was not a array");
632
        }
633
    }
634
635
    private static function validateThrowOnError(array &$filters, string $field) : bool
636
    {
637
        if (!array_key_exists(FilterOptions::THROW_ON_ERROR, $filters)) {
638
            return false;
639
        }
640
641
        $throwOnError = $filters[FilterOptions::THROW_ON_ERROR];
642
        if ($throwOnError !== true && $throwOnError !== false) {
643
            throw new InvalidArgumentException(
644
                sprintf(self::INVALID_BOOLEAN_FILTER_OPTION, FilterOptions::THROW_ON_ERROR, $field)
645
            );
646
        }
647
648
        unset($filters[FilterOptions::THROW_ON_ERROR]);
649
650
        return $throwOnError;
651
    }
652
653
    private static function validateReturnOnNull(array &$filters, string $field) : bool
654
    {
655
        if (!array_key_exists(FilterOptions::RETURN_ON_NULL, $filters)) {
656
            return false;
657
        }
658
659
        $returnOnNull = $filters[FilterOptions::RETURN_ON_NULL];
660
        if ($returnOnNull !== true && $returnOnNull !== false) {
661
            throw new InvalidArgumentException(
662
                sprintf(self::INVALID_BOOLEAN_FILTER_OPTION, FilterOptions::RETURN_ON_NULL, $field)
663
            );
664
        }
665
666
        unset($filters[FilterOptions::RETURN_ON_NULL]);
667
668
        return $returnOnNull;
669
    }
670
671
    private static function validateCustomError(array &$filters, string $field)
672
    {
673
        $customError = null;
674
        if (array_key_exists(FilterOptions::CUSTOM_ERROR, $filters)) {
675
            $customError = $filters[FilterOptions::CUSTOM_ERROR];
676
            if (!is_string($customError) || trim($customError) === '') {
677
                throw new InvalidArgumentException(
678
                    sprintf("%s for field '%s' was not a non-empty string", FilterOptions::CUSTOM_ERROR, $field)
679
                );
680
            }
681
682
            unset($filters[FilterOptions::CUSTOM_ERROR]);//unset so its not used as a filter
683
        }
684
685
        return $customError;
686
    }
687
688
    private static function getAllowUnknowns(array $options) : bool
689
    {
690
        $allowUnknowns = $options[FiltererOptions::ALLOW_UNKNOWNS];
691
        if ($allowUnknowns !== false && $allowUnknowns !== true) {
692
            throw new InvalidArgumentException(sprintf("'%s' option was not a bool", FiltererOptions::ALLOW_UNKNOWNS));
693
        }
694
695
        return $allowUnknowns;
696
    }
697
698
    private static function getDefaultRequired(array $options) : bool
699
    {
700
        $defaultRequired = $options[FiltererOptions::DEFAULT_REQUIRED];
701
        if ($defaultRequired !== false && $defaultRequired !== true) {
702
            throw new InvalidArgumentException(
703
                sprintf("'%s' option was not a bool", FiltererOptions::DEFAULT_REQUIRED)
704
            );
705
        }
706
707
        return $defaultRequired;
708
    }
709
710
    /**
711
     * @param string         $responseType   The type of object that should be returned.
712
     * @param FilterResponse $filterResponse The filter response to generate the typed response from.
713
     *
714
     * @return array|FilterResponse
715
     *
716
     * @see filter For more information on how responseType is handled and returns are structured.
717
     */
718
    private static function generateFilterResponse(string $responseType, FilterResponse $filterResponse)
719
    {
720
        if ($responseType === self::RESPONSE_TYPE_FILTER) {
721
            return $filterResponse;
722
        }
723
724
        if ($responseType === self::RESPONSE_TYPE_ARRAY) {
725
            return [
726
                $filterResponse->success,
727
                $filterResponse->success ? $filterResponse->filteredValue : null,
728
                $filterResponse->errorMessage,
729
                $filterResponse->unknowns
730
            ];
731
        }
732
733
        throw new InvalidArgumentException(sprintf("'%s' was not a recognized value", FiltererOptions::RESPONSE_TYPE));
734
    }
735
736
    private function addUsedInputToFilter(array $uses, array $filteredInput, string $field, array &$filter)
737
    {
738
        foreach ($uses as $usedField) {
739
            if (array_key_exists($usedField, $filteredInput)) {
740
                array_push($filter, $filteredInput[$usedField]);
741
                continue;
742
            }
743
744
            throw new FilterException(
745
                "{$field} uses {$usedField} but {$usedField} was not given."
746
            );
747
        }
748
    }
749
}
750