Search by

sujal / validation

sujal

A PHP validation library built around reusable Rule classes.

Package info

github.com/sujalK/validation

pkg:composer/sujal/validation

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-02 04:40 UTC

This package is auto-updated.

Last update: 2026-09-02 23:23:16 UTC


README

A PHP validation library built around reusable Rule classes.

You describe valid data with an immutable Schema of Field objects. Each check is a Rule class. A ValidationEngine runs the schema.

Built around SOLID boundaries — not a pile of if statements.

  • PHP 8.2+
  • No framework required
  • MIT licensed

Install

composer require sujal/validation

Inside this repository:

composer install

Quick start

<?php

require __DIR__ . '/vendor/autoload.php';

use Sujal\Validation\Field;
use Sujal\Validation\Schema;
use Sujal\Validation\Validator;
use Sujal\Validation\Rules\Required;
use Sujal\Validation\Rules\Email;

$schema = Schema::make(
    Field::named('email')->rules(new Required(), new Email()),
);

$result = Validator::check([
    'email' => 'ada@example.com',
], $schema);

if ($result->passed()) {
    print_r($result->validated());
} else {
    print_r($result->errors()->all());
}

Flow in plain English:

  1. Build a Schema from Field definitions
  2. Hand input + schema to Validator::check(...)
  3. Read passed() / errors() / validated()

Why this shape (SOLID)

Principle How this library uses it
Single responsibility Schema describes, ValidationEngine runs, MessageFormatter writes messages, DataReader reads paths
Open/closed New rules are new classes — the engine does not change
Liskov Any Rule is interchangeable; markers like AllowsNull extend behaviour safely
Interface segregation Small contracts: Rule, DataReader, MessageCatalog, FormatsMessages
Dependency inversion The engine depends on interfaces, not concrete helpers

Fields and schemas are immutable. Calling ->bail() returns a new field — no hidden “current field” state.

A fuller example

<?php

use Sujal\Validation\Field;
use Sujal\Validation\Schema;
use Sujal\Validation\Validator;
use Sujal\Validation\Rules\Required;
use Sujal\Validation\Rules\Email;
use Sujal\Validation\Rules\Min;
use Sujal\Validation\Rules\Confirmed;
use Sujal\Validation\Rules\In;

// This is the data you want to check — form POST, JSON body, etc.
$input = [
    'name' => 'Ada Lovelace',
    'email' => 'ada@example.com',
    'password' => 'secret123',
    'password_confirmation' => 'secret123',
    'role' => 'admin',
];

$schema = Schema::make(
    Field::named('name')->rules(new Required(), new Min(2)),
    Field::named('email')->rules(new Required(), new Email())->label('email address'),
    Field::named('password')->rules(new Required(), new Min(8), new Confirmed())->bail(),
    Field::named('role')->rules(new Required(), new In('admin', 'editor', 'viewer')),
);

$result = Validator::check($input, $schema);

if ($result->failed()) {
    foreach ($result->errors()->all() as $field => $messages) {
        foreach ($messages as $message) {
            echo $field . ': ' . $message . PHP_EOL;
        }
    }
}

Reuse schemas

Schemas are plain values. Merge the pieces you need:

$input = [
    'email' => 'ada@example.com',
    'password' => 'secret123',
    'name' => 'Ada',
];

$credentials = Schema::make(
    Field::named('email')->rules(new Required(), new Email()),
    Field::named('password')->rules(new Required(), new Min(8)),
);

$profile = Schema::make(
    Field::named('name')->rules(new Required(), new Min(2)),
);

$registration = $credentials->merge($profile);

$result = Validator::check($input, $registration);

Nested data and wildcards

Dotted paths read nested arrays:

Field::named('user.profile.email')->rules(new Required(), new Email())

Wildcards validate each item in a list:

$schema = Schema::make(
    Field::named('items.*.email')->rules(new Required(), new Email()),
);

$result = Validator::check([
    'items' => [
        ['email' => 'a@example.com'],
        ['email' => 'bad'],
    ],
], $schema);

// Fails on items.1.email

Nullable and Sometimes

use Sujal\Validation\Rules\Nullable;
use Sujal\Validation\Rules\Sometimes;

// null is allowed; other rules skipped when value is null
Field::named('nickname')->rules(new Nullable(), new Email())

// only validate when the key exists (PATCH-friendly)
Field::named('email')->rules(new Sometimes(), new Email())

Labels and custom messages

$schema = Schema::make(
    Field::named('email')
        ->rules(new Required(), new Email())
        ->label('work email')
        ->message('Required', 'Please enter your work email.'),
)->messages([
    // schema-wide catalog: attribute.RuleKey
    'email.Email' => 'That work email does not look right.',
]);

Placeholders:

  • :attribute → label or humanized field name
  • rule-specific ones like :min, :max, :other

Reading errors

$result->failed();
$result->errors()->all();
$result->errors()->first('email');
$result->errors()->firstMessage();
$result->errors()->flatten();
$result->validated(); // nested array of fields that passed

Throw instead of checking:

use Sujal\Validation\ValidationException;

$input = [
    'email' => 'ada@example.com',
];

$schema = Schema::make(
    Field::named('email')->rules(new Required(), new Email()),
);

try {
    $result = Validator::checkOrFail($input, $schema);
    $clean = $result->validated();
} catch (ValidationException $e) {
    $messages = $e->errorMessages();
}

Optional fields

Without Required, blank values skip most rules:

Field::named('email')->rules(new Email()) // '' passes

Conditional required

use Sujal\Validation\Rules\RequiredIf;

Field::named('company_name')->rules(
    new RequiredIf('account_type', 'business')
)

Nested other-fields work too: new RequiredIf('account.type', 'business').

One-off custom checks

use Sujal\Validation\Rules\Callback;

Field::named('sku')->rules(
    new Required(),
    new Callback(
        fn (string $attribute, mixed $value): bool => is_string($value)
            && str_starts_with($value, 'SKU-'),
        'The :attribute must start with SKU-.'
    )
)

Write your own rule

<?php

namespace App\Rules;

use Sujal\Validation\Contracts\Rule;

final class StartsWithSku implements Rule
{
    public function passes(string $attribute, mixed $value, array $data = []): bool
    {
        return is_string($value) && str_starts_with($value, 'SKU-');
    }

    public function message(): string
    {
        return 'The :attribute must start with SKU-.';
    }

    public function replacements(): array
    {
        return [];
    }
}

Use marker interfaces when the engine must treat the rule specially:

  • ImplicitRule — always run, even if blank (Required)
  • AllowsNull — skip remaining rules when value is null
  • ValidatesWhenPresent — skip the field when the key is missing

Swap dependencies

use Sujal\Validation\ValidationEngine;
use Sujal\Validation\Validator;

$input = [
    'email' => 'ada@example.com',
];

$schema = Schema::make(
    Field::named('email')->rules(new Required(), new Email()),
);

$engine = new ValidationEngine(
    reader: $yourDataReader,
    formatter: $yourFormatter,
);

$validator = new Validator(engine: $engine);
$result = $validator->validate($input, $schema);

Built-in rules

Rule What it checks
Required Present and not blank
RequiredIf Required when another field matches
Nullable Allow null; skip other rules when null
Sometimes Only validate if the key exists
Email Email shape
Integer / Numeric Numbers
Str / Arr / Boolean Types
Min / Max / Between / Length Size
In / Regex / Url Format / allow-list
Same / Confirmed Field matches
Callback Your closure

Import from Sujal\Validation\Rules.

Run the tests

composer test

License

Open source under the MIT License.