Skip to content
infocyphPublic

About

Fast, modern PHP request validation and sanitization. Schema-based rules, fail-fast execution, intelligent batching, and 100+ built-in validation rules.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

ReqShield

Security & Standards Packagist Downloads License: MIT Packagist Version Packagist PHP Version GitHub Code Size Documentation

Fast, modern PHP request validation and sanitization. Schema-based rules, fail-fast execution, intelligent batching, and 108 built-in validation rules.

$validator = Validator::make([
    'email' => 'required|email|max:255',
    'age' => 'required|integer|min:18',
])->setSanitizers([
    'email' => ['trim', 'lowercase'],
])->setCasts([
    'age' => 'integer',
]);

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

if ($result->passes()) {
    $clean = $result->typed();
    // All good!
}

Features

  • 108 Built-in Rules - Basic types, conditional rules, files, database checks, enums, and more
  • Built-in Sanitizers - Manual sanitization or built-in sanitize+validate pipeline
  • Intelligent Batching - Expensive DB checks are batched automatically
  • Native DBLayer 6 Bridge - Optional resolver-first exists / unique integration
  • Frozen Compiled Validators - Reusable snapshots for persistent runtimes and Fiber-interleaved execution
  • Schema Registry + Validator Profiles - Instance-owned frozen schema topology and immutable reusable configuration
  • Fail-Fast + Full Collection Modes - Per-field fail-fast with configurable behavior
  • Nested + Wildcard Validation - Dot notation with wildcard expansion
  • Custom Messages + Placeholders - :field, :rule, :min, and more
  • Locale Packs - Per-rule localized message templates with fallback
  • Failure Metadata - Structured failures (field, rule, message, value)
  • Schema Fragments + Composition - Reuse validation contracts across endpoints
  • Conditional Closures - sometimes() and when() for dynamic rule activation
  • Schema Export - JSON Schema, OpenAPI shape, and introspection metadata
  • Typed Output + DTO Mapping - Cast map + toDTO() support
  • Uploaded File Object Support - Array-style uploads and PSR-7 style objects
  • ️ Upload Hardening Rules - safe_filename, upload_meta, upload_id, secure_file
  • ️ PHP 8.4+ - Built with modern PHP features

Installation

composer require infocyph/reqshield

Requirements: PHP 8.4+, ext-hash with xxh3 support, ext-mbstring, and ext-fileinfo

Quick Start

Basic Validation

use Infocyph\ReqShield\Validator;

$validator = Validator::make([
    'email' => [
        'rules' => 'required|email|max:255',
        'sanitize' => ['trim', 'lowercase'],
        'alias' => 'Email Address',
    ],
    'password' => 'required|string|min:8|confirmed',
    'age' => 'required|integer|min:18',
])->setCasts([
    'age' => 'integer',
])->setCustomMessages([
    'email.required' => ':field is required.',
    '*.min' => ':field must be at least :min.',
]);

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

if ($result->passes()) {
    $validated = $result->typed();
    // Process your data...
} else {
    $errors = $result->errors();
    $failures = $result->failures();
    // Handle validation errors...
}

Sanitization

Manual sanitization:

use Infocyph\ReqShield\Sanitizer;

$clean = [
    'email' => Sanitizer::email($input['email']),           // 'john@example.com'
    'username' => Sanitizer::alphaDash($input['username']), // 'john_doe'
    'age' => Sanitizer::integer($input['age']),             // 25
    'bio' => Sanitizer::string($input['bio']),              // Strips HTML tags
];

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

Or built-in sanitize+validate pipeline:

$validator = Validator::make([
    'email' => 'required|email',
    'contacts.*.email' => 'required|email',
])->setSanitizers([
    'email' => ['trim', 'lowercase'],
    'contacts.*.email' => ['trim', 'lowercase'],
]);

Or import the namespaced helper:

use function Infocyph\ReqShield\sanitize;

$clean = sanitize('  TEST@ex.com  ', 'email');           // 'TEST@ex.com'
$clean = sanitize('<b>TEXT</b>', ['string', 'lowercase']); // 'text'

Available Rules (108)

ReqShield includes 108 validation rules covering several common scenarios:

  • Basic Types
  • Formats
  • Strings
  • Numbers
  • Dates
  • Conditionals
  • Database
  • Files
  • Arrays
  • Comparison
  • Patterns
  • Additional

View Complete Rule Reference

Upload Hardening Rules

Use upload-focused rules for request metadata and filename safety:

$validator = Validator::make([
    'upload' => 'required|secure_file',
    'filename' => 'required|safe_filename',
    'upload_id' => 'required|upload_id',
]);

secure_file combines file and upload_meta so you can enforce payload validity and safe upload metadata in one rule.


Available Sanitizers

ReqShield includes built-in sanitizers covering several common scenarios:

  • Basic Types
  • Case Conversions
  • Text Processing
  • Special Formats
  • Alphanumeric Filters
  • HTML and escaping utilities
  • Encoding
  • Array Operations

View Complete Sanitizer Reference

Slug transliteration supports glibc/libiconv or optional Intl, with a separator fallback when neither is available. Generated libiconv accent markers no longer add extra hyphens: Café déjà vu becomes cafe-deja-vu when transliteration is available. Caller punctuation is preserved before the normal slug filter.


Advanced Features

Request Input Helpers

$result = Validator::fromArray($rules, $data);
$result = Validator::fromQuery($rules, $_GET);
$result = Validator::fromBody($rules, $body);
$result = Validator::fromFiles($rules, $_FILES);

PSR-style request objects are supported through fromServerRequest() when compatible accessors are available.

$result = Validator::fromServerRequest($rules, $request);

Strict Unknown Field Handling

$validator = Validator::make($rules)
    ->strict();            // same as allowUnknown(false)

$validator = Validator::make($rules)
    ->stripUnknown();      // remove unknown fields instead of failing

These policies inspect both original and sanitized input, including nested fields introduced by JSON decoding. Unknown descendants are removed from validated parent arrays under either policy. See the sanitization guide for a complete example.

Enum Validation and Casting

'status' => 'required|enum:App\\Enums\\OrderStatus'
'status' => [
    'rules' => 'required|enum:App\\Enums\\OrderStatus',
    'cast' => App\Enums\OrderStatus::class,
]

After Validation Hooks

use Infocyph\ReqShield\Support\ValidationContext;

$validator->after(function (ValidationContext $ctx): void {
    if ((string) $ctx->get('start_date') > (string) $ctx->get('end_date')) {
        $ctx->addError('end_date', 'End date must be after start date.');
    }
});

API Error Formatters and Input Bag

$result->toProblemJson();
$result->toJsonApiErrors();
$result->toApiErrors();
$result->toFlatErrors();
$input = $result->input();
$input->string('email');
$input->int('age');
$input->only(['email', 'age']);

Nested Validation

Validate deeply nested arrays using dot notation:

$validator = Validator::make([
    'user.email' => 'required|email',
    'user.name' => 'required|min:3',
    'user.profile.age' => 'required|integer|min:18',
    'user.profile.bio' => 'string|max:500',
]);

$data = [
    'user' => [
        'email' => 'john@example.com',
        'name' => 'John Doe',
        'profile' => [
            'age' => 25,
            'bio' => 'Software developer',
        ],
    ],
];

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

Nested paths are detected automatically and optimized targeted traversal is the default. Use setNestedFlattenMode('all') only when full flattening is required.

Associative wildcard keys are supported, but keys containing ., , or | throw InvalidArgumentException before rule evaluation because those delimiters cannot safely represent a captured dependency path. This applies to mutable and compiled validators. See nested validation.

Custom Field Names

Make error messages user-friendly:

$validator->setFieldAliases([
    'user_email' => 'Email Address',
    'contacts.*.email' => 'Contact Email',
]);

Custom Messages + Locale Packs

$validator
    ->setCustomMessages([
        'email.required' => ':field is required.',
        '*.min' => ':field must be at least :min.',
        'contacts.*.email.email' => 'Each :field must be valid.',
    ])
    ->addLocalePack('es', [
        'required' => 'El campo :field es obligatorio.',
        '*' => 'El campo :field no es valido.',
    ])
    ->setLocale('es');

Throw Exceptions on Failure

use Infocyph\ReqShield\Exceptions\ValidationException;

$validator = Validator::make($rules)->throwOnFailure();

try {
    $result = $validator->validate($data);
    $validated = $result->validated();
} catch (ValidationException $e) {
    echo $e->getMessage();              // "Validation failed"
    print_r($e->getErrors());           // All errors
    echo $e->getErrorCount();           // Number of failed fields
    echo $e->getFirstFieldError('email'); // First error for specific field
    echo $e->getCode();                 // 0 by default; transport status belongs to your application
}

Failure Metadata for APIs

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

if ($result->fails()) {
    return [
        'errors' => $result->errors(),
        'failures' => $result->failures(), // field, rule, message, value
    ];
}

Conditional Rules

$validator
    ->sometimes('vat', 'required', fn(array $data) => ($data['type'] ?? null) === 'business')
    ->when(
        fn(array $data) => ($data['country'] ?? null) === 'US',
        fn() => ['state' => 'required|string'],
    );

Schema Fragments

Validator::defineFragment('address', [
    'line1' => 'required|string|max:120',
    'zip' => 'required|digits:5',
]);

$validator = Validator::make([
    'name' => 'required|string',
])->useFragment('address', 'billing');

Typed Output + DTO

$validator = Validator::make([
    'age' => 'required|integer',
    'active' => 'required|boolean',
])->setCasts([
    'age' => 'integer',
    'active' => 'boolean',
])->setDtoClass(App\DTO\UserInput::class);

$result = $validator->validate($data);
$typed = $result->typed();
$dto = $result->toDTO();

Frozen Compiled Validator

Validator::compile() returns a reusable frozen execution snapshot. Later mutation of the source builder cannot change the compiled validator.

$compiled = Validator::compile([
    'email' => 'required|email',
    'age' => 'required|integer|min:18',
]);

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

Custom Rules (Simple)

Use callbacks for quick custom validation:

use Infocyph\ReqShield\Rules\Callback;

$validator = Validator::make([
    'code' => [
        'required',
        new Callback(
            callback: fn($value, $field, $data) => $value % 2 === 0,
            message: 'The code must be an even number'
        ),
    ],
]);

Custom Rules (Advanced)

Create reusable rule classes:

use Infocyph\ReqShield\Contracts\Rule;

class StrongPassword implements Rule
{
    public function passes(mixed $value, string $field, array $data): bool
    {
        return strlen($value) >= 12 
            && preg_match('/[A-Z]/', $value)
            && preg_match('/[a-z]/', $value)
            && preg_match('/[0-9]/', $value)
            && preg_match('/[^A-Za-z0-9]/', $value);
    }

    public function message(string $field): string
    {
        return "The {$field} must be at least 12 characters with uppercase, lowercase, number, and special character.";
    }

    public function cost(): int { return 20; }
}

// Usage
$validator = Validator::make([
    'password' => ['required', new StrongPassword()],
]);

Database Validation

Validate against your database:

use Infocyph\ReqShield\Validator;
use Infocyph\ReqShield\Contracts\DatabaseProvider;

// Implement your database provider
class MyDatabaseProvider implements DatabaseProvider
{
    // Implement required methods...
}

$db = new MyDatabaseProvider();

$validator = Validator::make([
    'email' => 'required|email|unique:users,email',
    'category_id' => 'required|exists:categories,id',
], $db);

Database schemas require a provider and throw DatabaseProviderRequiredException when it is absent. The contract contains only batchExists() and batchUnique(); ReqShield owns logical validation batching, while providers own query construction and driver-safe physical chunking. ReqShield is database-library agnostic: a provider may use PDO, DBLayer, Laravel, Doctrine, or another database layer. DBLayer 6.0 is the minimum supported native integration and remains optional for consumers. DBLayer 5.x and ArrayKit versions below 5.3 are unsupported; Composer rejects those versions if present. When DBLayer is installed, ReqShield ships a native resolver-first bridge:

use Infocyph\ReqShield\Bridge\DBLayerDatabaseProvider;
use Infocyph\ReqShield\Validator;

$provider = new DBLayerDatabaseProvider(
    static fn() => $applicationDatabase->connection(),
);

$validator = Validator::make([
    'email' => 'required|email|unique:users,email',
    'category_id' => 'required|exists:categories,id',
], $provider);

The resolver is invoked once per logical database operation. DBLayer 6 owns physical query limits, security policy and binding; typed candidate comparisons may require smaller query chunks.

Benefits:

  • Automatic batching - Multiple checks become bounded DB-native match queries; restricted raw-SQL policies use query-builder-only lookups
  • Update support - Rule::unique('users', 'email')->ignore(5) ignores ID 5
  • Explicit object syntax - Rule::unique('users', 'email')->ignore($id)->withoutTrashed()

Optional Runwire 2.1.1 integration

DBLayer 6.0 is the intended optional database bridge, and Runwire 2.1.1 supplies opt-in host-owned cancellation/deadline integration. Neither package is required for ordinary ReqShield validation.

use Infocyph\ReqShield\Validator;

$compiled = Validator::compile(['items.*.id' => 'required|integer']);

// The active host, not ReqShield, supplies these execution objects.
$result = $compiled->validateWithRunwire(
    $payload, $hostRuntime, $hostRequest, $hostScope,
);

// No Runwire installation is required for the normal path.
$ordinary = $compiled->validate($payload);

The native DBLayer bridge borrows the host context around one logical batch and restores the previous connection binding, including after errors. Cancellation and deadlines are checked around sanitizer, condition, rule, after-callback and cast invocations, and before result delivery. Host cancellation propagates as Runwire's CancelledException; ordinary provider failures retain the sanitized database exception boundary.

Cooperative yielding requires both the host's advertised coroutine capability and a live scope from its current task. Without that capability or scope, validation remains synchronous. When no active Runwire runtime is available, use validate(); intermediary libraries can forward optional host contexts and select the same normal path. ReqShield creates no workers or event loops. See Runwire integration and upgrading to 3.3.

Schema Export / Introspection

$jsonSchema = $validator->exportSchema('json_schema');
$openApiShape = $validator->exportSchema('openapi');
$introspection = $validator->exportSchema('introspection');

Stop on First Error

For maximum performance, stop all validation on first error:

$validator = Validator::make($rules)
    ->setStopOnFirstError(true);

// Stops immediately when any field fails
$result = $validator->validate($data);

Performance

ReqShield is built for speed:

1. Cost-Based Rule Sorting

2. Intelligent Batching

Database rules are automatically batched:

// 3 separate rules...
'user_id' => 'exists:users,id',
'email' => 'unique:users,email',
'category_id' => 'exists:categories,id',

// ...are grouped into logical batches with bounded physical queries.
// Query counts depend on distinct columns, SQL policy and bind/SQL limits.

3. Fail-Fast Execution

Stops validating a field on first rule failure:

'email' => 'required|email|max:255'
// If empty → fails on 'required', skips 'email' and 'max:255'

4. Fast Ordinary Validation

Flat validation does not construct a Runwire context. Use matched-environment, end-to-end host RPM testing to assess production throughput; package microbenchmarks are not a substitute.

Security

Do not disclose suspected vulnerabilities in a public issue, discussion or pull request. Follow SECURITY.md and use GitHub private vulnerability reporting.

ReqShield is protected by PHPForge, which provides automated tests, static and taint analysis, dependency auditing, architecture checks and release-readiness gates. Automated controls do not replace responsible disclosure or manual review.


Made with ❤️ for the PHP community
MIT Licensed
Documentation • Security • Code of Conduct • Contributing
🗂️ Bug • Feature • Documentation • Question • CI failure
🔀 General • Bug fix • Feature • Refactor • Performance • Security & reliability • Documentation • Maintenance

About

Fast, modern PHP request validation and sanitization. Schema-based rules, fail-fast execution, intelligent batching, and 100+ built-in validation rules.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages