Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active July 9, 2026 06:27
Show Gist options
  • Select an option

  • Save masakielastic/57ee7593fea140082f23a5b9366bcb58 to your computer and use it in GitHub Desktop.

Select an option

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 は同時に指定…
<?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;
}
<?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