Serialització

Com serialitzar models Eloquent: toArray, toJson, atributs ocults, appends i API Resources.

Per què serialitzar?#

La serialització és el procés de convertir un model Eloquent (un objecte PHP) en un format que es pugui transportar, com un array o una cadena JSON. Això és essencial quan construeixes APIs que retornen dades en format JSON, quan passes dades a vistes JavaScript, o quan guardes l'estat d'un model en cache.

Serialització bàsica#

Eloquent proporciona dos mètodes principals per serialitzar models:

$user = User::find(1);
 
// Convertir a array PHP
$array = $user->toArray();
 
// Convertir a cadena JSON
$json = $user->toJson();
 
// JSON amb format llegible
$json = $user->toJson(JSON_PRETTY_PRINT);

Quan retornes un model des d'una ruta o controlador, Laravel el serialitza automàticament a JSON:

Route::get('/users/{user}', function (User $user) {
    return $user; // Es converteix a JSON automàticament
});

Les col·leccions Eloquent també es serialitzen:

$users = User::all();
 
$array = $users->toArray(); // Array d'arrays
$json = $users->toJson();   // JSON amb array d'objectes
 
// Retornar directament
return User::where('active', true)->get();

Quan serialitzes un model, les seves relacions carregades també s'inclouen automàticament. Si has fet with('articles'), els articles apareixeran dins la serialització de l'usuari.

Amagar atributs#

Sovint vols excloure atributs sensibles de la serialització, com la contrasenya o el token de recordatori. La propietat $hidden defineix quins atributs no s'inclouen:

class User extends Model
{
    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_secret',
    ];
}
 
$user->toArray();
// No inclou password, remember_token ni two_factor_secret

Alternativament, pots utilitzar $visible com a whitelist: només els atributs llistats s'inclouran:

class User extends Model
{
    protected $visible = [
        'id',
        'name',
        'email',
        'created_at',
    ];
}

$hidden i $visible són mútuament excloents. Utilitza $hidden quan vols amagar uns pocs camps, i $visible quan vols mostrar-ne uns pocs.

Modificar la visibilitat temporalment#

De vegades necessites mostrar o amagar atributs per a una consulta específica sense canviar el model:

// Mostrar temporalment un atribut ocult
$user->makeVisible('password')->toArray();
 
// Amagar temporalment un atribut visible
$user->makeHidden('email')->toArray();
 
// Funciona amb col·leccions
$users = User::all()->makeVisible('password');

Amagar relacions#

Les relacions carregades també es poden amagar:

class User extends Model
{
    protected $hidden = ['articles'];
}
 
// O amagar dinàmicament
$user->makeHidden('articles')->toArray();

Afegir atributs calculats#

Si tens accessors que generen atributs que no existeixen a la base de dades, pots incloure'ls a la serialització amb $appends:

class User extends Model
{
    protected $appends = ['full_name', 'is_admin'];
 
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn (mixed $value, array $attributes) =>
                $attributes['first_name'] . ' ' . $attributes['last_name'],
        );
    }
 
    protected function isAdmin(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->role === 'admin',
        );
    }
}
 
$user->toArray();
// Inclou 'full_name' i 'is_admin' a més dels atributs normals

Per afegir appends dinàmicament:

$user->append('full_name')->toArray();
 
// O amb col·leccions
$users = User::all()->each->append('full_name');

Serialitzar dates#

Per defecte, les dates es serialitzen en format ISO 8601 (2024-03-15T10:30:00.000000Z). Pots personalitzar el format globalment al model:

class Article extends Model
{
    protected function serializeDate(DateTimeInterface $date): string
    {
        return $date->format('Y-m-d H:i:s');
    }
}

O pots personalitzar el format de dates individuals amb casts:

protected $casts = [
    'published_at' => 'datetime:d/m/Y',
    'created_at' => 'datetime:Y-m-d H:i',
];

API Resources#

Les API Resources proporcionen una capa de transformació entre els models Eloquent i les respostes JSON. En lloc de retornar directament el model (que exposa l'estructura interna de la base de dades), una Resource et permet controlar exactament quins camps s'inclouen, com es nomejen i com s'estructuren.

Crear un Resource#

php artisan make:resource UserResource

Això genera un fitxer a app/Http/Resources/UserResource.php:

namespace App\Http\Resources;
 
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
 
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'member_since' => $this->created_at->toDateTimeString(),
        ];
    }
}

Dins del Resource, $this fa referència al model que s'està transformant. Pots accedir a qualsevol atribut o relació del model directament.

Utilitzar Resources#

use App\Http\Resources\UserResource;
 
// Un sol model
public function show(User $user)
{
    return new UserResource($user);
}
 
// Col·lecció de models
public function index()
{
    $users = User::paginate(15);
    return UserResource::collection($users);
}

La resposta JSON per a un sol model:

{
    "data": {
        "id": 1,
        "name": "Joan",
        "email": "joan@exemple.com",
        "member_since": "2024-01-15 10:30:00"
    }
}

Incloure relacions condicionalment#

Els Resources permeten incloure relacions només quan han estat carregades, evitant consultes innecessàries:

class ArticleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->body,
 
            // Només inclou si la relació està carregada
            'author' => new UserResource($this->whenLoaded('author')),
            'comments' => CommentResource::collection($this->whenLoaded('comments')),
 
            // Inclou el recompte si s'ha carregat
            'comments_count' => $this->whenCounted('comments'),
 
            // Inclou condicionalment
            'secret' => $this->when($request->user()?->isAdmin(), $this->secret),
 
            'published_at' => $this->published_at?->toDateTimeString(),
            'created_at' => $this->created_at->toDateTimeString(),
        ];
    }
}

El mètode whenLoaded() retorna la relació si s'ha carregat amb with(), i l'omet completament del JSON si no. Això significa que pots controlar les dades incloses simplement carregant o no les relacions al controlador:

// Retorna article amb autor i comentaris
public function show(Article $article)
{
    $article->load(['author', 'comments']);
    return new ArticleResource($article);
}
 
// Retorna article sense relacions
public function index()
{
    return ArticleResource::collection(Article::paginate(15));
}

Resource Collections#

Per a llistes de models, pots crear un Resource Collection dedicat que afegeixi metadades:

php artisan make:resource UserCollection
namespace App\Http\Resources;
 
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
 
class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total_users' => $this->collection->count(),
                'has_admin' => $this->collection->contains('role', 'admin'),
            ],
        ];
    }
}
return new UserCollection(User::paginate(15));

Paginació amb Resources#

Quan passes un paginador a un Resource, Laravel inclou automàticament les metadades de paginació:

return UserResource::collection(User::paginate(15));
{
    "data": [
        {"id": 1, "name": "Joan"},
        {"id": 2, "name": "Maria"}
    ],
    "links": {
        "first": "http://app.test/api/users?page=1",
        "last": "http://app.test/api/users?page=10",
        "prev": null,
        "next": "http://app.test/api/users?page=2"
    },
    "meta": {
        "current_page": 1,
        "last_page": 10,
        "per_page": 15,
        "total": 150
    }
}

Dades addicionals#

Pots afegir dades al nivell superior de la resposta amb additional():

return (new UserResource($user))
    ->additional([
        'meta' => [
            'permissions' => $user->permissions,
        ],
    ]);