How to implement DTO in Laravel

How to implement DTO in Laravel

In my work, I try to adhere to such a principle that the code is minimally dependent on the framework. So that it can be quickly transferred to another engine if necessary. To do this, we must be minimally tied to concepts such as Model or Request.

In my work, I try to adhere to such a principle that the code is minimally dependent on the framework. So that it can be quickly transferred to another engine if necessary. To do this, we must be minimally tied to concepts such as Model or Request.

DDD design principles and its implementation in Laravel - Porto tell us about this.

If we receive a request from a user, Laravel puts it in a Request object, which is then processed in the controller. And a key principle of good design is to make sure the controller is the last place we work with the Request object. Everyone has probably heard that the controller should be thin, there shouldn't be any logic in it. And this is true. All logic needs to be moved to another place - to services, actions, tasks, etc. If you follow the DDD principle, the controller passes the task to actions, which then distribute it to tasks. And the key question is the following - how can we pass to other classes what came to us from the controller, if we are not recommended to use Requests?

A similar situation occurs in the reverse order. When we get the model and we need to process the data in it, what is the best way to do this, abstracting from Laravel?

There are two options here. The simplest one is to convert Request or model to an array and work with it. But here we are faced with the problem of syntax highlighting and weak typing. We don't know what kind of data is in the array and what type of value is stored in the arrays.

Let me remind you that PHP is a weakly typed programming language. Considering that everything we get in the query is a string, this was not done in vain. And the simplest example, if we have a variable $ a = 'name', which has the "string" type, then we can convert it to an integer value, which will be $ a = 1;

Of course, this policy has its pros and cons. And the main disadvantage, in my opinion, is that strongly typed languages ​​have fewer bugs. If a variable is of an immutable type, then a number of consequences cannot occur. The type guarantee gives the programmer additional insurance against unexpected consequences.

So, you've probably seen code like this a lot:

function store(AccountStoreRequest $request, Account $account)

{

$validated = $request->validated();

$account->name = $validated['name'];

$account->phone = $validated['phone'];

// ...

}

You may be familiar with the problem that we don't know what data is available in the validated array. Arrays only represent a powerful data structure when we use them to represent a list of something. But when we are working with a large set of data that needs to be transformed or processed, there are other tools for that.

The second option, the most correct one, is to use DTO or Data Tranfer Objects, which in fact is an ordinary class that has the properties we need.

class AccountData

{

public int $id;

public string $name;

}

It looks a bit unusual, but it has the same purpose as an array. Those. we can now rewrite the previous code as follows:

function store(AccountStoreRequest $reequest, Account $account)

{

$validated = AccountData::fromRequest($requet);

$account->name = $validated->name;

}

In this approach, we get two advantages - our IDE tells us what properties this class has and they are strongly typed.

You may have noticed that we are using the fromRequest method to create the DTO. Luckily, laravel has a great library to help programmers work with DTOs: https://github.com/spatie/data-transfer-object

Follow the link to view its documentation.

To include this library in your project, run the command:

composer require spatie/data-transfer-object

And now we can create our own DTO. But first, let's create a layer that we'll inherit from to follow the Porto guidelines.

Let's create a new file in the Parents folder:

 

<?php

 

 

namespace Parents\DataTransferObjects;

 

 

use Carbon\Carbon;

 

class ObjectData extends \Spatie\DataTransferObject\DataTransferObject

{

    public static function generateCarbonObject(?string $date): ?Carbon

    {

        if (!$date) {

            return null;

        }

        return \Illuminate\Support\Carbon::createFromFormat(config('panel.date_format') . ' ' . config('panel.time_format'), $date);

    }

}

DTO также позволяет работать с коллекциями, нам потребуется и другой родительский класс:

 

<?php

 

 

namespace Parents\DataTransferObjects;

 

 

class ObjectDataCollection extends \Spatie\DataTransferObject\DataTransferObjectCollection

{

 

}

Теперь у нас есть всё необходимое, чтобы создать свой первый DTO. Давайте сделаем это на примере класса User:

 

<?php

declare(strict_types=1);

 

namespace Domains\Users\DataTransferObjects;

 

use Carbon\Carbon;

use Domains\Currencies\Models\Currency;

use Domains\Teams\Models\Team;

use Domains\Users\Models\User;

use Parents\Requests\Request;

 

 

final class UserData extends \Parents\DataTransferObjects\ObjectData

{

    public int $id;

 

    public string $name;

 

    public string $email;

 

    public ?Carbon $email_verified_at;

 

    public bool $approved;

 

    public bool $verified;

 

    public ?Carbon $verified_at;

 

    public Carbon $created_at;

 

    public string $login;

 

    public int $operations_number = 10;

 

    public int $budget_day_start = 1;

 

    public Currency $primary_currency;

 

    public ?string $timezone;

 

    public ?string $phone;

 

    public ?Team $team;

 

    public string $language;

 

    public bool $google_sync;

 

    public static function fromRequest(Request $request): self

    {

        return new self([

            'name' => $request->get('name'),

            'email' => $request->get('email'),

            'login' => $request->get('login'),

            'operations_number' => (int) $request->get('operations_number'),

            'budget_day_start' => (int) $request->get('budget_day_start')

        ]);

    }

 

    public static function fromModel(User $user): self

    {

        return new static([

            'id' => $user->id,

            'name' => $user->name,

            'email' => $user->email,

            'approved' => (bool) $user->approved,

            'verified' => (bool) $user->verified,

            'verified_at' => self::generateCarbonObject($user->verified_at),

            'created_at' => $user->created_at,

            'login' => $user->login,

            'operations_number' => $user->operations_number,

            'budget_day_start' => $user->budget_day_start,

            'primary_currency' => $user->currency,

            'timezone' => $user->timezone,

            'phone' => $user->phone,

            'team' => $user->team,

            'language' => $user->language,

            'google_sync' => (bool) $user->google_sync,

        ]);

    }

 

}

As you can see, we have two methods that allow us to generate an object from the model or from request.

Moreover, in PHP 8 you can rewrite the code something like this:

 

    public function fromRequest(

        Request $request

    ): UserData {

        return new UserData(

            name: $request->get('name'),

            email: $request->get('email'),

            login: $request->get('login'),

        );

    }

 

And then in the controller or any other method, we can create the object we need in the following way:

 

$user = User::first();

$userData = UserData::fromModel($user);

 

And we get an object that is not of type request and is not a model. Those is framework independent and adheres to design principles.

Now let's go ahead and see how we can return a response via the API. As we did before with models:

 

public function store(StoreUserRequest $request): \Illuminate\Http\JsonResponse

    {

        $user = User::create($request->all());

        $user->roles()->sync($request->input('roles', []));

 

        return (new UserResource($user))

            ->response()

            ->setStatusCode(Response::HTTP_CREATED);

    }

 

We had a UserResource object that returned the desired object in Json format. In the case of DTOs, Laravel doesn't know how to handle it. To do this, we need to tell him about it. To achieve this, we need to implement the Responsable interface. Therefore, let's create an analogue of the UserResource class that will return data in json.

 

<?php

 

 

namespace Parents\DataTransferObjects;

 

 

use Illuminate\Contracts\Support\Responsable;

 

final class ResponseData extends ObjectData implements Responsable

{

 

    public int $status = 200;

 

    public ObjectData|ObjectDataCollection $data;

 

    public function toResponse($request): \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response

    {

        return response()->json(

            [

                'data' => $this->data->toArray(),

            ],

            $this->status

        );

    }

}

 

Here we have two attributes - status (by default we return 200) and data, which the DTO accepts.

Have you noticed the familiar toArray () construct? As with models, the spatie package has an implementation for this method.

Now in the controller we can do something like this:

 

$user = User::whereId(2)->forstOrFail();

 

return new ResponseData([

    'data' => UserData::fromModel($user),

]);

 

In response, we get the following:

{

  "data": {

    "id": 2,

    "name": "user",

    "email": "user@user.com"

  }

}

 

What if we want to work with collections and return the result as pagination?

First, we need to have a class that manages DTO collections. Let's write it:

 

<?php

 

 

namespace Domains\Users\DataTransferObjects;

 

 

use Domains\Users\Models\User;

 

class UserDataCollection extends \Parents\DataTransferObjects\ObjectDataCollection

{

    public function current(): UserData

    {

        return parent::current();

    }

 

    /**

     * @param  User[]  $data

     * @return UserDataCollection

     */

    public static function fromArray(array $data): UserDataCollection

    {

        return new static(

            array_map(fn(User $item) => UserData::fromModel($item), $data)

        );

    }

}

 

Now we need to create a class that will form the response in the form of pagination.

 

<?php

 

 

namespace Parents\DataTransferObjects;

 

 

use Illuminate\Contracts\Support\Responsable;

use Illuminate\Contracts\Pagination\LengthAwarePaginator;

 

class ResponsePaginationData extends ObjectData implements Responsable

{

    public LengthAwarePaginator $paginator;

 

    public ObjectDataCollection $collection;

 

    public int $status = 200;

 

    public function toResponse($request): \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response

    {

        return response()->json(

            [

                'data' => $this->collection->toArray(),

                'meta' => [

                    'currentPage' => $this->paginator->currentPage(),

                    'lastPage' => $this->paginator->lastPage(),

                    'path' => $this->paginator->path(),

                    'perPage' => $this->paginator->perPage(),

                    'total' => $this->paginator->total(),

                ],

            ],

            $this->status

        );

    }

}

 

We now have three attributes: status, paginator, collection. In the controller, we can use it like this:

 

$users = User::paginate(100);

 

return new ResponsePaginationData([

    'paginator' => $users,

    'collection' => UserCollection::fromArray($users->items()),

]);

 

And we get this response:

{

  "data": [

    {

      "id": 1,

      "name": "admin",

      "email": "admin@admin.com"

    },

    {

      "id": 2,

      "name": "User",

      "email": "user@user.com"

    }

  ],

  "meta": {

    "currentPage": 1,

    "lastPage": 1,

    "path": "http://localhost/api/users",

    "perPage": 30,

    "total": 2

  }

}

 

Since working with data is at the heart of almost any project, especially when it comes to DDD, then DTO is an important element of the infrastructure, because they offer a way to work with data in a structured, typed, predictable, and convenient way.