Last active
July 9, 2026 06:27
-
-
Save masakielastic/57ee7593fea140082f23a5b9366bcb58 to your computer and use it in GitHub Desktop.
filter_var_array() の第2引数に渡す descriptor 配列を生成する helper 関数です。 アプリケーション側では filter / FILTER_* 定数を直接書かず、validate / sanitize / callback のいずれか1つを指定します。 filter_descriptors() は、その仕様配列を filter_var_array() が受け取れる descriptor 形式へ変換します。 validate と sanitize は標準 filter に変換されます。 callback は FILTER_CALLBACK descriptor に変換されます。 validate / sanitize / callback は同時に指定…
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', | |
| 'age' => '17', | |
| 'title' => '<b>Hello</b>', | |
| 'message' => '', | |
| ]; | |
| $app_spec = [ | |
| 'email' => [ | |
| 'validate' => 'email', | |
| ], | |
| 'age' => [ | |
| 'validate' => 'int', | |
| 'options' => [ | |
| 'min_range' => 18, | |
| 'max_range' => 120, | |
| ], | |
| ], | |
| 'title' => [ | |
| 'sanitize' => 'full_special_chars', | |
| ], | |
| 'message' => [ | |
| 'callback' => 'message', | |
| ], | |
| ]; | |
| $callback_registry = [ | |
| 'message' => 'validate_message', | |
| ]; | |
| $descriptors = filter_descriptors( | |
| spec: $app_spec, | |
| callback_registry: $callback_registry, | |
| ); | |
| $result = filter_var_array($input, $descriptors); | |
| var_dump($descriptors); | |
| var_dump($result); | |
| function validate_message(string $value): string|false | |
| { | |
| if ($value === '') { | |
| return false; | |
| } | |
| if (mb_strlen($value, 'UTF-8') > 1000) { | |
| return false; | |
| } | |
| return $value; | |
| } |
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 | |
| declare(strict_types=1); | |
| /** | |
| * Build descriptors for filter_var_array(). | |
| * | |
| * Input: | |
| * | |
| * [ | |
| * 'email' => [ | |
| * 'validate' => 'email', | |
| * ], | |
| * | |
| * 'age' => [ | |
| * 'validate' => 'int', | |
| * 'options' => [ | |
| * 'min_range' => 18, | |
| * 'max_range' => 120, | |
| * ], | |
| * ], | |
| * | |
| * 'title' => [ | |
| * 'sanitize' => 'full_special_chars', | |
| * ], | |
| * | |
| * 'message' => [ | |
| * 'callback' => 'message', | |
| * ], | |
| * ] | |
| * | |
| * Callback registry: | |
| * | |
| * [ | |
| * 'message' => 'validate_message', | |
| * ] | |
| */ | |
| function filter_descriptors( | |
| array $spec, | |
| array $callback_registry = [], | |
| ): 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, | |
| callback_registry: $callback_registry, | |
| ); | |
| } | |
| return $descriptors; | |
| } | |
| function filter_descriptor_from_spec( | |
| string $field, | |
| array $field_spec, | |
| array $callback_registry, | |
| ): array { | |
| $kind = filter_descriptor_kind($field, $field_spec); | |
| return match ($kind) { | |
| 'validate' => filter_descriptor( | |
| filter: validation_filter_id( | |
| rule_name($field, $field_spec['validate'], 'validate') | |
| ), | |
| options: filter_options($field, $field_spec), | |
| flags: filter_flags($field, $field_spec), | |
| ), | |
| 'sanitize' => filter_descriptor( | |
| filter: sanitize_filter_id( | |
| rule_name($field, $field_spec['sanitize'], 'sanitize') | |
| ), | |
| options: filter_options($field, $field_spec), | |
| flags: filter_flags($field, $field_spec), | |
| ), | |
| 'callback' => callback_filter_descriptor( | |
| field: $field, | |
| callback: callback_from_spec( | |
| field: $field, | |
| value: $field_spec['callback'], | |
| callback_registry: $callback_registry, | |
| ), | |
| 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 filter_descriptor( | |
| int $filter, | |
| array $options = [], | |
| int $flags = 0, | |
| ): array { | |
| $descriptor = [ | |
| 'filter' => $filter, | |
| ]; | |
| if ($options !== []) { | |
| $descriptor['options'] = $options; | |
| } | |
| if ($flags !== 0) { | |
| $descriptor['flags'] = $flags; | |
| } | |
| return $descriptor; | |
| } | |
| function callback_filter_descriptor( | |
| string $field, | |
| callable $callback, | |
| 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, | |
| ]; | |
| } | |
| 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 callback_from_spec( | |
| string $field, | |
| mixed $value, | |
| array $callback_registry, | |
| ): callable { | |
| if (is_string($value) && array_key_exists($value, $callback_registry)) { | |
| $callback = $callback_registry[$value]; | |
| } else { | |
| $callback = $value; | |
| } | |
| if (!is_callable($callback)) { | |
| throw new InvalidArgumentException( | |
| "Callback is not callable: {$field}" | |
| ); | |
| } | |
| return $callback; | |
| } | |
| 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