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!
}- 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/uniqueintegration - 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()andwhen()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
composer require infocyph/reqshieldRequirements: PHP 8.4+, ext-hash with xxh3 support, ext-mbstring, and ext-fileinfo
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...
}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'ReqShield includes 108 validation rules covering several common scenarios:
- Basic Types
- Formats
- Strings
- Numbers
- Dates
- Conditionals
- Database
- Files
- Arrays
- Comparison
- Patterns
- Additional
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.
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.
$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);$validator = Validator::make($rules)
->strict(); // same as allowUnknown(false)
$validator = Validator::make($rules)
->stripUnknown(); // remove unknown fields instead of failingThese 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.
'status' => 'required|enum:App\\Enums\\OrderStatus''status' => [
'rules' => 'required|enum:App\\Enums\\OrderStatus',
'cast' => App\Enums\OrderStatus::class,
]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.');
}
});$result->toProblemJson();
$result->toJsonApiErrors();
$result->toApiErrors();
$result->toFlatErrors();$input = $result->input();
$input->string('email');
$input->int('age');
$input->only(['email', 'age']);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.
Make error messages user-friendly:
$validator->setFieldAliases([
'user_email' => 'Email Address',
'contacts.*.email' => 'Contact Email',
]);$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');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
}$result = $validator->validate($data);
if ($result->fails()) {
return [
'errors' => $result->errors(),
'failures' => $result->failures(), // field, rule, message, value
];
}$validator
->sometimes('vat', 'required', fn(array $data) => ($data['type'] ?? null) === 'business')
->when(
fn(array $data) => ($data['country'] ?? null) === 'US',
fn() => ['state' => 'required|string'],
);Validator::defineFragment('address', [
'line1' => 'required|string|max:120',
'zip' => 'required|digits:5',
]);
$validator = Validator::make([
'name' => 'required|string',
])->useFragment('address', 'billing');$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();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);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'
),
],
]);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()],
]);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()
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.
$jsonSchema = $validator->exportSchema('json_schema');
$openApiShape = $validator->exportSchema('openapi');
$introspection = $validator->exportSchema('introspection');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);ReqShield is built for speed:
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.Stops validating a field on first rule failure:
'email' => 'required|email|max:255'
// If empty → fails on 'required', skips 'email' and 'max:255'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.
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.
MIT Licensed
Documentation • Security • Code of Conduct • Contributing
🗂️ Bug • Feature • Documentation • Question • CI failure
🔀 General • Bug fix • Feature • Refactor • Performance • Security & reliability • Documentation • Maintenance