qbejs / laravel-dto-mapper
Automatic DTO mapping for Laravel using PHP 8 Attributes
Installs: 8
Dependents: 0
Suggesters: 0
Security: 0
Stars: 14
Watchers: 0
Forks: 1
Open Issues: 0
pkg:composer/qbejs/laravel-dto-mapper
Requires
- php: ^8.1|^8.2|^8.3
- illuminate/http: ^9.0|^10.0|^11.0|^12.0
- illuminate/routing: ^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0
- illuminate/validation: ^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0
- phpunit/phpunit: ^9.5|^10.0|^11.0
README
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.
โจ Features
- ๐ฏ 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
๐ Requirements
- PHP 8.1 or higher
- Laravel 9.x, 10.x, 11.x, or 12.x
๐ง Installation
composer require qbejs/laravel-dto-mapper
The Service Provider will be automatically registered via Laravel Package Discovery.
๐ Quick Start
1. Create a DTO
<?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', ]; } }
2. Use in Controller
<?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); } }
3. Done! ๐
Your endpoint now:
- โ Automatically validates data
- โ Returns clear validation errors
- โ Maps data to type-safe DTO
- โ Is easy to test
๐ Documentation
Available Attributes
#[MapRequestPayload] - Request Body
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)]
#[MapQueryString] - URL Parameters
Maps query string parameters (GET) to DTO.
public function index( #[MapQueryString] UserFilterDTO $filters ): JsonResponse { // $filters contains parameters from ?search=...&page=... }
File Handling
Single File Upload
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 []; } }
Multiple File Uploads
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 []; } }
Usage in Controller
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); }
Bulk Operations
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); }
Error Handling
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"
}
๐งช Testing
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']); }
๐ก Best Practices
DTO Organization
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
Naming Conventions
- 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
Always Use Type Hints
// โ Good public string $name; public int $age; public ?string $phone; // โ Bad public $name; public $age;
DTOs Should NOT Have Constructors
// โ 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 }
๐ Common Issues
Property must not be accessed before initialization
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;
Unresolvable dependency
Problem: DTO has a constructor with parameters.
Solution: Remove the constructor. DTOs should only have public properties.
๐ค Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
๐ Changelog
See CHANGELOG.md for recent changes.
๐ License
The MIT License (MIT). Please see License File for more information.
๐ Credits
๐ฌ Support
- ๐ซ Create an issue
- ๐ฌ Discussions
โญ Show Your Support
If this package helped you, please consider giving it a โญ on GitHub!
Made with โค๏ธ for the Laravel community