Automatic HTTP request mapping to DTO classes in Laravel using PHP 8 Attributes. A simple, clean, and type-safe way to handle validation and data mapping in your controllers.
- π― PHP 8 Attributes - Clean and modern syntax
- β Automatic Validation - Uses Laravel's built-in Validator
- π Type Safety - Full support for typed properties
- π File Handling - Automatic
UploadedFilemapping - π Arrays & Bulk Operations - Complete array mapping support
- π Zero Configuration - Works out-of-the-box with Package Discovery
- π§ͺ Easy Testing - DTOs are simple PHP classes
- PHP 8.1 or higher
- Laravel 9.x, 10.x, 11.x, or 12.x
composer require qbejs/laravel-dto-mapperThe Service Provider will be automatically registered via Laravel Package Discovery.
<?php
namespace App\DTOs;
use LaravelDtoMapper\Contracts\MappableDTO;
class CreateUserDTO implements MappableDTO
{
public string $name;
public string $email;
public int $age;
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'age' => 'required|integer|min:18',
];
}
public function messages(): array
{
return [
'email.unique' => 'This email address is already taken.',
'age.min' => 'You must be at least :min years old.',
];
}
public function attributes(): array
{
return [
'name' => 'full name',
'email' => 'email address',
'age' => 'age',
];
}
}<?php
namespace App\Http\Controllers;
use App\DTOs\CreateUserDTO;
use LaravelDtoMapper\Attributes\MapRequestPayload;
use Illuminate\Http\JsonResponse;
class UserController extends Controller
{
public function store(
#[MapRequestPayload] CreateUserDTO $dto
): JsonResponse {
$user = User::create([
'name' => $dto->name,
'email' => $dto->email,
'age' => $dto->age,
]);
return response()->json($user, 201);
}
}Your endpoint now:
- β Automatically validates data
- β Returns clear validation errors
- β Maps data to type-safe DTO
- β Is easy to test
Maps data from request body (POST/PUT/PATCH) to DTO.
public function store(
#[MapRequestPayload] CreateUserDTO $dto
): JsonResponse {
// $dto contains validated data from request body
}Options:
validate: bool- Enable validation (default: true)stopOnFirstFailure: bool- Stop at first validation error (default: false)
Examples:
#[MapRequestPayload(validate: false)]
#[MapRequestPayload(stopOnFirstFailure: true)]Maps query string parameters (GET) to DTO.
public function index(
#[MapQueryString] UserFilterDTO $filters
): JsonResponse {
// $filters contains parameters from ?search=...&page=...
}use Illuminate\Http\UploadedFile;
class CreatePostDTO implements MappableDTO
{
public string $title;
public ?UploadedFile $thumbnail;
public function rules(): array
{
return [
'title' => 'required|string',
'thumbnail' => 'nullable|image|max:2048',
];
}
public function messages(): array { return []; }
public function attributes(): array { return []; }
}class CreatePostDTO implements MappableDTO
{
public string $title;
public array $attachments; // array of UploadedFile
public function rules(): array
{
return [
'title' => 'required|string',
'attachments' => 'nullable|array|max:5',
'attachments.*' => 'file|max:10240',
];
}
public function messages(): array { return []; }
public function attributes(): array { return []; }
}public function store(
#[MapRequestPayload] CreatePostDTO $dto
): JsonResponse {
$post = Post::create(['title' => $dto->title]);
if ($dto->thumbnail) {
$path = $dto->thumbnail->store('thumbnails');
$post->update(['thumbnail' => $path]);
}
foreach ($dto->attachments as $file) {
$post->attachments()->create([
'path' => $file->store('attachments'),
]);
}
return response()->json($post, 201);
}class BulkCreateUsersDTO implements MappableDTO
{
public array $users;
public function rules(): array
{
return [
'users' => 'required|array|min:1|max:100',
'users.*.name' => 'required|string',
'users.*.email' => 'required|email|unique:users',
'users.*.age' => 'required|integer|min:18',
];
}
public function messages(): array { return []; }
public function attributes(): array { return []; }
}
// Usage
public function bulkStore(
#[MapRequestPayload] BulkCreateUsersDTO $dto
): JsonResponse {
foreach ($dto->users as $userData) {
User::create($userData);
}
return response()->json([
'created' => count($dto->users)
], 201);
}When validation fails, a 422 response is returned:
{
"message": "Validation failed for field \"email\". Expected type: email, received: string",
"errors": {
"email": ["This email address is already taken."],
"age": ["You must be at least 18 years old."]
},
"field": "email",
"expected_type": "email",
"received_type": "string"
}public function test_creates_user_with_valid_data()
{
$response = $this->postJson('/api/users', [
'name' => 'John Doe',
'email' => 'john@example.com',
'age' => 25,
]);
$response->assertStatus(201)
->assertJsonStructure(['id', 'name', 'email']);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
]);
}
public function test_validation_fails_for_invalid_email()
{
$response = $this->postJson('/api/users', [
'name' => 'John',
'email' => 'invalid-email',
'age' => 25,
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['email']);
}Structure your DTOs by feature/entity:
app/DTOs/
βββ User/
β βββ CreateUserDTO.php
β βββ UpdateUserDTO.php
β βββ UserFilterDTO.php
βββ Post/
β βββ CreatePostDTO.php
β βββ PostSearchDTO.php
βββ Common/
βββ PaginationDTO.php
βββ SortingDTO.php
- Create:
Create{Entity}DTO- for creating new resources - Update:
Update{Entity}DTO- for updating existing resources - Filter/Search:
{Entity}FilterDTO- for search parameters - Bulk:
Bulk{Action}{Entity}DTO- for bulk operations
// β
Good
public string $name;
public int $age;
public ?string $phone;
// β Bad
public $name;
public $age;// β Wrong - will cause errors
class CreateUserDTO implements MappableDTO
{
public function __construct(
public string $name // DON'T DO THIS!
) {}
}
// β
Correct - only public properties
class CreateUserDTO implements MappableDTO
{
public string $name; // Just the property
}Problem: DTO property is not being populated.
Solution: Make sure:
- Property names match request parameter names (case-sensitive!)
- You're sending the parameter in the request
- For GET requests, use
#[MapQueryString] - For POST/PUT/PATCH, use
#[MapRequestPayload]
// Request: ?deviceId=123 (lowercase 'd' in Id)
public string $deviceId; // Must match exactly!
// NOT: ?deviceID=123
// NOT: public string $deviceID;Problem: DTO has a constructor with parameters.
Solution: Remove the constructor. DTOs should only have public properties.
Contributions are welcome! Please see CONTRIBUTING.md for details.
See CHANGELOG.md for recent changes.
The MIT License (MIT). Please see License File for more information.
- π« Create an issue
- π¬ Discussions
If this package helped you, please consider giving it a β on GitHub!
Made with β€οΈ for the Laravel community