Skip to content

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.

License

Notifications You must be signed in to change notification settings

qbejs/laravel-dto-mapper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“¦ Laravel DTO Mapper

Latest Stable Version Total Downloads License PHP Version Update Packagist

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 UploadedFile mapping
  • πŸ“‹ 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:

  1. Property names match request parameter names (case-sensitive!)
  2. You're sending the parameter in the request
  3. For GET requests, use #[MapQueryString]
  4. 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

⭐ Show Your Support

If this package helped you, please consider giving it a ⭐ on GitHub!


Made with ❀️ for the Laravel community

About

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.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages