« back published by @mmartin_joo on February 8, 2022

Domain-Driven Design with Laravel: Data Transfer Objects

What Is Domain-Driven Design or DDD?

Domain-Driven Design is a software development approach that tries to bring the business language and the source code as close as possible.

I think that's the most simple yet correct definition of DDD. To be a little bit more specific it achieves this by making every important "stuff" a first-class citizen. If "Change invoice status" or "Increase order item quantity" are two sentences that your business people say a lot then you probably should have

  • ChangeInvoiceStatus
  • IncreaseOrderItemQuantity

classes somewhere in your application. That's one example of being a first-class citizen.

Now let's say you're working on some server management / monitoring application. Maybe a commercial one, or some internal tool for your own projects. What is the single most important "stuff" in this application? In my opinion, it's the server status. If it says "Healthy" everyone is happy, but if it says "Down" you know you're about to have a hard day. We know it's important but still, it's just a string in our database table. It's the 15th attribute on our Server model. And we're changing it from "Healthy" to "Deploying" in the 873rd line of some random Command or Job, or in a Model somewhere, or even worse in a Controller. Instead of having these string values and changing them, we can have classes like:

  • ServerStatus/Healthy
  • ServerStatus/Down
  • ServerStatus/Deploying

We can also have transitions like:

  • HealthyToDeploying

So this is what I mean by making things, first-class citizens. This is what Domain-Driven Design is all about (and a little bit more, but now this is the important thing).

What Is a Data Transfer Object or DTO in the DDD World?

What is the most annoying thing when you're working with legacy code? Yeah there's a lot of annoying thing, so I show one of my favorite:

public function store(Request $request)
{
    $product = new Product();
    $product->fill($request->all());

    // Another 320 lines of code where $request comes up again and again
}

Basically you have no idea what properties are on the request. Of course you can check the database and see there are 10 properties in the products table. But if you dig into the store method you find that there's a lot more on the request. It includes 4-5 optional relationships. Each of them is in different format. Of course.

What you can do to avoid these situations? You can create a custom request and add some getters on the class:

class StoreProductRequest extends FormRequest
{
    public function getName(): string
    {
        return $this->name;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function rules(): array
    {
        return [
            'name' => 'required',
            'description' => 'nullable',
            ...
        ];
    }
}

This way you know exactly what kind of data the request has. But now you have to pass the StoreProductRequest around in your business layer. You may have a Service or Action class that creates the product. If you pass the Request as a parameter you cannot use this method from the command line, you cannot write a product import class. And in general having an HTTP request as a dependency outside of a Controller is not a good thing in my opinion.

Meet the DTO! It's a very simple with only one responsibility: store some data and transfer it. That's it! So we can create a ProductData DTO:

class ProductData
{
    public function __construct(
        public readonly string $name,
        public readonly ?string $description,
    ) {}
}

Now we can use this in the Controller:

public function store(Request $request)
{
    $productData = new ProductData(
        $request->getName(), 
        $request->getDescription()
    );

    // now you can pass the ProductData to a Service or an Action...
}

If you have a lot of attributes, or some more complicated data in the request you can write a factory method that construct the DTO from an array:

class ProductData
{
    public function __construct(
        public readonly string $name,
        public readonly ?string $description,
    ) {}

    public static function fromArray(array $data): self
    {
        return new static(
            $data['name'],
            Arr::get($data, 'description'),
        );
    }
}

And use it like this:

public function store(Request $request)
{
    $productData = new ProductData(...$request->all());

    // now you can pass the ProductData to a Service or an Action...
}

Or you can leverage array spread and named arguments:

$productData = new ProductData(...$request->validated());

PHP will extract every element from the array and use each of them as an an argument in the constructor. If you do this you need to give a default for the description:

public function __construct(
    public readonly string $name,
    public readonly ?string $description = null,
) {}

It will only work if the array keys are the same as your argument's names, so it will fail:

// Request
[
    'vat_id' => 1,
]

// ProductData
public function __construct(
    public readonly string $name,
    public readonly int $vatId,
    public readonly ?string $description = null,
) {}

It will fail because the constructor has no $vat_id argument. So this solution won't work every time, and it will cause some messy situations. But the fromArray() factory still has a problem: we don't know what's in the array, right?

There's another solution. A factory that takes a Request:

public static function fromRequest(StoreProductRequest $request): self
{
    return new static(
        $request->getName(),
        $request->getDescription(),
    );
}

This is my personal favorite. It gives me the best of both world:

  • Only Controller and DTO interacts with Requests.
  • I know exactly what data is available at any layer in my application.

This is a huge win in my opinion. There is no class in your application that takes some mystic argument called $data.

You can also use separate DTO factories if you wish. Most of the time I'm perfectly okay with factory methods.

Let's take a recap what we have! Our starting point is the Request:

class StoreProductRequest extends FormRequest
{
    public function getName(): string
    {
        return $this->name;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function rules()
    {
        return [
            'name' => 'required',
            'description' => 'nullable',
        ];
    }
}

We have a Data Transfer Object that can be created from the request above:

class ProductData
{
    public function __construct(
        public readonly string $name,
        public readonly ?string $description = null,
    ) {}

    public static function fromRequest(StoreProductRequest $request): self
    {
        return new static(
            $request->getName(),
            $request->getDescription(),
        );
    }
}

And we can glue them together in the Controller:

class ProductController extends Controller
{
    public function store(StoreProductRequest $request)
    {
        $productData = ProductData::fromRequest($request);
    }
}

The Problem with DTOs in Laravel

DTOs work very well, especially in large projects, but now we have another problem. Just imagine how many classes we need to create to store a product:

  • StoreProductRequest
  • ProductData
  • ProductResource

That’s the bare minimum, but in a real-world application, you may have product prices (as a separate model), category, custom attributes, and so on.

Fortunately, there’s an amazing package called larave-data by Spatie. This package gives DTOs that can be used as:

  • Request (with validation rules)
  • Resource
  • And a simple DTO

So a single ProductData class can eliminate the need for StoreProductRequest and ProductResource. This is what a laravel-data DTO looks like

class ProductData extends Data
{
    public function __construct(
        public readonly ?int $id,
        public readonly string $name,
        public readonly string $description,
        /** @var DataCollection<ProductPriceData> */
        public readonly null|Lazy|DataCollection $prices,
        public readonly null|Lazy|CategoryData $category,
    ) {}
}

The basics are very similar to a pure PHP DTO, but we have this Lazy thing. This is the laravel-data equivalent of Laravel’s whenLoaded method used in resources (it helps you avoid N+1 query problems). So now we have a product and it has a nested ProductPriceData collection, and also a nested Category property.

You can also specify validation rules in this class because this package is able to create a DTO from a request automatically. And the controller will look like this:

class ProductController extends Controller
{
      public function store(ProductData $data): ProductData
      {
            return ProductData::from(
                  CreateProductAction::execute($data)
            );
      }
}

You can inject any Data class and transformation from the request will happen automatically! And as you can see, a Data object can be returned from a controller action and will be cast to JSON (including the nested properties).

In my opinion, it's an amazing tool to have. It helps you reduce the number of classes and unify your data structure.

Difference Between Value Object and DTO

If you read my previous article about Value Objects you may think that a DTO and a VO are the same. They both contain some data. And you're right, but there's some differences.

I have to be honest with you: the above examples are the Laravel way of using DTOs. Originally they were designed to contain a concrete instance of an entity and transform it in your application or to the outer world. A traditional DTO almost always contains an ID.

Sound familiar? In Laravel we use Resources for the same purpose. We map a heavyweight model into a lightweight Resource object with its ID.

So in Laravel we use DTOs just a little bit different then originally. But there are some major difference between a Value Object and a DataTransferObject:

  • There is no logic implemented in a DTO. A Value Object can contain some logic to format the value for example.
  • The DTO almost always carries something that's gonna be inserted into the database. It's basically a beta version of a new Model (or an updated one)
  • A ValueObject will not inserted anywhere. It's just a value wrapped in a cool object.

Of course you don't have to blindly follow these "rules". For example there are developers who use both concepts but call it ValueObject. So they write their DTOs and VOs under the namespace of ValueObjects. And that's fine.

I personally try to separate them, but if you like to keep it simple, just go with one name and use it.

Domain-Driven Design with Laravel