Last active
July 9, 2026 09:04
-
-
Save masakielastic/fc33c07effde62bc98cd71af95d74726 to your computer and use it in GitHub Desktop.
filter_descriptors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| $input = [ | |
| 'email' => 'taro@example.com', // valid | |
| 'age' => '17', // invalid: less than 18 | |
| 'title' => 'Hello', // sanitized | |
| 'message' => '', // invalid: empty string | |
| ]; | |
| $app_spec = [ | |
| 'email' => [ | |
| 'validate' => 'email', | |
| ], | |
| 'age' => [ | |
| 'validate' => 'int', | |
| 'options' => [ | |
| 'min_range' => 18, | |
| 'max_range' => 120, | |
| ], | |
| ], | |
| 'title' => [ | |
| 'sanitize' => 'full_special_chars', | |
| ], | |
| 'message' => [ | |
| 'callback' => 'validate_message', | |
| ], | |
| ]; | |
| $filter_descriptors = filter_descriptors($app_spec); | |
| $result = filter_var_array($input, $filter_descriptors); | |
| var_dump($filter_descriptors); | |
| var_dump($result); | |
| function validate_message(mixed $value): string|false | |
| { | |
| if (!is_string($value)) { | |
| return false; | |
| } | |
| if ($value === '') { | |
| return false; | |
| } | |
| return $value; | |
| } | |
| function filter_descriptors(array $spec): array | |
| { | |
| $descriptors = []; | |
| foreach ($spec as $field => $field_spec) { | |
| if (!is_string($field)) { | |
| throw new InvalidArgumentException('Field name must be a string.'); | |
| } | |
| if (!is_array($field_spec)) { | |
| throw new InvalidArgumentException( | |
| "Field spec must be an array: {$field}" | |
| ); | |
| } | |
| $descriptors[$field] = filter_descriptor_from_spec( | |
| field: $field, | |
| field_spec: $field_spec, | |
| ); | |
| } | |
| return $descriptors; | |
| } | |
| function filter_descriptor_from_spec( | |
| string $field, | |
| array $field_spec, | |
| ): array { | |
| $kind = filter_descriptor_kind($field, $field_spec); | |
| return $kind === 'callback' | |
| ? callback_filter_descriptor( | |
| field: $field, | |
| field_spec: $field_spec, | |
| ) | |
| : standard_filter_descriptor( | |
| field: $field, | |
| kind: $kind, | |
| field_spec: $field_spec, | |
| ); | |
| } | |
| function filter_descriptor_kind(string $field, array $field_spec): string | |
| { | |
| $keys = array_values(array_intersect( | |
| ['validate', 'sanitize', 'callback'], | |
| array_keys($field_spec), | |
| )); | |
| if (count($keys) !== 1) { | |
| throw new InvalidArgumentException( | |
| "Specify exactly one of validate, sanitize, callback: {$field}" | |
| ); | |
| } | |
| return $keys[0]; | |
| } | |
| function standard_filter_descriptor( | |
| string $field, | |
| string $kind, | |
| array $field_spec, | |
| ): array { | |
| $rule_name = rule_name( | |
| field: $field, | |
| value: $field_spec[$kind], | |
| key: $kind, | |
| ); | |
| $descriptor = [ | |
| 'filter' => filter_id_for_kind($kind, $rule_name), | |
| ]; | |
| $options = filter_options($field, $field_spec); | |
| if ($options !== []) { | |
| $descriptor['options'] = $options; | |
| } | |
| $flags = filter_flags($field, $field_spec); | |
| if ($flags !== 0) { | |
| $descriptor['flags'] = $flags; | |
| } | |
| return $descriptor; | |
| } | |
| function callback_filter_descriptor( | |
| string $field, | |
| array $field_spec, | |
| ): array { | |
| if (array_key_exists('options', $field_spec)) { | |
| throw new InvalidArgumentException( | |
| "callback spec does not accept options: {$field}" | |
| ); | |
| } | |
| if (array_key_exists('flags', $field_spec)) { | |
| throw new InvalidArgumentException( | |
| "callback spec does not accept flags: {$field}" | |
| ); | |
| } | |
| return [ | |
| 'filter' => FILTER_CALLBACK, | |
| 'options' => callback_from_spec($field, $field_spec['callback']), | |
| ]; | |
| } | |
| function callback_from_spec(string $field, mixed $value): callable | |
| { | |
| if (!is_callable($value)) { | |
| throw new InvalidArgumentException( | |
| "Callback is not callable: {$field}" | |
| ); | |
| } | |
| return $value; | |
| } | |
| function rule_name(string $field, mixed $value, string $key): string | |
| { | |
| if (!is_string($value) || $value === '') { | |
| throw new InvalidArgumentException( | |
| "{$key} rule name must be a non-empty string: {$field}" | |
| ); | |
| } | |
| return $value; | |
| } | |
| function filter_options(string $field, array $field_spec): array | |
| { | |
| $options = $field_spec['options'] ?? []; | |
| if (!is_array($options)) { | |
| throw new InvalidArgumentException( | |
| "options must be an array: {$field}" | |
| ); | |
| } | |
| return $options; | |
| } | |
| function filter_flags(string $field, array $field_spec): int | |
| { | |
| $flags = $field_spec['flags'] ?? 0; | |
| if (!is_int($flags)) { | |
| throw new InvalidArgumentException( | |
| "flags must be an int: {$field}" | |
| ); | |
| } | |
| return $flags; | |
| } | |
| function filter_id_for_kind(string $kind, string $name): int | |
| { | |
| return match ($kind) { | |
| 'validate' => validation_filter_id($name), | |
| 'sanitize' => sanitize_filter_id($name), | |
| default => throw new InvalidArgumentException( | |
| "Unknown filter kind: {$kind}" | |
| ), | |
| }; | |
| } | |
| function validation_filter_id(string $name): int | |
| { | |
| return match ($name) { | |
| 'email' => FILTER_VALIDATE_EMAIL, | |
| 'int' => FILTER_VALIDATE_INT, | |
| 'float' => FILTER_VALIDATE_FLOAT, | |
| 'boolean', 'bool' => FILTER_VALIDATE_BOOLEAN, | |
| 'url' => FILTER_VALIDATE_URL, | |
| 'ip' => FILTER_VALIDATE_IP, | |
| 'domain' => FILTER_VALIDATE_DOMAIN, | |
| 'mac' => FILTER_VALIDATE_MAC, | |
| 'regexp', 'regex' => FILTER_VALIDATE_REGEXP, | |
| default => throw new InvalidArgumentException( | |
| "Unknown validation rule: {$name}" | |
| ), | |
| }; | |
| } | |
| function sanitize_filter_id(string $name): int | |
| { | |
| return match ($name) { | |
| 'email' => FILTER_SANITIZE_EMAIL, | |
| 'url' => FILTER_SANITIZE_URL, | |
| 'number_int' => FILTER_SANITIZE_NUMBER_INT, | |
| 'number_float' => FILTER_SANITIZE_NUMBER_FLOAT, | |
| 'special_chars' => FILTER_SANITIZE_SPECIAL_CHARS, | |
| 'full_special_chars' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, | |
| 'add_slashes' => FILTER_SANITIZE_ADD_SLASHES, | |
| 'encoded' => FILTER_SANITIZE_ENCODED, | |
| 'unsafe_raw' => FILTER_UNSAFE_RAW, | |
| default => throw new InvalidArgumentException( | |
| "Unknown sanitize rule: {$name}" | |
| ), | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment