wendelladriel/laravel-validated-dto
Validated DTOs for Laravel: extend ValidatedDTO, declare typed properties and validation rules, and get validated, cast, type-safe data objects from request payloads. Includes built-in casts and config publishing for cleaner controllers.
Install the package and publish its config so transformers and stubs are customizable:
composer require wendelladriel/laravel-validated-dto
php artisan vendor:publish --tag="validated-dto"
First place to look is the package docs site (laravel-validated-dto.wendelladriel.com), specifically the "Getting Started" and "Casting" pages — the README only covers the happy path. Then run php artisan list | grep dto to confirm the generator command (make:dto) is registered; scaffolding a DTO from the stub is faster than hand-writing one and gives you the rules(), defaults(), and casts() skeletons already wired.
The most valuable first use case is replacing a FormRequest + manual array plumbing in a controller with a single DTO. Declare typed properties, define rules, and inject the DTO into the controller action — the container resolves it from the current request, validates it, and hands you a typed object:
final class StoreUserController
{
public function __invoke(UserDTO $dto): JsonResponse
{
$user = User::create($dto->toArray());
return response()->json($user, 201);
}
}
Start with one endpoint, confirm ValidationException still renders as a 422 with the expected error keys (it does, since it reuses Laravel's validator), then expand to the rest of the app. If you want the shape without validation (e.g., outbound API payloads) extend SimpleDTO instead of ValidatedDTO.
Typed properties plus casts() is the whole value proposition. Rules validate the raw input; casts convert it into the typed property. Use StringCast, IntegerCast, FloatCast, BooleanCast, ArrayCast, CarbonCast, EnumCast, CollectionCast, and DTOCast rather than sprinkling (int) and Carbon::parse() in services. The #[Cast] attribute is a per-property alternative to the casts() method when you only need one or two casts.
Nested DTOs and collections. Use DTOCast(AddressDTO::class) for a single nested object and DTOCast(AddressDTO::class, collection: true) (or CollectionCast) for arrays of children. This is the pattern that pays off most in real applications: a CreateOrderDTO containing OrderLineDTO[] and ShippingAddressDTO keeps validation rules co-located with the data they describe, and toArray() recurses correctly as long as you're on a recent 3.x/4.x release (nested serialization was fixed in 3.1.x).
Defaults over nullable. If a field is sometimes, give it an entry in defaults() rather than making the typed property nullable. It removes null checks from every consumer and keeps toArray() output stable for the frontend.
Two directions of data flow. Use fromRequest() / fromArray() / fromJson() for inbound, and fromModel() for outbound. For API responses, a DTO built from a model gives you an explicit, versioned contract — you decide what leaves the application instead of leaking $hidden/$appends behaviour. toModel() covers the write path when you don't want to pass toArray() straight into create().
Lazy validation for queues and commands. fromCommand() and job payloads often arrive in pieces. The lazy validation feature (3.10.0) lets you build the DTO and defer validate() until you actually have all the data, rather than being forced to validate a half-populated payload in the constructor.
Cross-field rules go in after(). Laravel's after validation hook is exposed on the DTO. Use it for "end date must be after start date" style rules instead of dropping into a custom Rule object for one-off cases.
Data transformers for naming conventions. The published config lets you swap the global transformer, or override it per DTO, so camelCase PHP properties can serialize to snake_case JSON (or vice versa) without touching every property. This is the extension point to reach for when integrating with a third-party API.
Custom stubs. Publish and edit the stubs once so every make:dto in the team emits the same base class, namespace, and boilerplate (e.g., a shared BaseDTO with a toResponse() helper).
Uninitialized typed properties are the number-one footgun. Validation runs, then casts populate properties — but if a key is absent, has no default, and isn't sometimes, you'll get Typed property must not be accessed before initialization when you read it. Fix the rule or add a default; don't make everything nullable.
Casts run after validation, so rules see raw input. A rule of boolean on "0"/"1" strings will pass and then BooleanCast normalizes it — that's intended. But a rule of integer on an empty string historically produced surprising cast results (fixed in 3.5.2), so pin a recent patch version and test your edge-case payloads (empty string, "0", null) explicitly.
Don't rely on $this->data inside overridden methods. The data property is cleared after validation (3.6.0), and its lifecycle has changed more than once (3.9.0 fix). Use validatedData() / all() / toArray() instead of reaching into internals.
toArray() is not the same as "all properties". Properties marked #[SkipOnTransform] (4.4.0), plus hidden/forbidden ones, are excluded from serialization. If a field vanishes from a response, check the attribute before you check the cast.
Carbon and enum serialization. Carbon timezone offsets are preserved (3.7.1), but toArray() and toJson() can diverge — enums serialize to their values, Carbon to strings. If your tests assert on toArray() but production returns JSON, add a toJson() assertion too.
Static analysis will complain. PHPStan/Psalm see typed properties assigned outside the constructor. Add @property docblocks to the class or exclude DTOs from the strictest rules; don't "fix" it by switching to public untyped properties, which defeats the point.
File uploads. Uploaded files validated through fromRequest() need the file validation fixes from 3.4.0+. Type the property as UploadedFile and use a dedicated cast or handle the move in the service layer — don't let the DTO write to disk.
Debugging a failing DTO. dd($dto) shows constructor arguments, not resolved properties — use dd($dto->all()) or $dto->toArray(). When validation fails silently in a job, remember ValidationException doesn't render in console context; catch and log $e->errors().
Keep the package current. A large fraction of the changelog between 3.0 and 4.x is serialization fixes (nested DTOs, collections, models, enums, JsonSerializable). Version drift between environments here manifests as subtly different JSON, not hard errors — pin the version in composer.json rather than relying on ^.
How can I help you explore Laravel packages today?