Esporre delle risorse, utilizzando le Api Resource di Eloquent

E

Quando sviluppiamo delle API REST particolarmente complesse, può essere di enorme aiuto introdurre un layer di trasformazione, per isolare le “logiche di presentazione” (dei dati) al di fuori dei controller, rendendo i Model più leggeri, il codice più manutenibile e sempre più conforme al principio DRY.

Introdotte con Laravel 5.5, le API Resource di Eloquent ci forniscono un modo, semplice e veloce, per manipolare, in fase di serializzazione, l’output prelevato dal nostro database (nulla di nuovo per chi ha già lavorato con Fractal, che può direttamente passare alla code implementation).

Partiamo subito con un esempio pratico: supponiamo di avere un’entità Post da esporre su un endpoint REST:

class Post extends Model
{
    protected $fillable = [
        'title',
        'subtitle',
        'body',
        'published_at',
    ];
}

Route::get('post/{id}', function($id) {
    return Post::find($id);
});

Quando in una route ritorniamo un model, quest’ultimo viene serializzato includendo tutti i campi presenti nell’entità (ad eccezione dei campi hidden). Ma, se abbiamo la necessità di modificare l’oggetto JSON generato, possiamo servirci ad esempio, del metodo toArray (o di toJson in maniera del tutto equivalente).

class Post extends Model
{
    protected $fillable = [
        'title',
        'subtitle',
        'body',
        'published_at',
    ];

    public function toArray() {
        return [
            'id'           => $this->id,
            'title'        => $this->title,
            'subtitle'     => $this->subtitle,
            'body'         => $this->body,
            'publish_date' => $this->published_at,
            'summary'      => substr($this->body, 0, 150),
            'url'          => route('posts.show', ['id' => $this->id]),
        ];
    }
}

Ai più esperti salteranno subito all’occhio alcune delle limitazioni di questo approccio:

  • Si rischia di aggiungere codice che non è di competenza del model. Nel caso preso in esame, sia l’attributo summary, che l’attributo url, vengono calcolati sulla base di informazioni che il model layer non dovrebbe conoscere.
  • Modifiche alla base dati, si ripercuoterebbero automaticamente sulle interfacce delle nostre API, rendendone complesso il versionamento e causando potenziali problematiche ai vari client che ne fanno utilizzo.
  • In uno scenario più ampio, potremmo aver bisogno di rappresentare la stessa entità in diverse forme ovvero disporre di più metodi per serializzare la stesso model.

Abbiamo evidentemente bisogno di un livello ulteriore, che si occupi della sola trasformazione di questi dati.

Sporchiamoci un po’ le mani: un caso pratico di utilizzo delle api resource

Scarichiamo il repository di esempio e lanciamo il progetto (dopo aver scaricato tutte le dipendenze con composer):

$ git clone https://github.com/Rizzello/ic-api-resources.git --branch empty
$ cd ic-api-resources
$ composer install
$ php artisan serve

Il progetto viene fornito con un database sqlite già popolato. Il “dominio applicativo” del nostro progetto di esempio, è qualcosa di estremamente semplice:

  • L’entità Autore (Author), può essere associata a n Articoli
  • L’entità Articolo (Post), può essere associata ad un solo Autore

Nel file routes/api.php definiamo due route (d’ora in poi tutte le route saranno aggiunte unicamente in questo file) per il prelievo dei dati da db:

Route::get('v0/posts', function (Request $request) {
    return Post::with('author')->get();
});

Route::get('v0/posts/{id}', function (Request $request, $id) {
    return Post::with('author')->find($id);
});

In questo modo non abbiamo fatto altro che prelevare i dati dal database per fornirli in output senza aggiungere trasformazioni di alcun tipo (per brevità è stata rimossa la gestione degli errori). Ispezionando l’url http://localhost:8000/api/v0/posts/1 potrete ottenere un singolo oggetto Post:

{
    "id": 1,
    "title": "We will constantly strive",
    "subtitle": "Provide worldwide knowledge...",
    "body": "Fusce vitae lobortis augue...",
    "publish_date": "2017-10-27 14:40:09",
    "author_id": "1",
    ...
    "author": { ... }
}

Discorso analogo per la route http://localhost:8000/api/v0/posts, che ci fornirà un array di json object.

Procediamo quindi a creare una API Resource, utilizzando artisan, per l’entità Post, e due route che ne fanno utilizzo, e vediamo cosa succede:

php artisan make:resource Post

Il file appena creato dallo scaffolder è posizionato nella directory app/Http/Resources:

class Post extends Resource
{
    public function toArray($request) {
        return parent::toArray($request);
    }
}

Aggiungiamo quindi le due route (nel file routes/api.php), analoghe alle precedenti, che fanno utilizzo della resource appena creata:

use App\Http\Resources\Post as PostResource;

Route::get('v1/posts', function (Request $request) {
    return PostResource::collection(Post::with('author')->get());
});

Route::get('v1/posts/{id}', function (Request $request, $id) {
    return new PostResource(Post::with('author')->find($id));
});

Come potete notare una singola resource può gestire il singolo model o una sua collezione (tramite metodo statico “collection”). Il JSON generato dalla nuova route sarà in seguente:

{
    "data": {
        "id": 1,
        "title": "We will constantly strive",
        ...
    }
}

Il nostro oggetto JSON è ora “wrapped” nella proprietà “data” di un root object. Questa convenzione ci consente di veicolare una serie di informazioni, nel corpo della nostra response, come ad esempio: versione API, informazioni di carattere legale (link alla privacy policy, licensing, etc), data e ora di generazione, status, info di paginazione, etc:

{
    "data": {
        "id": 1,
        "title": "We will constantly strive",
        ...
    }
    "version": "1.0",
    "attribution": "http://www.example.com/evil-policy.html",
    "generated": "2017-10-29 09:30:00"
}

Apriamo quindi il file PostResource, formattiamo l’output della nostra response e aggiungiamo la versione delle nostre API utilizzando gli special method “toArray” (che si occupa della serializzazione della nostra entità) e “with” (che esegue l’append di informazioni aggiuntive per la singola risorsa):

class Post extends Resource
{
    public function toArray($request) {
        return [
            'id'           => $this->id,
            'title'        => $this->title,
            'subtitle'     => $this->subtitle,
            'body'         => $this->body,
            'publish_date' => $this->published_at,
            'author'       => $this->author->first_name . ' ' . 
                                  $this->author->last_name,
            'url'          => url(route('posts.show', ['id' => $this->id])),
        ];
    }
    public function with($request) {
        return [
            'version'      => '1.0',
        ];
    }
}

In fase di serializzazione, all’interno della Resource, possiamo accedere direttamente alle proprietà del model, in modo trasparente, perché la Resource è un Proxy di quest’ultimo.

Ovviamente nulla ci vieta di utilizzare le proprietà di navigazione del nostro model (nel nostro caso “author”) e di annidare le resource tra loro (purché non siano presenti riferimenti ricorsivi):

class Author extends Resource
{
    public function toArray($request) {
        return [
            'id'           => $this->id,
            'fullname'     => $this->first_name . ' ' . $this->last_name,
        ];
    }
}

class Post extends Resource
{
    public function toArray($request) {
        return [
            'id'           => $this->id,
            'title'        => $this->title,
            'subtitle'     => $this->subtitle,
            'body'         => $this->body,
            'publish_date' => $this->published_at,
            'author'       => new Author($this->author),
            'url'          => url(route('posts.show', ['id' => $this->id])),
        ];
    }
    ...
}

A questo punto abbiamo visto come generare le resource e come utilizzarle. Sebbene questo approccio risulti essere più elegante, finché il nostro dominio non varia, non possiamo notare grossi benefici. Senza dilungarci troppo, generiamo una nuova Resource, che chiameremo PostV2 e formattiamola in modo completamente diverso (supponiamo per un attimo di aver introdotto una nuova entità Tag):

class PostV2 extends Resource
{
    public function toArray($request) {
        return [
            'id'           => $this->id,
            'header'       => [
                'title'        =>$this->title, 
                'subtitle'     => $this->subtitle,
            ],
            'meta'         => [
                'publish_date' => $this->published_at,
                'tags'         => ['tag1', 'tag2', 'tag3'],
                'url'          => url(route('posts.show', ['id' => $this->id])),
            ],
            'content'      => $this->body,
            'author'       => new Author($this->author),
        ];
    }

    public function with($request) {
        return [
            'version'      => '2.0',
        ];
    }
}

Aggiungiamo quindi due route (nel file routes/api.php) che ne fanno utilizzo:

use App\Http\Resources\PostV2 as PostV2Resource;

Route::get('v2/posts', function (Request $request) {
    return PostV2Resource::collection(Post::with('author')->get());
});

Route::get('v2/posts/{id}', function (Request $request, $id) {
    return new PostV2Resource(Post::with('author')->find($id));
});

Navigando sull’url http://localhost:8000/api/v2/posts/1 possiamo notare un risultato completamente diverso dal precedente:

{
    "data": {
        "id": 1,
        "header": {
            "title": "We will constantly strive",
            "subtitle": "Provide worldwide knowledge ..."
        },
        "meta": {
            "publish_date": "2017-10-27 14:40:09",
            "url": "http://localhost:8000/posts/1"
        },
        "content": "Fusce vitae lobortis augue ...",
        "author": {
            "id": 1,
            "fullname": "John Doe"
        }
    },
    "version": "2.0"
}

Customizzare l’output di una collezione di API resource

Negli esempi precedenti, abbiamo accennato al fatto che, il metodo with, non viene elaborato sulle collezioni. Per customizzare l’output dei una collezione di entità, abbiamo bisogno di generare una ResourceCollection:

php artisan make:resource PostCollection --collection

Il parametro “–collection” serve ad indicare ad artisan che vogliamo creare una ResourceCollection e non una semplice Resource (in realtà il parametro è opzionale se il nome della nostre resource termina per “Collection”).

class PostCollection extends ResourceCollection
{
    public function toArray($request) {
        return parent::toArray($request);
    }
    public function with($request) {
        return [
            'version'      => '1.0',
        ];
    }
}

Generiamo quindi una nuova route andando, questa volta, a paginare i risultati:

use App\Http\Resources\PostCollection;

Route::get('v3/posts', function (Request $request) {
    return new PostCollection(
        Post::with('author')->paginate(env('PAGINATION', 4))
    );
});

E vediamo un po’ cosa succede:

{
    "data": [ ... ],
    "links": {
        "first": "http://localhost:8000/api/v3/posts?page=1",
        "last": "http://localhost:8000/api/v3/posts?page=2",
        "prev": null,
        "next": "http://localhost:8000/api/v3/posts?page=2"
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 2,
        "path": "http://localhost:8000/api/v3/posts",
        "per_page": "4",
        "to": 4,
        "total": 6
    },
    "version": "1.0"
}

I nostri risultati sono stati paginati e le informazioni di paginazione aggiunte nel root object, insieme al nostro attributo version. Mentre i 4 object presenti nell’array “data” sono automaticamente serializzati utilizzando, in automatico (grazie alla naming convention utilizzata), la Resource Post.

Conclusioni

Abbiamo introdotto le API Resource, perché utilizzarle e in che modo, come creare un’interfaccia, per i nostri model, che non varia con modifiche del dominio, e lasciando i nostri controller Skinny (anche se nell’articolo, per brevità, abbiamo lavorato direttamente nelle route, i principi e il codice prodotto, si adattano perfettamente alle action dei nostri controller).

A proposito di me

Paolo Rizzello

Nato e cresciuto in un'azienda di informatica, ha (inevitabilmente) intrapreso la strada dell'IT con un diploma da perito informatico prima e una laurea in ingegneria informatica poi, facendo, della sua passione, un lavoro a tutti gli effetti.
Si guadagna da vivere sviluppando web application in Laravel e ASP.NET... ma ha un oscuro passato di sviluppatore C++.

Di Paolo Rizzello

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti