Skip to content

Commit b0f7585

Browse files
authored
Merge pull request #85 from jncarver/filterer-instance
Filterer instance
2 parents 09303eb + 105be08 commit b0f7585

6 files changed

Lines changed: 643 additions & 103 deletions

File tree

src/Filterer.php

Lines changed: 207 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/**
1111
* Class to filter an array of input.
1212
*/
13-
final class Filterer
13+
final class Filterer implements FiltererInterface
1414
{
1515
/**
1616
* @var array
@@ -41,6 +41,15 @@ final class Filterer
4141
'url' => '\\TraderInteractive\\Filter\\Url::filter',
4242
];
4343

44+
/**
45+
* @var array
46+
*/
47+
const DEFAULT_OPTIONS = [
48+
'allowUnknowns' => false,
49+
'defaultRequired' => false,
50+
'responseType' => self::RESPONSE_TYPE_ARRAY,
51+
];
52+
4453
/**
4554
* @var string
4655
*/
@@ -54,7 +63,165 @@ final class Filterer
5463
/**
5564
* @var array
5665
*/
57-
private static $filterAliases = self::DEFAULT_FILTER_ALIASES;
66+
private static $registeredFilterAliases = self::DEFAULT_FILTER_ALIASES;
67+
68+
/**
69+
* @var array|null
70+
*/
71+
private $filterAliases;
72+
73+
/**
74+
* @var array
75+
*/
76+
private $specification;
77+
78+
/**
79+
* @var bool
80+
*/
81+
private $allowUnknowns;
82+
83+
/**
84+
* @var bool
85+
*/
86+
private $defaultRequired;
87+
88+
/**
89+
* @param array $specification The specification to apply to the value.
90+
* @param array $options The options apply during filtering.
91+
* 'allowUnknowns' (default false) true to allow or false to treat as error.
92+
* 'defaultRequired' (default false) true to make fields required by default.
93+
* @param array|null $filterAliases The filter aliases to accept.
94+
*
95+
* @throws InvalidArgumentException if 'allowUnknowns' option was not a bool
96+
* @throws InvalidArgumentException if 'defaultRequired' option was not a bool
97+
*/
98+
public function __construct(array $specification, array $options = [], array $filterAliases = null)
99+
{
100+
$options += self::DEFAULT_OPTIONS;
101+
102+
$this->specification = $specification;
103+
$this->filterAliases = $filterAliases;
104+
$this->allowUnknowns = self::getAllowUnknowns($options);
105+
$this->defaultRequired = self::getDefaultRequired($options);
106+
}
107+
108+
/**
109+
* @param mixed $input The input to filter.
110+
*
111+
* @return FilterResponse
112+
*
113+
* @throws InvalidArgumentException Thrown if the filters for a field were not an array.
114+
* @throws InvalidArgumentException Thrown if any one filter for a field was not an array.
115+
* @throws InvalidArgumentException Thrown if the 'required' value for a field was not a bool.
116+
*/
117+
public function execute(array $input) : FilterResponse
118+
{
119+
$filterAliases = $this->getAliases();
120+
$inputToFilter = array_intersect_key($input, $this->specification);
121+
$leftOverSpec = array_diff_key($this->specification, $input);
122+
$leftOverInput = array_diff_key($input, $this->specification);
123+
124+
$errors = [];
125+
foreach ($inputToFilter as $field => $input) {
126+
$filters = $this->specification[$field];
127+
self::assertFiltersIsAnArray($filters, $field);
128+
$customError = self::validateCustomError($filters, $field);
129+
unset($filters['required']);//doesn't matter if required since we have this one
130+
unset($filters['default']);//doesn't matter if there is a default since we have a value
131+
foreach ($filters as $filter) {
132+
self::assertFilterIsNotArray($filter, $field);
133+
134+
if (empty($filter)) {
135+
continue;
136+
}
137+
138+
$function = array_shift($filter);
139+
$function = self::handleFilterAliases($function, $filterAliases);
140+
141+
self::assertFunctionIsCallable($function, $field);
142+
143+
array_unshift($filter, $input);
144+
try {
145+
$input = call_user_func_array($function, $filter);
146+
} catch (Exception $exception) {
147+
$errors = self::handleCustomError($field, $input, $exception, $errors, $customError);
148+
continue 2;//next field
149+
}
150+
}
151+
152+
$inputToFilter[$field] = $input;
153+
}
154+
155+
foreach ($leftOverSpec as $field => $filters) {
156+
self::assertFiltersIsAnArray($filters, $field);
157+
$required = self::getRequired($filters, $this->defaultRequired, $field);
158+
if (array_key_exists('default', $filters)) {
159+
$inputToFilter[$field] = $filters['default'];
160+
continue;
161+
}
162+
163+
$errors = self::handleRequiredFields($required, $field, $errors);
164+
}
165+
166+
$errors = self::handleAllowUnknowns($this->allowUnknowns, $leftOverInput, $errors);
167+
168+
return new FilterResponse($inputToFilter, $errors, $leftOverInput);
169+
}
170+
171+
/**
172+
* @return array
173+
*
174+
* @see FiltererInterface::getAliases
175+
*/
176+
public function getAliases() : array
177+
{
178+
return $this->filterAliases ?? self::$registeredFilterAliases;
179+
}
180+
181+
/**
182+
* @return array
183+
*
184+
* @see FiltererInterface::getSpecification
185+
*/
186+
public function getSpecification() : array
187+
{
188+
return $this->specification;
189+
}
190+
191+
/**
192+
* @param array $filterAliases
193+
*
194+
* @return FiltererInterface
195+
*
196+
* @see FiltererInterface::withAliases
197+
*/
198+
public function withAliases(array $filterAliases) : FiltererInterface
199+
{
200+
return new Filterer($this->specification, $this->getOptions(), $filterAliases);
201+
}
202+
203+
/**
204+
* @param array $specification
205+
*
206+
* @return FiltererInterface
207+
*
208+
* @see FiltererInterface::withSpecification
209+
*/
210+
public function withSpecification(array $specification) : FiltererInterface
211+
{
212+
return new Filterer($specification, $this->getOptions(), $this->filterAliases);
213+
}
214+
215+
/**
216+
* @return array
217+
*/
218+
private function getOptions() : array
219+
{
220+
return [
221+
'defaultRequired' => $this->defaultRequired,
222+
'allowUnknowns' => $this->allowUnknowns,
223+
];
224+
}
58225

59226
/**
60227
* Example:
@@ -102,93 +269,38 @@ final class Filterer
102269
* }
103270
* </pre>
104271
*
105-
* @param array $spec the specification to apply to the $input. An array where each key is a known input field and
106-
* each value is an array of filters. Each filter should be an array with the first member being
107-
* anything that can pass is_callable() as well as accepting the value to filter as its first
108-
* argument. Two examples would be the string 'trim' or an object function specified like [$obj,
109-
* 'filter'], see is_callable() documentation. The rest of the members are extra arguments to the
110-
* callable. The result of one filter will be the first argument to the next filter. In addition
111-
* to the filters, the specification values may contain a 'required' key (default false) that
112-
* controls the same behavior as the 'defaultRequired' option below but on a per field basis. A
113-
* 'default' specification value may be used to substitute in a default to the $input when the
114-
* key is not present (whether 'required' is specified or not).
115-
* @param array $input the input the apply the $spec on.
116-
* @param array $options 'allowUnknowns' (default false) true to allow unknowns or false to treat as error,
117-
* 'defaultRequired' (default false) true to make fields required by default and treat as
118-
* error on absence and false to allow their absence by default
119-
* 'responseType' (default RESPONSE_TYPE_ARRAY) Determines the return type, as described
120-
* in the return section.
272+
* @param array $specification The specification to apply to the input.
273+
* @param array $input The input the apply the specification to.
274+
* @param array $options The options apply during filtering.
275+
* 'allowUnknowns' (default false) true to allow or false to treat as error.
276+
* 'defaultRequired' (default false) true to make fields required by default.
277+
* 'responseType' (default RESPONSE_TYPE_ARRAY)
278+
* Determines the return type, as described in the return section.
121279
*
122280
* @return array|FilterResponse If 'responseType' option is RESPONSE_TYPE_ARRAY:
123-
* on success [true, $input filtered, null, array of unknown fields]
124-
* on error [false, null, 'error message', array of unknown fields]
125-
* If 'responseType' option is RESPONSE_TYPE_FILTER: a FilterResponse instance.
281+
* On success: [true, $input filtered, null, array of unknown fields]
282+
* On error: [false, null, 'error message', array of unknown fields]
283+
* If 'responseType' option is RESPONSE_TYPE_FILTER: a FilterResponse instance
126284
*
127285
* @throws Exception
128-
* @throws InvalidArgumentException if 'allowUnknowns' option was not a bool
129-
* @throws InvalidArgumentException if 'defaultRequired' option was not a bool
130-
* @throws InvalidArgumentException if 'responseType' option was not a recognized type
131-
* @throws InvalidArgumentException if filters for a field was not an array
132-
* @throws InvalidArgumentException if a filter for a field was not an array
133-
* @throws InvalidArgumentException if 'required' for a field was not a bool
286+
* @throws InvalidArgumentException Thrown if the 'allowUnknowns' option was not a bool
287+
* @throws InvalidArgumentException Thrown if the 'defaultRequired' option was not a bool
288+
* @throws InvalidArgumentException Thrown if the 'responseType' option was not a recognized type.
289+
* @throws InvalidArgumentException Thrown if the filters for a field were not an array.
290+
* @throws InvalidArgumentException Thrown if any one filter for a field was not an array.
291+
* @throws InvalidArgumentException Thrown if the 'required' value for a field was not a bool.
292+
*
293+
* @see FiltererInterface::getSpecification For more information on specifications.
134294
*/
135-
public static function filter(array $spec, array $input, array $options = [])
295+
public static function filter(array $specification, array $input, array $options = [])
136296
{
137-
$options += ['allowUnknowns' => false, 'defaultRequired' => false, 'responseType' => self::RESPONSE_TYPE_ARRAY];
138-
139-
$allowUnknowns = self::getAllowUnknowns($options);
140-
$defaultRequired = self::getDefaultRequired($options);
297+
$options += self::DEFAULT_OPTIONS;
141298
$responseType = $options['responseType'];
142299

143-
$inputToFilter = array_intersect_key($input, $spec);
144-
$leftOverSpec = array_diff_key($spec, $input);
145-
$leftOverInput = array_diff_key($input, $spec);
146-
147-
$errors = [];
148-
foreach ($inputToFilter as $field => $value) {
149-
$filters = $spec[$field];
150-
self::assertFiltersIsAnArray($filters, $field);
151-
$customError = self::validateCustomError($filters, $field);
152-
unset($filters['required']);//doesn't matter if required since we have this one
153-
unset($filters['default']);//doesn't matter if there is a default since we have a value
154-
foreach ($filters as $filter) {
155-
self::assertFilterIsNotArray($filter, $field);
156-
157-
if (empty($filter)) {
158-
continue;
159-
}
160-
161-
$function = array_shift($filter);
162-
$function = self::handleFilterAliases($function);
163-
164-
self::assertFunctionIsCallable($function, $field);
300+
$filterer = new Filterer($specification, $options);
301+
$filterResponse = $filterer->execute($input);
165302

166-
array_unshift($filter, $value);
167-
try {
168-
$value = call_user_func_array($function, $filter);
169-
} catch (Exception $e) {
170-
$errors = self::handleCustomError($field, $value, $e, $errors, $customError);
171-
continue 2;//next field
172-
}
173-
}
174-
175-
$inputToFilter[$field] = $value;
176-
}
177-
178-
foreach ($leftOverSpec as $field => $filters) {
179-
self::assertFiltersIsAnArray($filters, $field);
180-
$required = self::getRequired($filters, $defaultRequired, $field);
181-
if (array_key_exists('default', $filters)) {
182-
$inputToFilter[$field] = $filters['default'];
183-
continue;
184-
}
185-
186-
$errors = self::handleRequiredFields($required, $field, $errors);
187-
}
188-
189-
$errors = self::handleAllowUnknowns($allowUnknowns, $leftOverInput, $errors);
190-
191-
return self::generateFilterResponse($responseType, $inputToFilter, $errors, $leftOverInput);
303+
return self::generateFilterResponse($responseType, $filterResponse);
192304
}
193305

194306
/**
@@ -198,7 +310,7 @@ public static function filter(array $spec, array $input, array $options = [])
198310
*/
199311
public static function getFilterAliases() : array
200312
{
201-
return self::$filterAliases;
313+
return self::$registeredFilterAliases;
202314
}
203315

204316
/**
@@ -211,15 +323,15 @@ public static function getFilterAliases() : array
211323
*/
212324
public static function setFilterAliases(array $aliases)
213325
{
214-
$originalAliases = self::$filterAliases;
215-
self::$filterAliases = [];
326+
$originalAliases = self::$registeredFilterAliases;
327+
self::$registeredFilterAliases = [];
216328
try {
217329
foreach ($aliases as $alias => $callback) {
218330
self::registerAlias($alias, $callback);
219331
}
220-
} catch (Exception $e) {
221-
self::$filterAliases = $originalAliases;
222-
throw $e;
332+
} catch (Throwable $throwable) {
333+
self::$registeredFilterAliases = $originalAliases;
334+
throw $throwable;
223335
}
224336
}
225337

@@ -239,7 +351,7 @@ public static function registerAlias($alias, callable $filter, bool $overwrite =
239351
{
240352
self::assertIfStringOrInt($alias);
241353
self::assertIfAliasExists($alias, $overwrite);
242-
self::$filterAliases[$alias] = $filter;
354+
self::$registeredFilterAliases[$alias] = $filter;
243355
}
244356

245357
/**
@@ -338,7 +450,7 @@ private static function assertIfStringOrInt($alias)
338450

339451
private static function assertIfAliasExists($alias, bool $overwrite)
340452
{
341-
if (array_key_exists($alias, self::$filterAliases) && !$overwrite) {
453+
if (array_key_exists($alias, self::$registeredFilterAliases) && !$overwrite) {
342454
throw new Exception("Alias '{$alias}' exists");
343455
}
344456
}
@@ -415,10 +527,10 @@ private static function assertFunctionIsCallable($function, string $field)
415527
}
416528
}
417529

418-
private static function handleFilterAliases($function)
530+
private static function handleFilterAliases($function, $filterAliases)
419531
{
420-
if ((is_string($function) || is_int($function)) && array_key_exists($function, self::$filterAliases)) {
421-
$function = self::$filterAliases[$function];
532+
if ((is_string($function) || is_int($function)) && array_key_exists($function, $filterAliases)) {
533+
$function = $filterAliases[$function];
422534
}
423535

424536
return $function;
@@ -467,23 +579,15 @@ private static function getDefaultRequired(array $options) : bool
467579
}
468580

469581
/**
470-
* @param string $responseType The type of object that should be returned.
471-
* @param array $filteredValue The filtered input to return.
472-
* @param array $errors The errors to return.
473-
* @param array $unknowns The unknowns to return.
582+
* @param string $responseType The type of object that should be returned.
583+
* @param FilterResponse $filterResponse The filter response to generate the typed response from.
474584
*
475585
* @return array|FilterResponse
476586
*
477587
* @see filter For more information on how responseType is handled and returns are structured.
478588
*/
479-
private static function generateFilterResponse(
480-
string $responseType,
481-
array $filteredValue,
482-
array $errors,
483-
array $unknowns
484-
) {
485-
$filterResponse = new FilterResponse($filteredValue, $errors, $unknowns);
486-
589+
private static function generateFilterResponse(string $responseType, FilterResponse $filterResponse)
590+
{
487591
if ($responseType === self::RESPONSE_TYPE_FILTER) {
488592
return $filterResponse;
489593
}

0 commit comments

Comments
 (0)