Els API Resources de Laravel són l'estàndard de facto per a la transformació de respostes API. Estan ben documentats, tenen suport oficial i són efectius per operacions CRUD senzilles. No obstant això, a mesura que augmenta la complexitat de l'aplicació i evolucionen els patrons de consulta, les limitacions d'aquest enfocament es fan evidents.
Aquest article examina quan els API Resources són apropiats i quan els Data Transfer Objects (DTOs) proporcionen beneficis arquitectònics superiors.
El cas estàndard: API Resources són suficients
Considera un endpoint CRUD típic d'usuaris on la resposta reflecteix fidelment el model subjacent:
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->format('Y-m-d'),
];
}
}
// Controller
public function index()
{
$users = User::all();
return UserResource::collection($users);
}Beneficis: codi mínim, implementació ràpida, reutilitzable en múltiples endpoints.
El problema N+1: un error comú
A mesura que els requisits s'expandeixen per incloure dades relacionals, emergeix un antipatró comú:
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'posts_count' => $this->posts->count(), // 🚨 N+1 query
'last_comment' => $this->comments()->latest()->first(), // 🚨 N+1 query
];
}
}Aquesta implementació dispara consultes addicionals per cada entitat. Amb 50 usuaris, això resulta en més de 100 consultes a la base de dades.
La solució canònica: eager loading al controlador:
$users = User::with(['posts', 'comments'])->get();No obstant això, aquest enfocament introdueix un problema de separació de responsabilitats: el Resource accedeix lliurement a les relacions mentre la responsabilitat d'optimitzar les consultes resideix al controlador. Oblidar la clàusula with() resulta en degradació del rendiment que pot no ser immediatament evident.
Agregacions complexes: on els Resources mostren limitacions
Considera un endpoint que requereix dades agregades de múltiples taules:
$stats = DB::table('users')
->join('posts', 'users.id', '=', 'posts.user_id')
->join('comments', 'posts.id', '=', 'comments.post_id')
->selectRaw('
users.id,
users.name,
COUNT(DISTINCT posts.id) as posts_count,
COUNT(comments.id) as comments_count,
MAX(posts.created_at) as last_post_date
')
->groupBy('users.id', 'users.name')
->get();Aquesta consulta retorna objectes stdClass en lloc de models Eloquent. Adaptar Resources per aquest cas d'ús requereix compromisos:
class UserStatsResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id ?? null,
'name' => $this->name ?? null,
'posts_count' => $this->posts_count ?? 0,
'comments_count' => $this->comments_count ?? 0,
];
}
}Tot i que funcional, aquest enfocament sacrifica la seguretat de tipus i introdueix ambigüitat sobre les expectatives d'estructura de dades.
DTOs: un enfocament alternatiu
Els Data Transfer Objects proporcionen estructura explícita i desacoblen l'obtenció de dades de la lògica de transformació:
// app/DTOs/UserStatsDTO.php
class UserStatsDTO
{
public function __construct(
public int $id,
public string $name,
public int $postsCount,
public int $commentsCount,
public ?string $lastPostDate
) {}
public static function fromQueryResult(object $result): self
{
return new self(
id: $result->id,
name: $result->name,
postsCount: $result->posts_count,
commentsCount: $result->comments_count,
lastPostDate: $result->last_post_date
);
}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'posts_count' => $this->postsCount,
'comments_count' => $this->commentsCount,
'last_post_date' => $this->lastPostDate,
];
}
}
// Controller
public function stats()
{
$results = DB::table('users')
->join('posts', 'users.id', '=', 'posts.user_id')
->selectRaw('...')
->get();
$dtos = $results->map(fn($r) => UserStatsDTO::fromQueryResult($r));
return response()->json($dtos->map->toArray());
}Avantatges: Seguretat de tipus completa amb propietats tipades de PHP 8+, contractes explícits d'estructura de dades, eliminació de consultes N+1 accidentals, testabilitat independent de la lògica de transformació, i reutilització en diferents capes de l'aplicació (jobs, events, comandaments).
Anàlisi comparativa
API Resources: casos d'ús òptims
Operacions CRUD simples, estructura de resposta que reflecteix fidelment l'estructura del model, equips amb fort coneixement del framework Laravel, i aplicacions de petita a mitjana escala.
DTOs: escenaris preferits
Consultes complexes amb joins i agregacions, composició de dades de múltiples fonts, APIs crítiques en rendiment, requisits estrictes de contractes, i necessitats de cobertura completa de tests.
Recomanació arquitectònica
En lloc d'adoptar un enfocament monolític, selecciona el patró apropiat segons els requisits específics de cada endpoint:
// CRUD simple → API Resource
public function show(User $user)
{
return new UserResource($user->load('profile'));
}
// Agregació complexa → DTO
public function dashboard()
{
$stats = $this->calculateDashboardStats();
return response()->json(
DashboardDTO::fromStats($stats)->toArray()
);
}Indicadors per adoptar DTOs: Ús de selectRaw() o DB::raw() en consultes, tres o més joins de taules, divergència significativa entre estructura de resposta i model, problemes recurrents de N+1, i requisits per testar la lògica de transformació de forma aïllada.
Conclusió
Tot i que els API Resources representen l'enfocament recomanat pel framework i funcionen efectivament per casos d'ús estàndard, la filosofia de Laravel no obliga a la seva aplicació universal. Els DTOs ofereixen control superior i claredat arquitectònica en escenaris que involucren transformacions de dades complexes.
El disseny òptim de programari prioritza solucions específiques al problema sobre l'adherència dogmàtica a patrons. L'elecció entre Resources i DTOs ha d'estar guiada per les característiques específiques de cada endpoint en lloc de la pràctica convencional.