yiisoft/hydrator-validator
Integrates Yii Hydrator with Yii Validator to validate input data while hydrating typed objects and DTOs. Map arrays into objects and enforce validation rules in one step, reducing boilerplate and keeping data checks close to object construction.
Minimal steps:
yiisoft/hydrator and yiisoft/validator as dependencies:
composer require yiisoft/hydrator-validator
use Yiisoft\Hydrator\HydratorInterface;
use Yiisoft\Hydrator\Validator\HydratorValidatorFactory;
use Yiisoft\Validator\Validator;
use Yiisoft\Validator\ValidatorInterface;
$this->app->singleton(ValidatorInterface::class, fn () => new Validator());
$this->app->singleton(HydratorInterface::class, fn ($app) =>
(new HydratorValidatorFactory())->create($app->make(ValidatorInterface::class))
);
Check the factory signature in src/HydratorValidatorFactory.php for the exact named arguments in the version you install.README.md, then src/HydratorValidator.php, src/Attribute/Validate.php, and the tests/ directory. The tests are the most reliable documentation for how raw data versus hydrated objects are validated.First use case — replace an ad-hoc FormRequest with a validated DTO:
use Yiisoft\Hydrator\Validator\Attribute\Validate;
use Yiisoft\Validator\Rule\Email;
use Yiisoft\Validator\Rule\Length;
use Yiisoft\Validator\Rule\Required;
final class ContactData
{
public function __construct(
#[Validate]
#[Required]
#[Length(min: 2, max: 100)]
public readonly string $name = '',
#[Validate]
#[Required]
#[Email]
public readonly string $email = '',
#[Validate]
#[Length(max: 1000)]
public readonly string $message = '',
) {}
}
public function store(Request $request, HydratorInterface $hydrator)
{
try {
$data = $hydrator->hydrate(ContactData::class, $request->all());
} catch (ValidationException $e) {
return response()->json(['errors' => $e->getResult()->getErrorMessages()], 422);
}
// $data is fully hydrated and validated
}
If you only need basic array-to-object mapping, yiisoft/hydrator alone is enough. This package is specifically for the case where you want the hydration step and the validation step to be one operation, and where validation should see the raw payload, not just the hydrated object.
Validated DTO at the HTTP boundary. Keep hydration in the controller (or a small action class) and pass the resulting readonly DTO into your service layer. The DTO becomes a proof-of-validation token: downstream code can trust its types and values without re-checking.
Raw-data validation before or alongside hydration. The core reason this package exists: if you validate only the hydrated object, missing input keys silently become property defaults. #[Validate] should be applied so rules see the original array values (or the package version's raw-data flag). Use Required on fields where an empty string default would otherwise pass.
Wrap ValidationException once. Create a base controller or a ValidatedRequest adapter that catches Yiisoft\Hydrator\Validator\Exception\ValidationException, converts getResult()->getErrorMessages() into Laravel's MessageBag, and throws Illuminate\Validation\ValidationException::withMessages(...). Then your controllers stay clean and your API returns Laravel-shaped 422 responses.
Reuse Yii validator rules as attributes. Rules like #[Required], #[Length], #[Email], and #[Regex] are attribute classes from yiisoft/validator. For custom business rules, implement Yiisoft\Validator\RuleInterface and attach it the same way. This is the main extension point for domain-specific checks (e.g., #[UniqueSku]).
Test the DTO directly, not through HTTP. Because the DTO is just a class with attributes, you can unit-test it:
$result = (new Validator())->validate($dto);
$this->assertFalse($result->isValid());
Feature-test the controller with invalid payloads only to verify the exception-to-422 mapping.
Nested DTOs. Hydration of nested objects and arrays requires explicit wiring in the hydrator (a custom attribute handler or a factory). Don't assume array<ItemData> maps automatically; verify with a small unit test first, since nested validation often needs to be declared on the parent as well.
Layer separation. Use the hydrator only at the edge. Don't hydrate Eloquent models from request data — hydrate DTOs, then explicitly map DTOs to models. This keeps mass-assignment risk out of the picture.
ValidationException yourself. Budget a small adapter class for this.Yiisoft\Hydrator\Validator\Exception\ValidationException will bubble up as a 500 unless you catch it. Map it in App\Exceptions\Handler or a base controller.#[Validate] in your pinned version checks the raw array for presence rules; add an explicit Required and a test that omits the key.TypeError or a hydration exception before the validator ever runs. Catch both the hydration exception and the validation exception at the boundary.#[Validate] is opt-in. A newly added constructor parameter without the attribute is silently unvalidated. Consider a coding-standard rule or a test that enumerates DTO parameters.getErrorMessages() returns a flat list keyed by property/rule path, not Laravel's nested field => [messages] structure. Normalize it in your adapter if clients depend on Laravel's JSON error shape.Result. In development, log $e->getResult()->getErrors() rather than just the exception message; it carries the rule-level detail.$request->validate() and this hydrator on the same payload creates two sources of truth and duplicated rules. Pick one per DTO and stick to it.FormRequest plus $request->validated() into a DTO is simpler and better supported. This package earns its place when you want framework-agnostic DTO validation shared with non-Laravel code, or you are already using yiisoft/validator rules.How can I help you explore Laravel packages today?