Files
René Halberstadt 7d08a9f1a0
PHP Composer / build (push) Failing after 10s
Added readme
2026-07-26 16:17:06 +02:00

5.9 KiB

tez-utils-validate

PHP validation toolkit with guard assertions, composable rules, schema validation, strict type casting, and array-item validation. PHP 8.3+, no framework dependencies.

Installation

composer require tez/utils-validate

Components

Class Purpose
Guard Assertion helpers that throw on failure and return the value on success
Rules Stateless boolean predicates — no exceptions, no side-effects
Composite Combines predicates with AND / OR / NONE logic
Schema Validates an array against a shape definition
TypeCast Strict type coercion that throws instead of silently truncating
Each Applies a validation rule to every item in an array

Guard

Assertions that return the value on success, making them suitable for inline use. Throws RuntimeException by default; the exception can be overridden with a class name, an instance, or a closure.

use Tez\Utils\Validate\Guard;

// Basic assertions
$name  = Guard::notNull($request->get('name'));
$email = Guard::notEmpty($request->get('email'));
$score = Guard::inRange($input, min: 0, max: 100);
$user  = Guard::instanceOf($entity, User::class);

// Boolean assertions
Guard::isTrue($user->isActive());
Guard::isFalse($user->isBanned());

// Numeric string
Guard::isNumeric($request->get('amount'));

// Custom predicate
Guard::that($price > 0, fn() => new DomainException('Price must be positive'));

Custom exceptions:

Guard::notNull($value, \InvalidArgumentException::class);
Guard::notNull($value, new \DomainException('Value is required'));
Guard::notNull($value, fn() => new \DomainException('Value is required'));

Rules

Stateless boolean predicates. Return true/false — never throw.

use Tez\Utils\Validate\Rules;

Rules::isEmail('user@example.com');          // true
Rules::isUrl('https://example.com');         // true
Rules::isUrl('http://...', requireHttps: true); // false
Rules::isUuid('550e8400-e29b-41d4-...');     // true
Rules::isIp('192.168.1.1');                  // true
Rules::isIpv4('192.168.1.1');               // true
Rules::isIpv6('::1');                        // true

Rules::minLength('hello', 3);               // true
Rules::maxLength('hello', 10);              // true
Rules::matches('abc123', '/^[a-z0-9]+$/'); // true

Rules::between(5, 1, 10);                   // true
Rules::isPositive(1);                        // true
Rules::isNegative(-1);                       // true
Rules::notEmpty('hello');                    // true

Rules::isDate('2026-04-23');                // true
Rules::isDate('23.04.2026', 'd.m.Y');       // true

Composite

Combines multiple predicates into one using AND, OR, or NONE logic. Each method returns a Closure that can be passed directly to Guard::that().

use Tez\Utils\Validate\Composite;
use Tez\Utils\Validate\Rules;

$isStrongPassword = Composite::all(
    fn($v) => Rules::minLength($v, 8),
    fn($v) => Rules::matches($v, '/[A-Z]/'),
    fn($v) => Rules::matches($v, '/[0-9]/'),
);

$isStrongPassword('Secret42!'); // true
$isStrongPassword('weak');      // false

// OR — passes when at least one rule matches
$isEmailOrUuid = Composite::any(
    fn($v) => Rules::isEmail($v),
    fn($v) => Rules::isUuid($v),
);

// NONE — passes when no rule matches
$isNotReserved = Composite::none(
    fn($v) => $v === 'admin',
    fn($v) => $v === 'root',
);

Schema

Validates an array against a shape definition. Fails fast on the first violation and reports the dot-notation path.

Supported type strings: string, int, float, bool, array, mixed
Prefix with ? to mark a field as optional (absent or null is accepted).

use Tez\Utils\Validate\Schema;
use Tez\Utils\Validate\Guard;

Schema::validate($data, [
    'name'    => 'string',
    'age'     => 'int',
    'active'  => 'bool',
    'email'   => '?string',   // optional
    'score'   => fn($v) => Guard::inRange($v, 0, 100), // custom rule
    'address' => [            // nested schema
        'city'    => 'string',
        'country' => '?string',
    ],
]);

On failure, an UnexpectedValueException is thrown with the dot-notation path:

address.zip: expected string, got int
user.profile.displayName: required field missing

TypeCast

Strict type coercion — throws UnexpectedValueException when a value cannot be unambiguously converted, unlike PHP built-ins that silently truncate or guess.

use Tez\Utils\Validate\TypeCast;

TypeCast::toInt('42');      // 42
TypeCast::toInt(3.0);       // 3
TypeCast::toInt(3.7);       // throws — fractional float

TypeCast::toFloat('3.14');  // 3.14
TypeCast::toString(42);     // '42'

TypeCast::toBool('true');   // true
TypeCast::toBool('yes');    // true
TypeCast::toBool('off');    // false
TypeCast::toBool('maybe');  // throws — ambiguous string

// Nullable variants — pass through null, convert everything else
TypeCast::toNullableInt(null);    // null
TypeCast::toNullableInt('42');    // 42
TypeCast::toNullableFloat(null);  // null
TypeCast::toNullableString(null); // null
TypeCast::toNullableBool(null);   // null

Each

Applies a validation rule to every item in an array. Fails fast on the first violation and prepends the index path to the exception message.

use Tez\Utils\Validate\Each;
use Tez\Utils\Validate\Guard;

$orders = [
    ['amount' => 50],
    ['amount' => -10],
];

Each::validate($orders, function (mixed $order): void {
    if ($order['amount'] <= 0) {
        throw new \DomainException('Amount must be positive.');
    }
});
// throws DomainException: "items[1]: Amount must be positive."

// Custom path label
Each::validate($lines, fn($line) => Guard::notEmpty($line), path: 'lines');
// throws: "lines[2]: ..."

The original exception type is preserved, and the original exception is available via getPrevious().


Requirements

  • PHP 8.3+
  • No runtime dependencies

License

MIT