Eloquent: Recursos de API
- Introducción
- Generando Recursos
- Visión General del Concepto
- Escribiendo Recursos
- Respuestas de Recursos
Introducción
Al construir una API, es posible que necesites una capa de transformación que se sitúe entre tus modelos Eloquent y las respuestas JSON que realmente se devuelven a los usuarios de tu aplicación. Por ejemplo, es posible que desees mostrar ciertos atributos para un subconjunto de usuarios y no para otros, o que desees incluir siempre ciertas relaciones en la representación JSON de tus modelos. Las clases de recursos de Eloquent te permiten transformar de manera expresiva y sencilla tus modelos y colecciones de modelos en JSON.
Claro, siempre puedes convertir modelos o colecciones de Eloquent a JSON utilizando sus métodos toJson
; sin embargo, los recursos de Eloquent ofrecen un control más granular y robusto sobre la serialización JSON de tus modelos y sus relaciones.
Generando Recursos
Para generar una clase de recurso, puedes usar el comando Artisan make:resource
. Por defecto, los recursos se colocarán en el directorio app/Http/Resources
de tu aplicación. Los recursos extienden la clase Illuminate\Http\Resources\Json\JsonResource
:
php artisan make:resource UserResource
Además de generar recursos que transforman modelos individuales, puedes generar recursos que son responsables de transformar colecciones de modelos. Esto permite que tus respuestas JSON incluyan enlaces y otra información meta que sea relevante para toda una colección de un recurso dado.
Para crear una colección de recursos, debes usar la bandera --collection
al crear el recurso. O, incluyendo la palabra Collection
en el nombre del recurso, indicarás a Laravel que debe crear un recurso de colección. Los recursos de colección extienden la clase Illuminate\Http\Resources\Json\ResourceCollection
:
php artisan make:resource User --collection php artisan make:resource UserCollection
Resumen del Concepto
[!NOTA] Esta es una visión general de alto nivel de los recursos y colecciones de recursos. Se te recomienda encarecidamente leer las otras secciones de esta documentación para obtener una comprensión más profunda de la personalización y el poder que te ofrecen los recursos. Antes de profundizar en todas las opciones disponibles para ti al escribir recursos, primero echemos un vistazo a alto nivel sobre cómo se utilizan los recursos dentro de Laravel. Una clase de recurso representa un solo modelo que necesita ser transformado en una estructura JSON. Por ejemplo, aquí hay una simple clase de recurso
UserResource
: Cada clase de recurso define un métodotoArray
que devuelve el array de atributos que deben convertirse a JSON cuando el recurso se devuelve como respuesta desde una ruta o método de controlador. Nota que podemos acceder a las propiedades del modelo directamente desde la variable$this
. Esto se debe a que una clase de recurso hará automáticamente un proxy del acceso a propiedades y métodos hacia el modelo subyacente para un acceso conveniente. Una vez que se define el recurso, puede ser devuelto desde una ruta o un controlador. El recurso acepta la instancia del modelo subyacente a través de su constructor:
Colecciones de Recursos
Si estás devolviendo una colección de recursos o una respuesta paginada, debes usar el método collection
proporcionado por tu clase de recurso al crear la instancia del recurso en tu ruta o controlador:
Ten en cuenta que esto no permite ninguna adición de metadatos personalizados que puedan necesitar ser devueltos con tu colección. Si deseas personalizar la respuesta de la colección de recursos, puedes crear un recurso dedicado para representar la colección:
php artisan make:resource UserCollection
Una vez que se haya generado la clase de colección de recursos, puedes definir fácilmente cualquier metadato que deba incluirse con la respuesta:
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<int|string, mixed> */ public function toArray(Request $request): array { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; }}
Después de definir tu colección de recursos, puede ser devuelta desde una ruta o controlador:
Preservando las Claves de la Colección
Al devolver una colección de recursos desde una ruta, Laravel restablece las claves de la colección para que estén en orden numérico. Sin embargo, puedes añadir una propiedad preserveKeys
a tu clase de recurso indicando si se deben preservar las claves originales de una colección:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Indicates if the resource's collection keys should be preserved. * * @var bool */ public $preserveKeys = true;}
Cuando la propiedad preserveKeys
está configurada en true
, las claves de la colección se preservarán cuando la colección sea devuelta desde una ruta o un controlador:
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all()->keyBy->id);});
Personalizando la Clase de Recurso Subyacente
Típicamente, la propiedad $this->collection
de una colección de recursos se llena automáticamente con el resultado de mapear cada elemento de la colección a su clase de recurso singular. Se supone que la clase de recurso singular es el nombre de la clase de la colección sin la porción Collection
al final del nombre de la clase. Además, dependiendo de tu preferencia personal, la clase de recurso singular puede o no estar sufijada con Resource
.
Por ejemplo, UserCollection
intentará mapear las instancias de usuario dadas en el recurso UserResource
. Para personalizar este comportamiento, puedes sobrescribir la propiedad $collects
de tu colección de recursos:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * The resource that this resource collects. * * @var string */ public $collects = Member::class;}
Recursos de Escritura
[!NOTE] Si no has leído la visión general del concepto, se te recomienda encarecidamente que lo hagas antes de continuar con esta documentación. Los recursos solo necesitan transformar un modelo dado en un array. Así que, cada recurso contiene un método
toArray
que traduce los atributos de tu modelo en un array amigable con las API que se puede devolver desde las rutas o controladores de tu aplicación:
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Transform the resource into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; }}
Una vez que se ha definido un recurso, puede ser devuelto directamente desde una ruta o un controlador:
Relaciones
Si deseas incluir recursos relacionados en tu respuesta, puedes agregarlos al array devuelto por el método toArray
de tu recurso. En este ejemplo, utilizaremos el método collection
del recurso PostResource
para añadir las publicaciones del blog del usuario a la respuesta del recurso:
use App\Http\Resources\PostResource;use Illuminate\Http\Request; /** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts' => PostResource::collection($this->posts), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
[!NOTA] Si deseas incluir relaciones solo cuando ya han sido cargadas, consulta la documentación sobre relaciones condicionales.
Colecciones de Recursos
Mientras que los recursos transforman un solo modelo en un array, las colecciones de recursos transforman una colección de modelos en un array. Sin embargo, no es absolutamente necesario definir una clase de colección de recursos para cada uno de tus modelos, ya que todos los recursos proporcionan un método collection
para generar una colección de recursos "ad-hoc" sobre la marcha:
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/users', function () { return UserResource::collection(User::all());});
Sin embargo, si necesitas personalizar los metadatos devueltos con la colección, es necesario definir tu propia colección de recursos:
<?php namespace App\Http\Resources; use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ]; }}
Al igual que los recursos singulares, las colecciones de recursos pueden devolverse directamente desde rutas o controladores:
use App\Http\Resources\UserCollection;use App\Models\User; Route::get('/users', function () { return new UserCollection(User::all());});
Envoltura de Datos
Por defecto, tu recurso más externo está envuelto en una clave data
cuando la respuesta del recurso se convierte a JSON. Así que, por ejemplo, una respuesta de colección de recursos típica se ve como lo siguiente:
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", "email": "therese28@example.com" }, { "id": 2, "name": "Liliana Mayert", "email": "evandervort@example.com" } ]}
Si deseas desactivar el envoltura del recurso más externo, debes invocar el método withoutWrapping
en la clase base Illuminate\Http\Resources\Json\JsonResource
. Típicamente, debes llamar a este método desde tu AppServiceProvider
u otro proveedor de servicios que se carga en cada solicitud a tu aplicación:
<?php namespace App\Providers; use Illuminate\Http\Resources\Json\JsonResource;use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider{ /** * Register any application services. */ public function register(): void { // ... } /** * Bootstrap any application services. */ public function boot(): void { JsonResource::withoutWrapping(); }}
[!WARNING] El método
withoutWrapping
solo afecta la respuesta más externa y no eliminará las clavesdata
que añadas manualmente a tus propias colecciones de recursos.
Envolviendo Recursos Anidados
Tienes total libertad para determinar cómo se envuelven las relaciones de tu recurso. Si deseas que todas las colecciones de recursos estén envueltas en una clave data
, sin importar su anidación, debes definir una clase de colección de recursos para cada recurso y devolver la colección dentro de una clave data
.
Puede que te estés preguntando si esto hará que tu recurso más externo esté envuelto en dos claves data
. No te preocupes, Laravel nunca permitirá que tus recursos estén envueltos accidentalmente de forma doble, así que no tienes que preocuparte por el nivel de anidación de la colección de recursos que estás transformando:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class CommentsCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return ['data' => $this->collection]; }}
Envolvimiento de Datos y Paginación
Al devolver colecciones paginadas a través de una respuesta de recurso, Laravel envolverá tus datos de recurso en una clave data
, incluso si se ha llamado al método withoutWrapping
. Esto se debe a que las respuestas paginadas siempre contienen claves meta
y links
con información sobre el estado del paginador:
Paginación
Puedes pasar una instancia de paginador de Laravel al método collection
de un recurso o a una colección de recursos personalizada:
use App\Http\Resources\UserCollection;use App\Models\User; Route::get('/users', function () { return new UserCollection(User::paginate());});
Las respuestas paginadas siempre contienen las claves meta
y links
con información sobre el estado del paginador:
{ "data": [ { "id": 1, "name": "Eladio Schroeder Sr.", "email": "therese28@example.com" }, { "id": 2, "name": "Liliana Mayert", "email": "evandervort@example.com" } ], "links":{ "first": "http://example.com/users?page=1", "last": "http://example.com/users?page=1", "prev": null, "next": null }, "meta":{ "current_page": 1, "from": 1, "last_page": 1, "path": "http://example.com/users", "per_page": 15, "to": 10, "total": 10 }}
Personalizando la Información de Paginación
Si deseas personalizar la información incluida en las claves links
o meta
de la respuesta de paginación, puedes definir un método paginationInformation
en el recurso. Este método recibirá los datos $paginated
y el array de información $default
, que es un array que contiene las claves links
y meta
:
/** * Customize the pagination information for the resource. * * @param \Illuminate\Http\Request $request * @param array $paginated * @param array $default * @return array */public function paginationInformation($request, $paginated, $default){ $default['links']['custom'] = 'https://example.com'; return $default;}
Atributos Condicionales
A veces es posible que desees incluir solo un atributo en una respuesta de recurso si se cumple una condición dada. Por ejemplo, es posible que desees incluir un valor solo si el usuario actual es un "administrador". Laravel proporciona una variedad de métodos auxiliares para ayudarte en esta situación. El método when
se puede utilizar para añadir condicionalmente un atributo a una respuesta de recurso:
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'secret' => $this->when($request->user()->isAdmin(), 'secret-value'), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
En este ejemplo, la clave secret
solo se devolverá en la respuesta del recurso final si el método isAdmin
del usuario autenticado devuelve true
. Si el método devuelve false
, la clave secret
será eliminada de la respuesta del recurso antes de ser enviada al cliente. El método when
te permite definir tus recursos de manera expresiva sin recurrir a declaraciones condicionales al construir el array.
El método when
también acepta una función anónima como su segundo argumento, lo que te permite calcular el valor resultante solo si la condición dada es true
:
'secret' => $this->when($request->user()->isAdmin(), function () { return 'secret-value';}),
El método whenHas
se puede utilizar para incluir un atributo si está realmente presente en el modelo subyacente:
'name' => $this->whenHas('name'),
Además, el método whenNotNull
se puede utilizar para incluir un atributo en la respuesta del recurso si el atributo no es nulo:
'name' => $this->whenNotNull($this->name),
Fusionando Atributos Condicionales
A veces puede que tengas varios atributos que solo deben incluirse en la respuesta del recurso basándose en la misma condición. En este caso, puedes usar el método mergeWhen
para incluir los atributos en la respuesta solo cuando la condición dada sea true
:
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, $this->mergeWhen($request->user()->isAdmin(), [ 'first-secret' => 'value', 'second-secret' => 'value', ]), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
Nuevamente, si la condición dada es false
, estos atributos serán eliminados de la respuesta del recurso antes de que sea enviada al cliente.
[!WARNING] El método
mergeWhen
no debe usarse dentro de arrays que mezclan claves de cadena y numéricas. Además, no debe usarse dentro de arrays con claves numéricas que no estén ordenadas de forma secuencial.
Relaciones Condicionales
Además de cargar atributos de manera condicional, también puedes incluir relaciones de manera condicional en las respuestas de tu recurso según si la relación ya ha sido cargada en el modelo. Esto permite que tu controlador decida qué relaciones se deben cargar en el modelo y tu recurso puede incluirlas fácilmente solo cuando realmente han sido cargadas. En última instancia, esto facilita evitar problemas de consultas "N+1" dentro de tus recursos.
El método whenLoaded
se puede usar para cargar una relación de manera condicional. Para evitar cargar relaciones innecesariamente, este método acepta el nombre de la relación en lugar de la relación misma:
use App\Http\Resources\PostResource; /** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts' => PostResource::collection($this->whenLoaded('posts')), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
En este ejemplo, si la relación no ha sido cargada, la clave posts
será eliminada de la respuesta del recurso antes de ser enviada al cliente.
Contadores de Relaciones Condicionales
Además de incluir relaciones de forma condicional, puedes incluir condicionalmente los "contadores" de relaciones en tus respuestas de recursos según si el conteo de la relación ha sido cargado en el modelo:
new UserResource($user->loadCount('posts'));
El método whenCounted
se puede usar para incluir condicionalmente el conteo de una relación en la respuesta de tu recurso. Este método evita incluir innecesariamente el atributo si el conteo de las relaciones no está presente:
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'posts_count' => $this->whenCounted('posts'), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ];}
En este ejemplo, si el conteo de la relación posts
no ha sido cargado, la clave posts_count
será eliminada de la respuesta del recurso antes de ser enviada al cliente.
Otros tipos de agregados, como avg
, sum
, min
y max
también pueden ser cargados de manera condicional utilizando el método whenAggregated
:
'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),'words_min' => $this->whenAggregated('posts', 'words', 'min'),'words_max' => $this->whenAggregated('posts', 'words', 'max'),
Información de Pivote Condicional
Además de incluir información de relación de manera condicional en las respuestas de tus recursos, también puedes incluir datos de las tablas intermedias de relaciones muchos a muchos de manera condicional utilizando el método whenPivotLoaded
. El método whenPivotLoaded
acepta el nombre de la tabla pivot como su primer argumento. El segundo argumento debe ser una función anónima que devuelva el valor que se debe retornar si la información del pivot está disponible en el modelo:
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'expires_at' => $this->whenPivotLoaded('role_user', function () { return $this->pivot->expires_at; }), ];}
Si tu relación está utilizando un modelo de tabla intermedia personalizada, puedes pasar una instancia del modelo de la tabla intermedia como el primer argumento al método whenPivotLoaded
:
'expires_at' => $this->whenPivotLoaded(new Membership, function () { return $this->pivot->expires_at;}),
Si tu tabla intermedia está utilizando un acceso diferente a pivot
, puedes usar el método whenPivotLoadedAs
:
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'id' => $this->id, 'name' => $this->name, 'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () { return $this->subscription->expires_at; }), ];}
Agregar Metadatos
Algunos estándares de API JSON requieren la adición de metadatos a las respuestas de tus recursos y colecciones de recursos. Esto a menudo incluye cosas como enlaces
al recurso o recursos relacionados, o metadatos sobre el recurso en sí. Si necesitas devolver metadatos adicionales sobre un recurso, inclúyelos en tu método toArray
. Por ejemplo, podrías incluir información de enlaces
al transformar una colección de recursos:
/** * Transform the resource into an array. * * @return array<string, mixed> */public function toArray(Request $request): array{ return [ 'data' => $this->collection, 'links' => [ 'self' => 'link-value', ], ];}
Al devolver metadatos adicionales de tus recursos, nunca tienes que preocuparte por sobrescribir accidentalmente las claves links
o meta
que son añadidas automáticamente por Laravel al devolver respuestas paginadas. Cualquier links
adicional que definas se combinará con los enlaces proporcionados por el paginador.
Metadatos de Nivel Superior
A veces es posible que desees incluir solo ciertos metadatos con una respuesta de recurso si el recurso es el recurso externo que se devuelve. Típicamente, esto incluye información meta sobre la respuesta en su conjunto. Para definir estos metadatos, añade un método with
a tu clase de recurso. Este método debería devolver un array de metadatos que se incluirán con la respuesta del recurso solo cuando el recurso sea el recurso externo que se está transformando:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\ResourceCollection; class UserCollection extends ResourceCollection{ /** * Transform the resource collection into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return parent::toArray($request); } /** * Get additional data that should be returned with the resource array. * * @return array<string, mixed> */ public function with(Request $request): array { return [ 'meta' => [ 'key' => 'value', ], ]; }}
Agregar Meta Datos al Construir Recursos
También puedes añadir datos de nivel superior al construir instancias de recursos en tu ruta o controlador. El método additional
, que está disponible en todos los recursos, acepta un array de datos que se deben añadir a la respuesta del recurso:
return (new UserCollection(User::all()->load('roles'))) ->additional(['meta' => [ 'key' => 'value', ]]);
Respuestas de Recursos
Como ya has leído, los recursos pueden ser devueltos directamente desde las rutas y controladores:
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/user/{id}', function (string $id) { return new UserResource(User::findOrFail($id));});
Sin embargo, a veces puede que necesites personalizar la respuesta HTTP saliente antes de que se envíe al cliente. Hay dos formas de lograr esto. Primero, puedes encadenar el método response
al recurso. Este método devolverá una instancia de Illuminate\Http\JsonResponse
, dándote control total sobre los encabezados de la respuesta:
use App\Http\Resources\UserResource;use App\Models\User; Route::get('/user', function () { return (new UserResource(User::find(1))) ->response() ->header('X-Value', 'True');});
Alternativamente, puedes definir un método withResponse
dentro del recurso mismo. Este método se llamará cuando el recurso se devuelva como el recurso externo en una respuesta:
<?php namespace App\Http\Resources; use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource{ /** * Transform the resource into an array. * * @return array<string, mixed> */ public function toArray(Request $request): array { return [ 'id' => $this->id, ]; } /** * Customize the outgoing response for the resource. */ public function withResponse(Request $request, JsonResponse $response): void { $response->header('X-Value', 'True'); }}