Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save masakielastic/fc33c07effde62bc98cd71af95d74726 to your computer and use it in GitHub Desktop.
filter_descriptors
<?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