rakit/validation
Rakit Validation is a standalone PHP validation library inspired by Laravel. It provides familiar validation rules, custom rules, nested array support, and customizable error messages without requiring the Laravel framework.
Install with Composer:
composer require rakit/validation
As of v1.0.0 the package requires PHP 7.0+. Read the README first (Validator::make() → validate() → errors()/passes()), then src/Rule.php and one shipped rule (e.g. src/Rules/Required.php) — that's the contract you'll extend. If you're doing anything with uploads, also read src/Rules/UploadedFile.php, src/Rules/Mimes.php, and Helper.php; v1.0.0 added the file-validation surface area and the helper methods used to build messages.
A minimal first use case — validating a plain array (from $_POST, a decoded JSON body, or a console command):
use Rakit\Validation\Validator;
$validator = new Validator();
$validation = $validator->make($_POST, [
'name' => 'required|min:3',
'email' => 'required|email',
]);
$validation->validate(); // explicit — do not skip this
if ($validation->fails()) {
$errors = $validation->errors()->toArray(); // ['email' => ['Email is not valid'], ...]
}
$clean = $validation->getValidatedData(); // only the fields that passed
Pass $clean downstream rather than the raw array. If your first use case includes uploads, v1.0.0 lets you validate them without a separate path:
$validation = $validator->make($_POST + $_FILES, [
'avatar' => 'required|uploaded_file:0,2M|mimes:jpg,jpeg,png',
'documents' => 'array',
'documents.*' => 'uploaded_file|max:5M',
]);
Wrap the boilerplate in one helper. The package is stateless; the repetition is the make/validate/branch dance. Centralize it so call sites stay declarative:
function validateOrFail(array $data, array $rules, array $messages = []): array
{
$v = new Validator();
$validation = $v->make($data, $rules, $messages);
$validation->validate();
if ($validation->fails()) {
throw new ValidationException($validation->errors()->toArray());
}
return $validation->getValidatedData();
}
If you register custom rules, do it inside this factory (or a base class), not at random call sites.
Custom rules as classes for anything reused or domain-specific. Extend Rakit\Validation\Rule, implement check(): bool, override the message, and register it:
class IsbnRule extends Rule
{
protected $message = ':attribute is not a valid ISBN';
public function check($value): bool
{
return (bool) preg_match('/^(97[89])?\d{9}[\dX]$/', (string) $value);
}
}
$validator->addValidator('isbn', new IsbnRule());
// then: 'book.isbn' => 'required|isbn'
Rule parameters (myrule:foo,bar) arrive via the rule's parameter setter, so one class can serve several variants instead of subclassing. v1.0.0 added Rule::setParameterText() / getParametersTexts() — use these to render friendly parameter descriptions in messages instead of hand-interpolating tokens:
public function check($value): bool
{
$this->setParameterText('min', 'at least ' . $this->getParameter('min') . ' characters');
// ...
}
Closure rules for one-offs. For a check that genuinely never repeats, put a closure directly in the rule array rather than creating a class:
'coupon' => ['nullable', function ($value) {
return $value === null || str_starts_with($value, 'PROMO-');
}],
Keep closures out of shared rule sets — they can't be tested in isolation and can't be reused.
Nested payloads via dot notation and wildcards. This is the reason to pick Rakit over hand-rolled validation for anything with arrays:
'items' => 'required|array',
'items.*.sku' => 'required|string',
'items.*.qty' => 'required|integer|min:1',
'shipping.address.line1' => 'required',
v1.0.0's uploaded_file support for multiple uploads means the same wildcard pattern now works for $_FILES arrays (documents.*), so you can validate a multi-file form with one rule set.
File uploads as first-class fields. Use uploaded_file for presence/upload-error checks, mimes for allowed types, and min/max/between for file size (v1.0.0 made those size-aware for $_FILES). Keep the size unit consistent across rules and document it next to the rule set; mixing bytes and K/M suffixes is an easy source of confusing failures. uploaded_file now emits context-sensitive default messages, so you usually don't need a custom message for "no file" vs "upload failed."
Compose named rule sets. Keep rules as constants or small methods on the form/DTO class (OrderRules::create(), ::update()), so create vs. update differences (sometimes, required_if) live in one place instead of drifting across controllers.
Structured error responses. $validation->errors()->toArray() is already a per-field map. Pair it with an attribute-alias map so messages read "Shipping address is required" rather than "shipping.address.line1 is required." first($field), has($field), and all() cover most presentation needs. Note that v1.0.0 changed ErrorBag::firstOfAll() to return an associative array (field => message) instead of an indexed list — update any consumer that assumed numeric keys.
Localization and message formatting. v1.0.0 adds Rakit\Validation\Traits\TranslationsTrait (setTranslations() / getTranslations()), so you can register translations on the validator instance directly rather than only relying on the shipped lang/ files. Combine that with Helper::join() and Helper::wraps() when building messages that list allowed values or quote field names:
use Rakit\Validation\Helper;
$allowed = Helper::join(['draft', 'published', 'archived'], ', ', ' or ');
// "draft, published or archived"
$quoted = Helper::wraps(['name', 'email'], '"'); // ['"name"', '"email"']
Use this for in/not_in style messages; v1.0.0's built-in in/not_in messages already include the allowed/disallowed options, so keep custom messages consistent with that format.
Integration points. Since make() takes any array, adapt at the boundary: $request->all() plus $_FILES for uploads, get_object_vars($dto) for simple objects, json_decode($body, true) for webhooks. It slots cleanly into Lumen, legacy PHP apps, and small libraries where pulling in illuminate/validation would be disproportionate.
validate() is not implicit. Unlike Laravel's validate() helper, make() alone does nothing. Calling fails() before validate() gives misleading results — always call it first, or hide it inside a helper.Rule::check() must now return bool — custom rules that returned truthy non-booleans (ints, strings, preg_match results without a cast) need explicit (bool) casts. ErrorBag::firstOfAll() now returns an associative array, so code that did firstOfAll()[0] will break. PHP 7.0 is the new floor; if you still support 5.x, pin the previous release.composer.lock, run your suite on your target PHP version, and keep an eye on deprecation notices. The new typehints and PHPDoc improve static analysis, but they also mean looser dynamic usage that worked before may now fail.$attribute, $value, $parameters, $validator). Don't paste Laravel closure rules in verbatim.items.*.sku come back keyed with the wildcard, and :attribute in messages will render items.*.sku unless you alias the attribute. Map wildcard paths to friendly labels in your error formatter. The same applies to documents.* file fields.nullable is load-bearing. Without it, min:, integer, email, etc. still fire on null/missing values. Put nullable first for optional fields, and consider sometimes when a key may be absent entirely versus present-but-empty.required vs. empty collections. An empty array is typically valid for array but fails required; check your payload shape (missing key, empty string, empty array) against each rule rather than assuming Laravel parity.addValidator() mutates that instance only. If you construct a new Validator() per request (the common pattern), register custom rules in a factory or a base class, or they'll silently be unknown and fail with "validation rule not found."uploaded_file takes size bounds and mimes takes extensions/types; min/max/between now behave differently on $_FILES than on strings. Test with the actual $_FILES shape (including error, tmp_name, and multi-file nesting), not a hand-built array that looks similar.setParameterText(), if you override getMessage() you must still interpolate any remaining placeholders; otherwise users see raw :min-style tokens.Rule subclasses are the clean seam for domain logic; lang/ files and TranslationsTrait are the clean seam for messaging. Avoid patching Validator itself — it's a small class and merges will hurt when you audit against a newer fork or your own patch.How can I help you explore Laravel packages today?