wendelladriel/laravel-validated-dto
Create Data Transfer Objects that validate input on instantiation. Define rules once and reuse them across controllers, services, jobs, and CLI commands—reducing duplication and keeping validation decoupled from HTTP requests. Compatible with Laravel 11–13.
composer require wendelladriel/laravel-validated-dto
php artisan vendor:publish --provider="WendellAdriel\ValidatedDTO\ValidatedDTOServiceProvider"
php artisan make:dto UserCreateDto
This creates a UserCreateDto class in app/Dtos/UserCreateDto.php with a basic structure.use App\Dtos\UserCreateDto;
use Illuminate\Http\Request;
public function store(Request $request)
{
$dto = UserCreateDto::fromRequest($request);
// Validation errors are automatically thrown if invalid
$user = User::create($dto->toArray());
return response()->json($user, 201);
}
vendor/wendelladriel/laravel-validated-dto/src/ValidatedDTO.phpvendor/wendelladriel/laravel-validated-dto/src/Attributes/vendor/wendelladriel/laravel-validated-dto/src/Console/Define Validation Rules:
use WendellAdriel\ValidatedDTO\Attributes\Validated;
#[Validated]
class UserCreateDto
{
#[Validated('required|string|max:255')]
public string $name;
#[Validated('required|email')]
public string $email;
}
Instantiate and Validate:
$dto = UserCreateDto::fromArray($request->all());
// or
$dto = UserCreateDto::fromRequest($request);
Access Validated Data:
$name = $dto->name; // Automatically validated
$data = $dto->toArray(); // Transforms nested DTOs, models, and collections
#[Validated]
class AddressDto
{
#[Validated('required|string')]
public string $street;
}
#[Validated]
class UserCreateDto
{
public AddressDto $address;
}
use WendellAdriel\ValidatedDTO\Attributes\Cast;
#[Validated]
class UserDto
{
#[Cast('date:Y-m-d')]
public string $birthdate;
}
#[Validated(lazy: true)]
class UserDto
{
// Validation runs only when accessing properties
public function __get($key)
{
$this->validate();
return parent::__get($key);
}
}
use App\Dtos\UserCreateDto;
use Illuminate\Console\Command;
class UserCreateCommand extends Command
{
protected $signature = 'user:create {--name= : User Name} {--email= : User Email}';
public function handle()
{
$dto = UserCreateDto::fromArray([
'name' => $this->option('name'),
'email' => $this->option('email'),
]);
// Proceed with validated data
}
}
use WendellAdriel\ValidatedDTO\Attributes\Resource;
#[Resource]
#[Validated]
class UserResourceDto
{
public string $name;
public string $email;
public function toArray(): array
{
return [
'full_name' => $this->name,
'email' => $this->email,
];
}
}
use WendellAdriel\ValidatedDTO\Attributes\CollectionOf;
#[Validated]
class UserListDto
{
#[CollectionOf(UserDto::class)]
public array $users;
}
Validation Timing:
lazy: true to defer validation.$dto->validate() when needed.Nested DTO Transformations:
Arrayable or Jsonable for proper toArray()/toJson() behavior.ValidatedDTO or implement the required interfaces.Circular References:
User has Address, Address has User) may cause infinite loops.#[SkipOnTransform] on problematic properties or break cycles manually.Type Safety:
DateTime property) may not throw errors until validation.#[Cast] attributes for explicit type conversion.Validation Error Handling:
ValidationException by default. Catch them globally or per-use-case.try {
$dto = UserCreateDto::fromArray($data);
} catch (ValidationException $e) {
return response()->json($e->errors(), 422);
}
Inspect Validation Rules:
$dto = new UserCreateDto();
dd($dto->getValidationRules()); // Dump raw validation rules
Enable Debug Mode:
config(['validated-dto.debug' => true]);
Logs validation steps and transformations to storage/logs/laravel.log.
Check for Missing Properties:
toArray() skips properties, verify:
#[Validated] attribute.#[SkipOnTransform].Custom Error Messages:
#[Validated('required|string', message: 'Name must be a string')]
public string $name;
Custom Validation Logic:
use WendellAdriel\ValidatedDTO\Contracts\ValidatedDTO;
class CustomDto extends ValidatedDTO
{
public function validate(): void
{
parent::validate();
// Add custom validation
if ($this->email === 'test@example.com') {
$this->addError('email', 'Test email is not allowed.');
}
}
}
Override Transformation:
use WendellAdriel\ValidatedDTO\Contracts\Arrayable;
#[Validated]
class UserDto implements Arrayable
{
public function toArray(): array
{
return [
'id' => $this->id,
'name' => strtoupper($this->name), // Custom logic
];
}
}
Dynamic Validation:
#[Validated]
class DynamicDto
{
public function getValidationRules(): array
{
$rules = parent::getValidationRules();
if ($this->isAdmin) {
$rules['email'] = 'required|email|verified';
}
return $rules;
}
}
Integration with Laravel Policies:
use App\Dtos\UserUpdateDto;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function update(User $user, UserUpdateDto $dto): bool
{
return $user->id === auth()->id();
}
}
Default Validation Behavior:
config/validated-dto.php:
'validation' => [
'throw_exceptions' => true, // Default: true
'lazy' => false, // Default: false
],
Stub Customization:
php artisan vendor:publish --tag="validated-dto-stubs"
resources/stubs/dtos/.Performance:
#[Lazy] to avoid validating unused properties:
#[Lazy]
public function getFullName(): string
{
return "{$this->firstName} {$this->lastName}";
}
How can I help you explore Laravel packages today?