Filterer::execute()   C
last analyzed

Complexity

Conditions 12
Paths 69

Size

Total Lines 79
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 51
c 3
b 0
f 0
dl 0
loc 79
rs 6.9666
cc 12
nc 69
nop 1

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