Moving from Laravel API resources to DTOs
February 26, 2020 — 4 min readLaravel’s API resources are a transformation layer that sits between your Eloquent models and the JSON responses returned by your API. This approach is completely fine, however the use case of these resources is quite limited to API responses only, they aren’t type hinted and lack autocompletion.
API resources can be replaced with DTOs. Since native DTOs aren’t supported in PHP, we can use data-transfer-object by Spatie (I’d recommend taking a look at it first, before reading this post), which can be installed by running:
composer require spatie/data-transfer-object
Our First Data Transfer Object
<?php
declare(strict_types=1);
namespace App\DataTransferObjects;
use App\Models\User;
use Spatie\DataTransferObject\DataTransferObject;
final class UserData extends DataTransferObject
{
public int $id;
public string $name;
public string $email;
public static function fromModel(User $user): UserData
{
return new static([
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
]);
}
}
Notice that we’ve defined a static constructor fromModel
, which receives a type hinted User
model. This way we have maximum flexibility to construct the DTO, inject relationships and so on.
Now we can instantiate UserData
:
$user = User::first();
UserData::fromModel($user);
Returning the DTO as a Response
Unfortunately, UserData
can’t be returned by API endpoints, since Laravel doesn’t know how to serialize it.
To achieve this, we can implement the Illuminate\Contracts\Support\Responsable
interface. It would be nice to have whole thing reusable, so we can define ResponseData
, which will implement the Responsable
interface and wrap other DTOs.
Keep in mind, that we still want to wrap our DTOs in a
data
key, just like Laravel’s resources do.
<?php
declare(strict_types=1);
namespace App\DataTransferObjects;
use Illuminate\Contracts\Support\Responsable;
use Spatie\DataTransferObject\DataTransferObject;
final class ResponseData extends DataTransferObject implements Responsable
{
public int $status = 200;
/** @var \Spatie\DataTransferObject\DataTransferObject|\Spatie\DataTransferObject\DataTransferObjectCollection */
public $data;
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
return response()->json(
[
'data' => $this->data->toArray(),
],
$this->status
);
}
}
We’ve defined 2 attributes:
status
- response status (default 200).data
- payload, which can be either a DataTransferObject or DataTransferObjectCollection.
Since we’ve implemented the Responsable
interface, we have to define the toResponse
method, where we can return a JSON response.
Notice the
$this->data->toArray()
part? BothDataTransferObject
andDataTransferObjectCollection
expose thetoArray
method.
Now inside controller methods, we can return the DTO as a response like that:
$user = User::first();
return new ResponseData([
'data' => UserData::fromModel($user),
]);
And we’ll get:
{
"data": {
"id": 1,
"name": "John",
"email": "john@example.org"
}
}
Returning Paginated Collection Responses
The same way we’ve defined ResponseData
, we can define ResponsePaginationData
:
<?php
declare(strict_types=1);
namespace App\DataTransferObjects;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Support\Responsable;
use Spatie\DataTransferObject\DataTransferObject;
use Spatie\DataTransferObject\DataTransferObjectCollection;
final class ResponsePaginationData extends DataTransferObject implements Responsable
{
public LengthAwarePaginator $paginator;
public DataTransferObjectCollection $collection;
public int $status = 200;
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
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
);
}
}
Here, we’ve defined 3 attributes:
status
- response status (default 200).paginator
- Laravel’s paginator.collection
- our collection.
Usage in controller methods:
$users = User::paginate(30);
return new ResponsePaginationData([
'paginator' => $users,
'collection' => new UserCollection($users->items()),
]);
Output:
{
"data": [
{
"id": 1,
"name": "John",
"email": "john@example.org"
},
{
"id": 2,
"name": "Mike",
"email": "mike@example.org"
}
],
"meta": {
"currentPage": 1,
"lastPage": 1,
"path": "http://localhost/api/users",
"perPage": 30,
"total": 2
}
}
However, if you’ve decided to instantiate your DTOs with static constructors, like the fromModel
example above, you won’t be able to instantiate your collections with the default constructor. Instead, we need to define a static constructor in collections as well:
<?php
declare(strict_types=1);
namespace App\DataTransferObjects;
use App\Models\User;
use Spatie\DataTransferObject\DataTransferObjectCollection;
final class UserCollection extends DataTransferObjectCollection
{
public function current(): UserData
{
return parent::current();
}
/**
* @param User[] $data
* @return UserCollection
*/
public static function fromArray(array $data): UserCollection
{
return new static(
array_map(fn(User $item) => UserData::fromModel($item), $data)
);
}
}
So here’s how the controller method will look in the end:
$users = User::paginate(30);
return new ResponsePaginationData([
'paginator' => $users,
'collection' => UserCollection::fromArray($users->items()),
]);
Hey, thank you for reading this post entirely! Hopefully it was helpful for you. If you’ve enjoyed this post, subscribe below and get notified when new articles will be released.
Join the Newsletter
Subscribe to get my latest posts by email.