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