DTO package for Laravel.

1.0.1 2025-02-13 15:39 UTC

This package is auto-updated.

Last update: 2025-06-13 16:17:52 UTC


README

Introduction

LTO helps you structure and manage data transfer in your application. It simplifies handling incoming request data, ensures type safety, and improves code clarity.

By leveraging DTOs, you can enforce a consistent data structure, prevent unexpected values, and seamlessly integrate with Laravel’s validation, models, and dependency injection.

πŸš€ Why Use LTO?

βœ… Full IDE Auto-Completion Support β†’ Since DTOs use typed properties and constructor parameters, your IDE (PhpStorm, VS Code, etc.) can provide full auto-completion while writing code.

βœ… Strict Type Safety β†’ Prevents unexpected data structures and makes your code more predictable.

βœ… Improved Code Readability β†’ Instead of working with raw request arrays, DTOs give a structured way to handle incoming data.

βœ… Seamless Integration with Laravel β†’ Works natively with request validation, dependency injection, and models.

Installation

You can install LTO via Composer:

composer require serter35/lto

LTO supports Laravel 10 and Laravel 11 and requires PHP 8.2 or higher.

Quick Start

First, create a DTO class:

php artisan lto CommentDTO

Define the DTO class with the relevant properties.

For example, in the following example, we have created a DTO to store comments:

<?php

namespace App\DTOs;

use SerterSerbest\LTO\Attributes\Request\FromBody;
use SerterSerbest\LTO\BaseDTO;

#[FromBody] 
class CommentDTO extends BaseDTO
{
    public function __construct(
        public ?string $title,
        public ?string $description
    ) {}
}

You can use this DTO class within a controller using Laravel's Method Injection feature:

<?php

namespace App\Http\Controllers;

use App\DTOs\CommentDTO;
use App\Models\Comment;

class CommentController extends Controller
{
    public function store(CommentDTO $dto)
    {
        return Comment::create($dto->toArray());
    }        
}

This way, the #[FromBody] attribute automatically maps the body data of the Laravel request to the DTO. Now, let's take a look at what kind of DTOs we can create for different request types.

Creating a DTO

1. Using Request DTO

You can use the relevant attributes to bind DTOs with Laravel requests. If you want to use DTOs via Dependency Injection in controllers, you must specify an attribute (e.g., #[FromBody]). However, attributes are not mandatory for using DTOsβ€”you can still instantiate them manually. The package supports the following request sources:

  • FromBody β†’ Gets data from the body of the request.
  • FromQuery β†’ Gets data from the query string.
  • FromRoute β†’ Gets data from route parameters.
  • FromRequest β†’ Automatically maps without specifying the source.

a) Using FromBody

#[FromBody] 
class PostStoreDTO extends BaseDTO
{
    public function __construct(
        public ?string $title,
        public ?string $description
    ) {}
}

b) Using FromQuery

#[FromQuery]
class QueryFilterDTO extends BaseDTO
{
    public function __construct(
        public ?string $sortColumn,
        public ?string $sortBy,
        public ?array $contains
    ) {}
}

Usage example:

[GET] http://localhost/users?sortColumn=created_at&sortBy=desc&contains[key]=name&contains[value]=john

c) Using a Complex DTO

You can gather different request parameters within a single DTO. In the example below, the #[FromBody] attribute is applied to the class itself, meaning that by default, all properties will be mapped from the request body. However, individual properties can override this behavior using different attributes such as #[FromQuery] or #[FromRoute].

#[FromBody]
class CommentBodyDTO extends BaseDTO
{
    public function __construct(
        public ?string $title,
        public ?string $body,
        #[FromQuery] public ?bool $no_interaction,
        #[FromRoute('post')] public int $postId
    ) {}
}

d) Using FromRequest

To automatically scan all request sources:

#[FromRequest]
class CommentBodyDTO extends BaseDTO
{
    public function __construct(
        public ?string $firstname,
        public ?string $lastname
    ) {}
}

🎯 Using Binding Keys in Attributes

In addition to the default behavior, attributes allow you to specify a binding key to map request data from a different field name.

For example, if the request body contains a field named "header", but you want to map it to $title in the DTO, you can do:

#[FromBody('header')]
public ?string $title;

This tells the package to extract the "header" field from the request body and assign it to $title. You can use this approach to rename request fields dynamically while keeping your DTO structure clean.

2. Creating a Simple DTO (Without Attributes)

Attributes are not required to define a DTO. You can create a DTO as a regular PHP class without using attributes.

However, if a DTO does not have an attribute, Laravel will not be able to automatically inject it via Dependency Injection (DI) in controllers. Instead, you must manually instantiate it.

class PostStoreDTO extends BaseDTO
{
    public function __construct(
        public ?string $title,
        public ?string $description
    ) {}
}

πŸš€ Note: Even without attributes, you can still use methods like fromRequest(), fromArray(), and fromModel() to populate the DTO. These methods are explained in the following sections.

Using DTOs

1. Using Dependency Injection (DI)

You can pass DTOs into controller methods via Dependency Injection. This takes advantage of Laravel's automatic dependency resolution feature.

public function store(UserCreateDTO $dto)
{
    return User::create($dto->toArray());
}  

For details on how DTOs are created, see the Creating a DTO section.

2. Instantiating DTOs Manually

You can manually create a DTO instance using the new keyword. This approach is useful when Dependency Injection is not an option, or when you need to create a DTO dynamically.

$dto = new UserCreateDTO(
    name: 'John Doe',
    email: 'john.doe@example.com'
);

2. Using FromRequest

DTOs can be used to directly retrieve data from the incoming request object. This is a common approach to integrate request validation and data processing into the DTO.

public function store(UserStoreFormRequest $request)
{
    $dto = UserCreateDTO::fromRequest($request);
    return $this->postService->create($dto);
}   

3. Using FromModel

When retrieving data from a model, you can convert the DTO to a model instance. This is ideal for using model data in a specific format.

public function changeActive(User $user)
{
    return tap($user, function (User $user) {
        $user->update(['status' => 1]);
        $dto = MailBodyDTO::fromModel($user);
        $this->mailService->send($dto);
    });
} 

4. Using FromArray

This is used to convert an array of data into a DTO. It's a common way to work with external data sources.

$dto = UserDTO::fromArray([
    'name' => 'John Doe',
    'email' => 'john.doe@example.com',
    'password' => 'secret'
]);

Converting DTOs

You can convert a DTO to different formats using the following methods:

  • toArray() β†’ Converts to an array.
  • toCollection() β†’ Converts to a Laravel Collection.
  • toModel() β†’ Converts to a model instance.

Examples:

$attributes = $dto->toArray();
$collection = $dto->toCollection();
$userModel = $dto->toModel(User::class);

DTOs with Validation Support

1. Defining a Validatable DTO

To create a Validatable DTO class via the console, you can run the following command:

php artisan lto UserDTO --validatable

To add validation support to an existing DTO class, you can use the Validatable trait:

#[FromBody]
class UserUpdateDTO extends BaseDTO
{
    use Validatable;
    
    public function __construct(
        public ?string $first_name,
        public ?string $last_name,
        public ?string $email
    ) {}
    
    protected function getValidationRules(): array
    {
        return [
            'first_name' => 'required|min:2|max:255'
        ];   
    }
}

To customize validation error messages:

protected function getValidationMessages(): array
{
    return [
        'first_name.required' => ':attribute cannot be empty.'
    ];
}
protected function getValidationAttributes(): array
{
    return [
        'first_name' => 'First Name'
    ];
}

2. Using DTO Validation

public function update(User $user, UserUpdateDTO $dto)
{
    $validated = $dto->validate();
    return $user->update($validated);
}

To get only the validated data:

$validatedArray = $dto->toValidatedArray();
$validatedCollection = $dto->toValidatedCollection();
$validatedModel = $dto->toValidatedModel(User::class);