Completed
Push — master ( de16cb...408c7c )
by Chad
13s queued 10s
created

Filterer::assertFilterIsArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
623
    {
624
        $allowUnknowns = $options[FiltererOptions::ALLOW_UNKNOWNS];
625
        if ($allowUnknowns !== false && $allowUnknowns !== true) {
626
            throw new InvalidArgumentException(sprintf("'%s' option was not a bool", FiltererOptions::ALLOW_UNKNOWNS));
627
        }
628
629
        return $allowUnknowns;
630
    }
631
632 View Code Duplication
    private static function getDefaultRequired(array $options) : bool
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
633
    {
634
        $defaultRequired = $options[FiltererOptions::DEFAULT_REQUIRED];
635
        if ($defaultRequired !== false && $defaultRequired !== true) {
636
            throw new InvalidArgumentException(
637
                sprintf("'%s' option was not a bool", FiltererOptions::DEFAULT_REQUIRED)
638
            );
639
        }
640
641
        return $defaultRequired;
642
    }
643
644
    /**
645
     * @param string         $responseType   The type of object that should be returned.
646
     * @param FilterResponse $filterResponse The filter response to generate the typed response from.
647
     *
648
     * @return array|FilterResponse
649
     *
650
     * @see filter For more information on how responseType is handled and returns are structured.
651
     */
652
    private static function generateFilterResponse(string $responseType, FilterResponse $filterResponse)
653
    {
654
        if ($responseType === self::RESPONSE_TYPE_FILTER) {
655
            return $filterResponse;
656
        }
657
658
        if ($responseType === self::RESPONSE_TYPE_ARRAY) {
659
            return [
660
                $filterResponse->success,
661
                $filterResponse->success ? $filterResponse->filteredValue : null,
662
                $filterResponse->errorMessage,
663
                $filterResponse->unknowns
664
            ];
665
        }
666
667
        throw new InvalidArgumentException(sprintf("'%s' was not a recognized value", FiltererOptions::RESPONSE_TYPE));
668
    }
669
670
    private function addUsedInputToFilter(array $uses, array $filteredInput, string $field, array &$filter)
671
    {
672
        foreach ($uses as $usedField) {
673
            if (array_key_exists($usedField, $filteredInput)) {
674
                array_push($filter, $filteredInput[$usedField]);
675
                continue;
676
            }
677
678
            throw new FilterException(
679
                "{$field} uses {$usedField} but {$usedField} was not given."
680
            );
681
        }
682
    }
683
}
684