Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 |
||
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 string |
||
66 | */ |
||
67 | const INVALID_THROW_ON_ERROR_VALUE_ERROR_FORMAT = ( |
||
68 | FilterOptions::THROW_ON_ERROR . " for field '%s' was not a boolean value" |
||
69 | ); |
||
70 | |||
71 | /** |
||
72 | * @var array |
||
73 | */ |
||
74 | private static $registeredFilterAliases = self::DEFAULT_FILTER_ALIASES; |
||
75 | |||
76 | /** |
||
77 | * @var array|null |
||
78 | */ |
||
79 | private $filterAliases; |
||
80 | |||
81 | /** |
||
82 | * @var array |
||
83 | */ |
||
84 | private $specification; |
||
85 | |||
86 | /** |
||
87 | * @var bool |
||
88 | */ |
||
89 | private $allowUnknowns; |
||
90 | |||
91 | /** |
||
92 | * @var bool |
||
93 | */ |
||
94 | private $defaultRequired; |
||
95 | |||
96 | /** |
||
97 | * @param array $specification The specification to apply to the value. |
||
98 | * @param array $options The options apply during filtering. |
||
99 | * 'allowUnknowns' (default false) true to allow or false to treat as error. |
||
100 | * 'defaultRequired' (default false) true to make fields required by default. |
||
101 | * @param array|null $filterAliases The filter aliases to accept. |
||
102 | * |
||
103 | * @throws InvalidArgumentException if 'allowUnknowns' option was not a bool |
||
104 | * @throws InvalidArgumentException if 'defaultRequired' option was not a bool |
||
105 | */ |
||
106 | public function __construct(array $specification, array $options = [], array $filterAliases = null) |
||
115 | |||
116 | /** |
||
117 | * @param mixed $input The input to filter. |
||
118 | * |
||
119 | * @return FilterResponse |
||
120 | * |
||
121 | * @throws InvalidArgumentException Thrown if the filters for a field were not an array. |
||
122 | * @throws InvalidArgumentException Thrown if any one filter for a field was not an array. |
||
123 | * @throws InvalidArgumentException Thrown if the 'required' value for a field was not a bool. |
||
124 | */ |
||
125 | public function execute(array $input) : FilterResponse |
||
201 | |||
202 | /** |
||
203 | * @return array |
||
204 | * |
||
205 | * @see FiltererInterface::getAliases |
||
206 | */ |
||
207 | public function getAliases() : array |
||
211 | |||
212 | private static function extractConflicts(array &$filters, string $field, array $conflicts) : array |
||
228 | |||
229 | private static function handleConflicts(array $inputToFilter, array $conflicts, array $errors) |
||
245 | |||
246 | private static function extractUses(&$filters) |
||
252 | |||
253 | /** |
||
254 | * @return array |
||
255 | * |
||
256 | * @see FiltererInterface::getSpecification |
||
257 | */ |
||
258 | public function getSpecification() : array |
||
262 | |||
263 | /** |
||
264 | * @param array $filterAliases |
||
265 | * |
||
266 | * @return FiltererInterface |
||
267 | * |
||
268 | * @see FiltererInterface::withAliases |
||
269 | */ |
||
270 | public function withAliases(array $filterAliases) : FiltererInterface |
||
274 | |||
275 | /** |
||
276 | * @param array $specification |
||
277 | * |
||
278 | * @return FiltererInterface |
||
279 | * |
||
280 | * @see FiltererInterface::withSpecification |
||
281 | */ |
||
282 | public function withSpecification(array $specification) : FiltererInterface |
||
286 | |||
287 | /** |
||
288 | * @return array |
||
289 | */ |
||
290 | private function getOptions() : array |
||
297 | |||
298 | /** |
||
299 | * Example: |
||
300 | * <pre> |
||
301 | * <?php |
||
302 | * class AppendFilter |
||
303 | * { |
||
304 | * public function filter($value, $extraArg) |
||
305 | * { |
||
306 | * return $value . $extraArg; |
||
307 | * } |
||
308 | * } |
||
309 | * $appendFilter = new AppendFilter(); |
||
310 | * |
||
311 | * $trimFunc = function($val) { return trim($val); }; |
||
312 | * |
||
313 | * list($status, $result, $error, $unknowns) = TraderInteractive\Filterer::filter( |
||
314 | * [ |
||
315 | * 'field one' => [[$trimFunc], ['substr', 0, 3], [[$appendFilter, 'filter'], 'boo']], |
||
316 | * 'field two' => ['required' => true, ['floatval']], |
||
317 | * 'field three' => ['required' => false, ['float']], |
||
318 | * 'field four' => ['required' => true, 'default' => 1, ['uint']], |
||
319 | * ], |
||
320 | * ['field one' => ' abcd', 'field two' => '3.14'] |
||
321 | * ); |
||
322 | * |
||
323 | * var_dump($status); |
||
324 | * var_dump($result); |
||
325 | * var_dump($error); |
||
326 | * var_dump($unknowns); |
||
327 | * </pre> |
||
328 | * prints: |
||
329 | * <pre> |
||
330 | * bool(true) |
||
331 | * array(3) { |
||
332 | * 'field one' => |
||
333 | * string(6) "abcboo" |
||
334 | * 'field two' => |
||
335 | * double(3.14) |
||
336 | * 'field four' => |
||
337 | * int(1) |
||
338 | * } |
||
339 | * NULL |
||
340 | * array(0) { |
||
341 | * } |
||
342 | * </pre> |
||
343 | * |
||
344 | * @param array $specification The specification to apply to the input. |
||
345 | * @param array $input The input the apply the specification to. |
||
346 | * @param array $options The options apply during filtering. |
||
347 | * 'allowUnknowns' (default false) true to allow or false to treat as error. |
||
348 | * 'defaultRequired' (default false) true to make fields required by default. |
||
349 | * 'responseType' (default RESPONSE_TYPE_ARRAY) |
||
350 | * Determines the return type, as described in the return section. |
||
351 | * |
||
352 | * @return array|FilterResponse If 'responseType' option is RESPONSE_TYPE_ARRAY: |
||
353 | * On success: [true, $input filtered, null, array of unknown fields] |
||
354 | * On error: [false, null, 'error message', array of unknown fields] |
||
355 | * If 'responseType' option is RESPONSE_TYPE_FILTER: a FilterResponse instance |
||
356 | * |
||
357 | * @throws Exception |
||
358 | * @throws InvalidArgumentException Thrown if the 'allowUnknowns' option was not a bool |
||
359 | * @throws InvalidArgumentException Thrown if the 'defaultRequired' option was not a bool |
||
360 | * @throws InvalidArgumentException Thrown if the 'responseType' option was not a recognized type. |
||
361 | * @throws InvalidArgumentException Thrown if the filters for a field were not an array. |
||
362 | * @throws InvalidArgumentException Thrown if any one filter for a field was not an array. |
||
363 | * @throws InvalidArgumentException Thrown if the 'required' value for a field was not a bool. |
||
364 | * |
||
365 | * @see FiltererInterface::getSpecification For more information on specifications. |
||
366 | */ |
||
367 | public static function filter(array $specification, array $input, array $options = []) |
||
377 | |||
378 | /** |
||
379 | * Return the filter aliases. |
||
380 | * |
||
381 | * @return array array where keys are aliases and values pass is_callable(). |
||
382 | */ |
||
383 | public static function getFilterAliases() : array |
||
387 | |||
388 | /** |
||
389 | * Set the filter aliases. |
||
390 | * |
||
391 | * @param array $aliases array where keys are aliases and values pass is_callable(). |
||
392 | * @return void |
||
393 | * |
||
394 | * @throws Exception Thrown if any of the given $aliases is not valid. @see registerAlias() |
||
395 | */ |
||
396 | public static function setFilterAliases(array $aliases) |
||
409 | |||
410 | /** |
||
411 | * Register a new alias with the Filterer |
||
412 | * |
||
413 | * @param string|int $alias the alias to register |
||
414 | * @param callable $filter the aliased callable filter |
||
415 | * @param bool $overwrite Flag to overwrite existing alias if it exists |
||
416 | * |
||
417 | * @return void |
||
418 | * |
||
419 | * @throws \InvalidArgumentException if $alias was not a string or int |
||
420 | * @throws Exception if $overwrite is false and $alias exists |
||
421 | */ |
||
422 | public static function registerAlias($alias, callable $filter, bool $overwrite = false) |
||
428 | |||
429 | /** |
||
430 | * Filter an array by applying filters to each member |
||
431 | * |
||
432 | * @param array $values an array to be filtered. Use the Arrays::filter() before this method to ensure counts when |
||
433 | * you pass into Filterer |
||
434 | * @param array $filters filters with each specified the same as in @see self::filter. |
||
435 | * Eg [['string', false, 2], ['uint']] |
||
436 | * |
||
437 | * @return array the filtered $values |
||
438 | * |
||
439 | * @throws FilterException if any member of $values fails filtering |
||
440 | */ |
||
441 | public static function ofScalars(array $values, array $filters) : array |
||
455 | |||
456 | /** |
||
457 | * Filter an array by applying filters to each member |
||
458 | * |
||
459 | * @param array $values as array to be filtered. Use the Arrays::filter() before this method to ensure counts when |
||
460 | * you pass into Filterer |
||
461 | * @param array $spec spec to apply to each $values member, specified the same as in @see self::filter. |
||
462 | * Eg ['key' => ['required' => true, ['string', false], ['unit']], 'key2' => ...] |
||
463 | * |
||
464 | * @return array the filtered $values |
||
465 | * |
||
466 | * @throws Exception if any member of $values fails filtering |
||
467 | */ |
||
468 | public static function ofArrays(array $values, array $spec) : array |
||
493 | |||
494 | /** |
||
495 | * Filter $value by using a Filterer $spec and Filterer's default options. |
||
496 | * |
||
497 | * @param array $value array to be filtered. Use the Arrays::filter() before this method to ensure counts when you |
||
498 | * pass into Filterer |
||
499 | * @param array $spec spec to apply to $value, specified the same as in @see self::filter. |
||
500 | * Eg ['key' => ['required' => true, ['string', false], ['unit']], 'key2' => ...] |
||
501 | * |
||
502 | * @return array the filtered $value |
||
503 | * |
||
504 | * @throws FilterException if $value fails filtering |
||
505 | */ |
||
506 | public static function ofArray(array $value, array $spec) : array |
||
515 | |||
516 | private static function assertIfStringOrInt($alias) |
||
522 | |||
523 | private static function assertIfAliasExists($alias, bool $overwrite) |
||
529 | |||
530 | private static function checkForUnknowns(array $leftOverInput, array $errors) : array |
||
538 | |||
539 | private static function handleAllowUnknowns(bool $allowUnknowns, array $leftOverInput, array $errors) : array |
||
547 | |||
548 | private static function handleRequiredFields(bool $required, string $field, array $errors) : array |
||
555 | |||
556 | private static function getRequired($filters, $defaultRequired, $field) : bool |
||
567 | |||
568 | private static function assertFiltersIsAnArray($filters, string $field) |
||
574 | |||
575 | private static function handleCustomError( |
||
591 | |||
592 | private static function assertFunctionIsCallable($function, string $field) |
||
600 | |||
601 | private static function handleFilterAliases($function, $filterAliases) |
||
609 | |||
610 | private static function assertFilterIsArray($filter, string $field) |
||
616 | |||
617 | private static function validateThrowOnError(array &$filters, string $field) : bool |
||
634 | |||
635 | private static function validateCustomError(array &$filters, string $field) |
||
651 | |||
652 | View Code Duplication | private static function getAllowUnknowns(array $options) : bool |
|
661 | |||
662 | View Code Duplication | private static function getDefaultRequired(array $options) : bool |
|
673 | |||
674 | /** |
||
675 | * @param string $responseType The type of object that should be returned. |
||
676 | * @param FilterResponse $filterResponse The filter response to generate the typed response from. |
||
677 | * |
||
678 | * @return array|FilterResponse |
||
679 | * |
||
680 | * @see filter For more information on how responseType is handled and returns are structured. |
||
681 | */ |
||
682 | private static function generateFilterResponse(string $responseType, FilterResponse $filterResponse) |
||
699 | |||
700 | private function addUsedInputToFilter(array $uses, array $filteredInput, string $field, array &$filter) |
||
713 | } |
||
714 |
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.