Validation chains with error accumulation

Compose small single-purpose validators into a single check that either passes or returns a list of every problem. Compare with imperative validation code that short-circuits on the first failure.

Functions used

The usual imperative shape

function validate(array $data): array {
    $errors = [];
    if (empty($data['name']))                     { $errors[] = 'name is required'; }
    if (empty($data['email']) || !str_contains($data['email'], '@')) { $errors[] = 'email invalid'; }
    if (!is_int($data['age']) || $data['age'] < 0){ $errors[] = 'age invalid'; }
    return $errors;
}

Works. But each rule mixes “predicate” and “error message” together and you can’t reuse them.

Factor rules into data

Each rule is a pair: a predicate that says whether the field is VALID, and the message when it’s not:

use PinkCrab\FunctionConstructors\GeneralFunctions as F;
use PinkCrab\FunctionConstructors\Arrays as A;
use PinkCrab\FunctionConstructors\Comparisons as C;
use PinkCrab\FunctionConstructors\Strings as Str;

$rules = [
    ['name is required',  fn($d) => C\notEmpty($d['name'] ?? '')],
    ['email invalid',     fn($d) => Str\contains('@')($d['email'] ?? '')],
    ['age invalid',       fn($d) => is_int($d['age'] ?? null) && $d['age'] >= 0],
];

Validator as a pure function over rules

$validate = fn(array $data): array => array_values(array_map(
    fn($rule) => $rule[0],                          // the message
    array_filter($rules, fn($rule) => ! $rule[1]($data))   // rules that failed
));

Use it

print_r($validate([
    'name'  => '',
    'email' => 'nope',
    'age'   => 30,
]));

// ['name is required', 'email invalid']

Every failing rule is collected. A well-formed input returns [] — truthy-empty, so $errors ? fail() : proceed() reads naturally.

Why this scales

Reusing predicates across rules

If several rules share a shape (“field X is a non-empty string”), compose that predicate once:

$isPresentString = F\compose(
    fn($d) => $d['name'] ?? null,   // or parameterise
    fn($v) => is_string($v) && $v !== ''
);

Or build a rule factory:

$mustBeString = fn(string $field) => fn(array $d) =>
    isset($d[$field]) && is_string($d[$field]) && $d[$field] !== '';

$rules = [
    ['name is required',  $mustBeString('name')],
    ['title is required', $mustBeString('title')],
    // ...
];