Skip to content

thesignalforge/validation-php

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Signalforge Validation for PHP

A pure PHP validation library for input validation. No framework dependencies, no string parsing, no magic.

Installation

composer require signalforge/validation

Requirements: PHP 8.2+

Quick Start

use Signalforge\Validation\Validator;

$validator = new Validator([
    'email' => ['required', 'email'],
    'name' => ['required', 'string', ['min', 2], ['max', 100]],
    'age' => ['nullable', 'integer', ['between', 18, 120]],
]);

$result = $validator->validate([
    'email' => 'user@example.com',
    'name' => 'John Doe',
    'age' => 25,
]);

if ($result->valid()) {
    $data = $result->validated(); // Only validated fields
} else {
    $errors = $result->errors(); // Validation errors with message keys
}

Core Philosophy

  • No string parsing — Rules are always arrays, never pipe-separated strings
  • No database validation — No unique, exists rules. Use database constraints.
  • Keys, not messages — Validator returns message keys for i18n, not hardcoded strings
  • Fail fast — Invalid rule definitions throw during setup, not during validation
  • Stateless — Validators are pure functions with no side effects

Rule Syntax

Simple Rules

$rules = [
    'email' => ['required', 'email'],
    'name' => ['required', 'string'],
];

Parameterized Rules

$rules = [
    'name' => ['required', 'string', ['min', 2], ['max', 100]],
    'age' => ['nullable', 'integer', ['between', 18, 120]],
    'role' => ['required', ['in', ['admin', 'user', 'guest']]],
];

Array Validation with Wildcards

$rules = [
    'tags' => ['required', 'array', ['min', 1], ['max', 10]],
    'tags.*' => ['required', 'string', ['max', 50]],

    'items' => ['required', 'array'],
    'items.*.name' => ['required', 'string'],
    'items.*.quantity' => ['required', 'integer', ['min', 1]],
    'items.*.options.*' => ['string'],
];

Conditional Rules

Apply rules based on other field values:

$rules = [
    'type' => ['required', ['in', ['personal', 'business']]],

    // Required only when type is 'business'
    'company_name' => [
        ['when', ['type', '=', 'business'], [
            'required',
            'string',
            ['max', 200],
        ]],
    ],

    // With else branch
    'document_type' => [
        'required',
        ['when', ['category', '=', 'legal'],
            [['in', ['contract', 'nda', 'agreement']]],  // then
            [['in', ['report', 'memo', 'note']]],        // else
        ],
    ],
];

Self-Referential Conditions

Apply rules based on the current field's value:

$rules = [
    'bio' => [
        'nullable',
        'string',
        // Only validate min length if field has a value
        ['when', ['@filled'], [
            ['min', 10],
        ]],
        // Apply different max based on length
        ['when', ['@length', '>=', 256], [
            ['max', 65535],
        ]],
    ],
];

Compound Conditions

Combine multiple conditions with and/or:

$rules = [
    'vat_number' => [
        ['when', ['and', ['type', '=', 'business'], ['country', 'in', ['HR', 'SI', 'AT']]], [
            'required',
            'vat_eu',
        ]],
    ],

    'admin_code' => [
        ['when', ['or', ['role', '=', 'admin'], ['role', '=', 'superadmin']], [
            'required',
            'string',
        ]],
    ],
];

Validation Result

$result = $validator->validate($data);

// Check if validation passed
$result->valid();    // bool
$result->failed();   // bool

// Get validated data (only if valid)
$result->validated(); // array

// Get all errors
$result->errors();
// [
//     'email' => [
//         ['key' => 'validation.required', 'params' => ['field' => 'email']],
//     ],
//     'name' => [
//         ['key' => 'validation.min.string', 'params' => ['field' => 'name', 'min' => 2, 'actual' => 1]],
//     ],
// ]

// Check specific field
$result->hasError('email');     // bool
$result->errorsFor('email');    // array

// Convert to array
$result->toArray();
// ['valid' => false, 'errors' => [...], 'validated' => [...]]

Message Formatting

The validator returns message keys, not formatted messages. Use MessageFormatter for i18n:

use Signalforge\Validation\MessageFormatter;

// Load from directory
$formatter = MessageFormatter::fromDirectory(__DIR__ . '/messages');

// Or define inline
$formatter = new MessageFormatter([
    'en' => [
        'validation.required' => 'The {field} field is required.',
        'validation.min.string' => 'The {field} must be at least {min} characters.',
        'validation.email' => 'The {field} must be a valid email address.',
    ],
    'hr' => [
        'validation.required' => 'Polje {field} je obavezno.',
        'validation.min.string' => 'Polje {field} mora imati najmanje {min} znakova.',
        'validation.email' => 'Polje {field} mora biti valjana email adresa.',
    ],
]);

// Format errors
$messages = $formatter->format($result, 'en');
// ['email' => ['The email field is required.']]

$messages = $formatter->format($result, 'hr');
// ['email' => ['Polje email je obavezno.']]

Built-in Rules

Presence Rules

Rule Description
required Field must be present and not empty
nullable Field can be null (stops further validation if null)
filled If present, must not be empty
present Field must exist in input (can be empty)

Type Rules

Rule Description
string Must be a string
integer Must be an integer
numeric Must be numeric (int, float, or numeric string)
boolean Must be boolean or boolean-like (1, 0, 'true', 'false', 'yes', 'no')
array Must be an array

Size Rules

Works differently based on type (string length, numeric value, array count):

Rule Description
['min', n] Minimum length/value/count
['max', n] Maximum length/value/count
['between', min, max] Between min and max

String Rules

Rule Description
['regex', pattern] Must match regex pattern
['not_regex', pattern] Must not match regex pattern
alpha Only alphabetic characters (Unicode-aware)
alpha_num Only alphanumeric characters
alpha_dash Alphanumeric, dashes, underscores
lowercase Must be lowercase
uppercase Must be uppercase
['starts_with', prefix] Must start with prefix
['ends_with', suffix] Must end with suffix
['contains', substring] Must contain substring

Numeric Comparison Rules

Rule Description
['gt', field] Greater than other field's value
['gte', field] Greater than or equal to other field
['lt', field] Less than other field's value
['lte', field] Less than or equal to other field

Array Rules

Rule Description
distinct All items must be unique

Format Rules

Rule Description
email Valid email address
url Valid URL
['url', ['http', 'https']] URL with specific schemes
ip Valid IP address (v4 or v6)
['ip', 'v4'] Valid IPv4 address
['ip', 'v6'] Valid IPv6 address
uuid Valid UUID
['uuid', 4] Valid UUID v4
json Valid JSON string

Date Rules

Rule Description
date Valid date string
['date_format', format] Date matching PHP format
['after', date] Date after given date
['before', date] Date before given date
['after_or_equal', date] Date after or equal to given date
['before_or_equal', date] Date before or equal to given date

Comparison Rules

Rule Description
['in', [...values]] Value must be in list
['not_in', [...values]] Value must not be in list
['same', field] Must match other field's value
['different', field] Must differ from other field
confirmed Field {name}_confirmation must match

Croatian-Specific Rules

Rule Description
oib Valid Croatian OIB (11-digit, MOD 11-10 checksum)
['phone', 'HR'] Valid Croatian phone number

European Rules

Rule Description
iban Valid IBAN (any country)
['iban', 'HR'] Valid IBAN for specific country
vat_eu Valid EU VAT number
['vat', 'HR'] Valid VAT for specific country
['phone', 'SI'] Valid phone for Slovenia
['phone', 'AT'] Valid phone for Austria
['phone', 'DE'] Valid phone for Germany

Address Rules

Rule Description
postal_code Valid postal code (generic)
['postal_code', 'GB'] Valid UK postal code (e.g., "SW1A 1AA")
['postal_code', 'DE'] Valid German postal code (5 digits)
['postal_code', 'FR'] Valid French postal code (5 digits)
['postal_code', 'ES'] Valid Spanish postal code (5 digits)
['postal_code', 'DK'] Valid Danish postal code (4 digits)
['postal_code', 'NL'] Valid Dutch postal code (e.g., "1012 AB")
['postal_code', 'AT'] Valid Austrian postal code (4 digits)
['postal_code', 'BE'] Valid Belgian postal code (4 digits)
['postal_code', 'CH'] Valid Swiss postal code (4 digits)
['postal_code', 'IT'] Valid Italian postal code (5 digits)
['postal_code', 'PL'] Valid Polish postal code (e.g., "00-001")
['postal_code', 'SE'] Valid Swedish postal code (e.g., "111 22")
['postal_code', 'NO'] Valid Norwegian postal code (4 digits)
['postal_code', 'FI'] Valid Finnish postal code (5 digits)
['postal_code', 'IE'] Valid Irish Eircode (e.g., "D02 X285")
['postal_code', 'PT'] Valid Portuguese postal code (e.g., "1000-001")
['postal_code', 'US'] Valid US ZIP code (e.g., "10001" or "10001-1234")
['postal_code', 'CA'] Valid Canadian postal code (e.g., "K1A 0B1")
country_code Valid ISO 3166-1 alpha-2 country code
['country_code', 'alpha2'] Valid ISO 3166-1 alpha-2 code (e.g., "DE")
['country_code', 'alpha3'] Valid ISO 3166-1 alpha-3 code (e.g., "DEU")

Supported postal code countries: GB, UK, DE, FR, ES, DK, NL, AT, BE, CH, IT, PL, PT, SE, NO, FI, IE, CZ, HU, HR, SI, SK, RO, BG, GR, US, CA

Condition Operators

Self-Referential Conditions

Reference the current field being validated:

['@length', '>=', 256]      // String length comparison
['@length', '<', 10]
['@value', '=', 'special']  // Exact value comparison
['@value', '!=', 'default']
['@matches', '/^\d+$/']     // Regex match
['@type', 'string']         // PHP type check
['@type', 'array']
['@empty']                  // null, '', or []
['@filled']                 // Not empty

Cross-Field Conditions

Reference other fields in the input:

['field', '=', 'value']
['field', '!=', 'value']
['field', '>', 100]
['field', '>=', 100]
['field', '<', 100]
['field', '<=', 100]
['field', 'in', ['a', 'b', 'c']]
['field', 'not_in', ['x', 'y']]
['field', 'filled']         // Other field is not empty
['field', 'empty']          // Other field is empty

Compound Conditions

['and', [...condition1], [...condition2]]
['or', [...condition1], [...condition2]]

Static Factory

$result = Validator::make($data, $rules)->validate($data);

Error Message Keys

The validator returns standardized message keys:

validation.{rule}                  # Simple rules: validation.required
validation.{rule}.{type}           # Type-specific: validation.min.string
validation.{rule}.{variant}        # Variants: validation.ip.v4

Common message keys:

  • validation.required
  • validation.email
  • validation.min.string, validation.min.numeric, validation.min.array
  • validation.max.string, validation.max.numeric, validation.max.array
  • validation.between.string, validation.between.numeric, validation.between.array
  • validation.in
  • validation.same
  • validation.confirmed
  • validation.oib
  • validation.iban
  • validation.vat_eu

Rule Name Constraints

Rule names must:

  • Start with a lowercase letter
  • Contain only lowercase letters, numbers, and underscores
  • Be at most 1024 characters
// Valid
'required', 'email', 'min', 'phone_hr', 'vat_eu'

// Invalid (throws InvalidRuleException)
'Required', '123rule', 'rule-name', 'rule.name'

Extending with Custom Rules

Implement RuleInterface:

use Signalforge\Validation\Rules\AbstractRule;
use Signalforge\Validation\ValidationError;

final class Slug extends AbstractRule
{
    public function name(): string
    {
        return 'slug';
    }

    public function validate(
        mixed $value,
        array $params,
        array $data,
        string $field,
    ): ?ValidationError {
        if (!is_string($value) || !preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $value)) {
            return $this->error($field);
        }
        return null;
    }
}

Register the rule:

$validator = new Validator($rules);
$validator->registerRule('slug', Slug::class);

Examples

User Registration

$validator = new Validator([
    'username' => ['required', 'string', ['min', 3], ['max', 20], 'alpha_dash', 'lowercase'],
    'email' => ['required', 'email'],
    'password' => ['required', 'string', ['min', 8], 'confirmed'],
    'age' => ['nullable', 'integer', ['between', 13, 120]],
    'terms' => ['required', 'boolean'],
]);

E-commerce Order

$validator = new Validator([
    'customer' => ['required', 'array'],
    'customer.name' => ['required', 'string', ['max', 100]],
    'customer.email' => ['required', 'email'],
    'customer.phone' => ['nullable', ['phone', 'HR']],

    'shipping_address' => ['required', 'array'],
    'shipping_address.street' => ['required', 'string'],
    'shipping_address.city' => ['required', 'string'],
    'shipping_address.postal_code' => ['required', 'string'],
    'shipping_address.country' => ['required', ['in', ['HR', 'SI', 'AT', 'DE']]],

    'items' => ['required', 'array', ['min', 1]],
    'items.*.product_id' => ['required', 'integer'],
    'items.*.quantity' => ['required', 'integer', ['min', 1], ['max', 100]],
    'items.*.notes' => ['nullable', 'string', ['max', 500]],

    'payment' => ['required', 'array'],
    'payment.method' => ['required', ['in', ['card', 'bank_transfer', 'cash']]],
    'payment.iban' => [
        ['when', ['payment.method', '=', 'bank_transfer'], [
            'required',
            ['iban', 'HR'],
        ]],
    ],
]);

Croatian Business Registration

$validator = new Validator([
    'type' => ['required', ['in', ['personal', 'business']]],

    'oib' => ['required', 'oib'],

    'company_name' => [
        ['when', ['type', '=', 'business'], [
            'required',
            'string',
            ['max', 200],
        ]],
    ],

    'vat_number' => [
        ['when', ['type', '=', 'business'], [
            'required',
            'vat_eu',
        ]],
    ],

    'iban' => ['required', ['iban', 'HR']],
    'phone' => ['required', ['phone', 'HR']],
]);

Multi-Country Address Validation

$validator = new Validator([
    'address' => ['required', 'array'],
    'address.street' => ['required', 'string', ['min', 5], ['max', 200]],
    'address.city' => ['required', 'string', ['min', 2], ['max', 100]],
    'address.country' => ['required', 'country_code'],

    // Conditional postal code validation based on country
    'address.postal_code' => [
        'required',
        'string',
        // UK postal codes
        ['when', ['address.country', '=', 'GB'], [
            ['postal_code', 'GB'],
        ]],
        // German postal codes
        ['when', ['address.country', '=', 'DE'], [
            ['postal_code', 'DE'],
        ]],
        // French postal codes
        ['when', ['address.country', '=', 'FR'], [
            ['postal_code', 'FR'],
        ]],
        // Spanish postal codes
        ['when', ['address.country', '=', 'ES'], [
            ['postal_code', 'ES'],
        ]],
        // Danish postal codes
        ['when', ['address.country', '=', 'DK'], [
            ['postal_code', 'DK'],
        ]],
        // Dutch postal codes
        ['when', ['address.country', '=', 'NL'], [
            ['postal_code', 'NL'],
        ]],
    ],
]);

// Example valid data
$result = $validator->validate([
    'address' => [
        'street' => '10 Downing Street',
        'city' => 'London',
        'country' => 'GB',
        'postal_code' => 'SW1A 2AA',
    ],
]);

European Shipping Form

$validator = new Validator([
    'sender' => ['required', 'array'],
    'sender.name' => ['required', 'string', ['max', 100]],
    'sender.street' => ['required', 'string'],
    'sender.postal_code' => ['required', ['postal_code', 'DE']],
    'sender.city' => ['required', 'string'],
    'sender.country' => ['required', ['country_code', 'alpha2']],

    'recipient' => ['required', 'array'],
    'recipient.name' => ['required', 'string', ['max', 100]],
    'recipient.street' => ['required', 'string'],
    'recipient.postal_code' => ['required', 'string'],
    'recipient.city' => ['required', 'string'],
    'recipient.country' => ['required', ['in', ['GB', 'FR', 'ES', 'NL', 'DK', 'DE', 'AT', 'BE']]],

    'package' => ['required', 'array'],
    'package.weight' => ['required', 'numeric', ['min', 0.1], ['max', 30]],
    'package.dimensions' => ['required', 'array'],
    'package.dimensions.length' => ['required', 'integer', ['min', 1], ['max', 150]],
    'package.dimensions.width' => ['required', 'integer', ['min', 1], ['max', 150]],
    'package.dimensions.height' => ['required', 'integer', ['min', 1], ['max', 150]],
]);

Testing

# Run tests
composer test

# Run static analysis
composer analyse

License

MIT

About

PHP library for input validation

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages