toa/validator
toa/validator is a PHP validation package for Laravel applications, offering a structured way to define and apply validation rules. It integrates with Laravel's validation workflow, helping developers keep request and data validation organized and reusable.
toa/validator is a Symfony-Validator-inspired component with effectively zero public adoption (0 stars, 0 dependents). That single fact should shape your whole approach: the source and tests are the documentation, not the README, and not Symfony's docs.
Minimal steps
composer require toa/validator, then commit composer.lock and pin the exact version in composer.json. Do not use a floating constraint on a package nobody else is exercising.composer.json first — check the PHP version constraint, whether it pulls in a translator, a PSR container, or annotation/attribute parsing libraries. Those transitive dependencies often reveal more about the design than the README does.src/ top-down. You are looking for four things:
Validator class with a validate() method),Constraint base class and how options are passed to it,ConstraintValidatorInterface),validate().tests/ next. For a package at this maturity, the test suite is the most accurate record of intended usage and edge-case behavior.First use case — validate an inbound payload before it reaches a service:
use Toa\Validator\Validator;
use Toa\Validator\Constraint as Assert; // confirm namespace in src/
$validator = new Validator(); // constructor signature varies — check src/Validator.php
$errors = $validator->validate($payload, [
new Assert\NotBlank(['message' => 'Email is required']),
new Assert\Email(),
new Assert\Length(['min' => 3, 'max' => 255]),
]);
if (count($errors) > 0) {
// Inspect the error object first: getMessage(), getPropertyPath(),
// or array access — the shape is package-specific.
}
Before wiring this into a controller, spend ten minutes confirming the result object's API. Whether validate() returns an array, an ErrorList, or a traversable determines how you format 422 responses, and guessing here produces the most common early bug.
Define the boundary immediately. Create one ValidationService interface in your application and one adapter class that wraps toa/validator. Every call site depends on your interface, never on the package's classes. Given the adoption risk, this is the cheapest possible insurance policy — if the package stalls, you rewrite one adapter and swap in symfony/validator, which this package is explicitly modeled on.
Per-field constraint maps. The natural fit for request validation is a map of field name to constraint list, applied in a loop:
$rules = [
'email' => [new Assert\NotBlank(), new Assert\Email()],
'password' => [new Assert\NotBlank(), new Assert\Length(['min' => 8])],
];
$errors = [];
foreach ($rules as $field => $constraints) {
$result = $validator->validate($payload[$field] ?? null, $constraints);
if (count($result) > 0) {
$errors[$field] = $result; // normalize into your own error shape here
}
}
Flattening into your own array structure at the boundary keeps the package's error objects from leaking into views, API responses, and tests.
Custom constraint + validator pair. This is where the Symfony lineage pays off. You define a constraint class (a value object holding options/messages) and a validator class (the logic), then register the association. Look at how existing constraints in src/ declare their validator — via a validatedBy()-style method, an attribute, or a registry — and copy that exact mechanism. Do not assume the Symfony registration path works.
final class Uuid extends Assert\Constraint
{
public string $message = 'Invalid UUID.';
}
final class UuidValidator // implements the package's validator contract
{
public function validate($value, Uuid $constraint): void
{
if ($value !== null && !preg_match('/^[0-9a-f]{8}-/i', $value)) {
// report violation via whatever API the package exposes
}
}
}
Nested and collection validation. If the package supports cascading into objects, verify it with a three-line test before relying on it in a domain model. If it does not, do not build deep object-graph validation on top of it — validate at the edges (arrays/DTOs) and let your domain enforce its own invariants. This is a better architecture anyway and avoids depending on an unproven traversal implementation.
Conditional and grouped rules. Symfony-style groups are useful for "validate on create" vs "validate on update". Check whether groups are actually implemented or just named in the API. If they are stubbed, model conditions in plain PHP — build a different constraint array per scenario — rather than fighting a half-implemented feature.
Test fixtures. Write a small ValidatorFactory helper for tests that returns a configured validator instance. Because the constructor signature and dependencies (translator? container?) are package-specific, centralizing construction keeps your test suite stable if you later swap implementations.
Integration point. Wire validation into a form-request or middleware layer, not into controllers directly, and return a consistent error payload. Keep the adapter's surface narrow: validate(array $data, array $rules): array returning plain arrays is usually the entire API you need.
Symfony documentation will mislead you. The package is inspired by Symfony, not compatible with it. Class names, option keys, the error API, and constraint registration may all differ. Every assumption must be checked against src/ or tests/. Copying a Symfony snippet and fixing namespaces is a reliable way to burn an afternoon.
No community support means no bug oracle. With zero dependents, you are likely the first person to hit any given bug. Budget for reading the implementation when something behaves oddly, and add a regression test in your suite for every quirk you discover — upstream fixes may never arrive.
Null vs empty string semantics. Constraint chains usually treat null as "skip unless NotBlank" and '' differently. Test both explicitly for each constraint you adopt. This is the single most common source of validation mismatches in Symfony-derived libraries.
Constructor and dependency coupling. If Validator requires a translator or a container at construction, instantiate it once and inject it — do not build it per request. Also check whether messages are translated by default; if a translator is required but optional, missing it may silently swallow messages or throw.
Attributes vs annotations. If the package supports metadata-driven validation, confirm which PHP versions and which mechanism (docblock annotations need a parser dependency; attributes need PHP 8+). If metadata support is partial, prefer explicit constraint arrays — they are greppable, testable, and immune to reflection quirks.
Error object mutability and shape. Some implementations return mutable violation lists that accumulate if you reuse the validator instance across a loop. Validate that a fresh call returns a fresh result, or reset explicitly. Normalize to arrays immediately so downstream code never touches package objects.
Pin, wrap, and plan the exit. Pin the version, keep the adapter as the only integration point, and write one integration test that runs the adapter end-to-end. If the package becomes unmaintained, that single test plus the adapter is your migration checklist to symfony/validator — whose API this package mirrors closely enough that the swap should be mechanical.
Extension points worth knowing. Identify early whether the package exposes a way to register custom validators globally, override messages, or hook error formatting. Those are the levers you will need most often in day-to-day work, and in a low-adoption package they are sometimes the least documented part of the code.
How can I help you explore Laravel packages today?