Respuestas

1. Generalizar respuestas de la API

2. Transformaciones por seguridad y compatibilidad

Permiten transformar respuestas que retorna la API RESTful, cambiar nombre de atributos, tipos de datos, etc...

Son muy útiles, por ejemplo, si en algún momento cambia el nombre de un atributo en la DB , se le puede aplicar una transformación a la respuesta para que el resultado final obtenido por el cliente sea el mismo

En PHP las transformaciones se pueden realizar con un paquete llamado Fractal, sin embargo su uso es complejo, por tanto haremos uso de un paquete de laravel que facilita el uso de Fractal.

Info adicional

http://fractal.thephpleague.com/

  • Descargar paquete de fractal para laravel

composer require spatie/laravel-fractal
  • Registrar el service provider en config/app.php

/*
* Package Service Providers...
*/
...
Spatie\Fractal\FractalServiceProvider::class,

Si escribimos en la consola php artisan veremos que existe un nuevo comando make:transformer, este utilizaremos para crear los transformadores para cada modelo.

  • Crear transformadores para cada modelo

Los transformadores se guardan en la carpeta app/Transformers

php artisan make:transformer ProductTransformer
  • Personalizar transformadores para cada modelo

use App\Product;

public function transform(Product $product)
{
    return [
        'identifier' => (int) $product->id,
        'name' => (string) $product->name,
        'details' => (string) $product->description,
        'available' => (int) $product->quantity,
        'status' => (string) $product->status,
        'image' => url("img/{$product->image}"),
        'seller' => (int) $product->seller_id,
        'createdAt' => (string) $product->created_at,
        'updatedAt' => (string) $product->updated_at,
        'deletedAt' => isset($product->deleted_at) ? (string) $product->deleted_at : null,
    ];
}
  • Relacionar cada modelo con su transformación

Para esto añadir el atributo $transformer en cada modelo, por ejemplo en Product

use App\Transformers\ProductTransformer;

// Transformer
public $transformer = ProductTransformer::class;
  • Retornar respuestas transformadas

Utilizar métodos de transformación en trait ApiResponser para retornar valores en showOne y showAll

protected function transformData($data, $transformer)
{
    // create transformer
    $transformation = fractal($data, new $transformer);

    // convert transformation to array (more comprehensible by laravel)
    return $transformation->toArray();
}
protected function showAll(Collection $collection, $code = 200)
{
    // check if collection is empty
    if($collection->isEmpty())
    {
     return $this->successResponse(['data' => $collection], $code);
    }

    $transformer = $collection->first()->transformer;

    $collection = $this->transformData($collection, $transformer); // fractal adds by default 'data', is not needed to specify it

    return $this->successResponse($collection, $code);
}

protected function showOne(Model $instance, $code = 200)
{
    $transformer = $instance->transformer;
    $instance = $this->transformData($instance, $transformer);

    return $this->successResponse($instance, $code);
}

Nota: al retornar los datos transformados no es necesario especificar la raíz data, ['data' => $collection], porque fractal lo añade automáticamente.

3. Validaciones y Transformaciones

El transformador del modelo Category tiene la estructura

'identifier' => (int) $category->id,
'title' => (string) $category->name,
'details' => (string) $category->description,
...

Al hacer un post, se presentan 2 problemas:

  • Problema 1

Al enviar los datos en el formulario con los valores de la transformación, estos no son considerados válidos

  • Crear e implementar middleware para solucionar problema

Para solucionar el problema interceptar las peticiones recibidas, usando un middleware. Este solo se aplicará a peticiones POST PUT PATCH

php artisan make:middleware TransformInput

Registrar en app/Http/Kernel.php

protected $routeMiddleware = [
    ...
    'transform.input' => \App\Http\Middleware\TransformInput::class,
];

Personalizar middleware

class TransformInput
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $transformer)
    {
        $transformedInputs = [];

        // foreach input get its original attribute
        foreach($request->request->all() as $input => $value)
        {
            $transformedInputs[$transformer::originalAttribute($input)] = $value;
        }

        // replace inputs by its original attribute
        $request->replace($transformedInputs);

        return $next($request);
    }
}

Al crear una nueva categoría, se considera válido el campo title

Usar middleware en controladores con store y update, especificando que será necesario solo en esos métodos y enviándole como parámetro la estructura del transformador del modelo involucrado

use App\Transformers\UserTransformer;

class UserController extends ApiController
{
    public function __construct()
    {
        $this->middleware('transform.input:' . UserTransformer::class)->only(['store', 'update']);
    }

    ...
}
  • Problema 2

Los nombres de campos retornados por la validaciones no coinciden con los establecidos en los transformadores. Como se refleja anteriormente, se tienen name y description que son los valores originales del modelo y no title o details que se especificaron con el transformador.

  • Solucionar problema

Al obtener los errores de validación es necesario, para cada campo original del modelo, obtener su valor transformado mostrárselo al usuario.

Así, se debe crear en cada transformación un método (transformedAttribute) que retorne el correspondiente valor transformado para un atributo del modelo:

class CategoryTransformer extends TransformerAbstract
{
    ...

    public static function originalAttribute($index)
    {
        $attributes = [
            'identifier' => 'id',
            'title' => 'name',
            'details' => 'description',
            'createdAt' => 'created_at',
            'updatedAt' => 'updated_at',
            'deletedAt' => 'deleted_at',
        ];

        return isset($attributes[$index]) ? $attributes[$index] : null;
    }

    public static function transformedAttribute($index)
    {
        $attributes = [
            'id' => 'identifier',
            'name' => 'title',
            'description' => 'details',
            'created_at' => 'createdAt',
            'updated_at' => 'updatedAt',
            'deleted_at' => 'deletedAt'
        ];

        return isset($attributes[$index]) ? $attributes[$index] : null;
    }
}

Añadir nuevas validaciones al middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Validation\ValidationException;

class TransformInput
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $transformer)
    {
        $transformedInputs = [];

        // foreach input get its original attribute
        foreach($request->request->all() as $input => $value)
        {
            $transformedInputs[$transformer::originalAttribute($input)] = $value;
        }

        // replace inputs by its original attribute
        $request->replace($transformedInputs);

        $response = $next($request);

        // only handle responses that are errors and instances of ValidationException
        if (isset($response->exception) && $response->exception instanceof ValidationException) {
            $data = $response->getData();

            $transformedErrors = [];

            // get errors fields on response and replace them by the transformed attribute
            foreach ($data->error as $field => $error) {
                $transformedField = $transformer::transformedAttribute($field);
                $transformedErrors[$transformedField] = str_replace($field, $transformedField, $error);
            }

            $data->error = $transformedErrors;

            $response->setData($data);
        }

        return $response;
    }
}

4. Ordenar y filtrar según parámetros de la URL

Estos mecanismos de filtrado y ordenamiento se deben aplicar sobre los datos sin transformar, porque operan sobre colecciones y no sobre instancias de fractal (retornadas por la transformación)

  • Ordenar resultados usando cualquier atributo

https://elbauldelprogramador.com/buenas-practicas-para-el-diseno-de-una-api-restful-pragmatica/#ordenación

Problema:

No existe hasta ahora una manera de saber cuál atributo del modelo corresponde a cada atributo transformado, para esto es necesario crear un método estático (no es necesario instanciar la clase para acceder a él) que realice un mapeado dentro de la transformación

class ProductTransformer extends TransformerAbstract
{
    /**
     * A Fractal transformer.
     *
     * @return array
     */
    public function transform(Product $product)
    {
        return [
            'identifier' => (int) $product->id,
            'name' => (string) $product->name,
            'details' => (string) $product->description,
            'available' => (int) $product->quantity,
            'status' => (string) $product->status,
            'image' => url("img/{$product->image}"),
            'seller' => (int) $product->seller_id,
            'createdAt' => (string) $product->created_at,
            'updatedAt' => (string) $product->updated_at,
            'deletedAt' => isset($product->deleted_at) ? (string) $product->deleted_at : null,
        ];
    }

    public static function originalAttribute($index)
    {
        $attributes = [
            'identifier' => 'id',
            'name' => 'name',
            'details' => 'description',
            'available' => 'quantity',
            'status' => 'status',
            'image' => 'image',
            'seller' => 'seller_id',
            'createdAt' => 'created_at',
            'updatedAt' => 'updated_at',
            'deletedAt' => 'deleted_at',
        ];

        return isset($attributes[$index]) ? $attributes[$index] : null;
    }
}

Dentro del ApiResponser, en sortData utilizar el método originalAttributes para obtener a cuál valor del modelo corresponde el valor recibido como parámetro en la url

protected function sortData(Collection $collection, $transformer)
{
    // if request has sort_by attribute
    if (request()->has('sort_by'))
    {
        // order by transformed attributes, not original ones
        // there might be a way to determine the transformed attribute to which the actual model attribute corresponds
        $attribute = $transformer::originalAttribute(request()->sort_by);

        $collection = $collection->sortBy->{$attribute};
    }

    return $collection;
}
protected function showAll(Collection $collection, $code = 200)
{
   // check if collection is empty
   if($collection->isEmpty())
   {
        return $this->successResponse(['data' => $collection], $code);
    }

    $transformer = $collection->first()->transformer;

    // sort by --> executed before transformer because that function returns a fractal instance not a collection
    $collection = $this->sortData($collection, $transformer);

    $collection = $this->transformData($collection, $transformer); // fractal adds by default 'data', is not needed to specify it

    return $this->successResponse($collection, $code);
}
  • Filtrar resultados según múltiples parámetros

protected function filterData(Collection $collection, $transformer)
{
    // get list of parameters from url
    foreach(request()->query() as $query => $value)
    {
        // get model attribute
        $attribute = $transformer::originalAttribute($query);

        if(isset($attribute, $value)) // if both attribute and value are !null
        {
            $collection = $collection->where($attribute, $value); // check by equal
        }
    }

    return $collection;
}
protected function showAll(Collection $collection, $code = 200)
{
    // check if collection is empty
    if($collection->isEmpty())
    {
     return $this->successResponse(['data' => $collection], $code);
    }

    $transformer = $collection->first()->transformer;

    // filter before sorting
    $collection = $this->filterData($collection, $transformer);

    // sort by --> executed before transformer because that function returns a fractal instance not a collection
    $collection = $this->sortData($collection, $transformer);

    $collection = $this->transformData($collection, $transformer); // fractal adds by default 'data', is not needed to specify it

    return $this->successResponse($collection, $code);
}
  • Paginar resultados

La paginación permite dividir los resultados en segmentos, es especialmente importante cuando se tiene un número grande de datos.

Eloquent cuenta con el método paginate, que opera sobre colecciones de la base de datos y retorna los resultados en segmentos (páginas), sin embargo esta solución no es completamente adecuada para nuestro caso, teniendo cuenta que no funciona para colecciones a las que se les apliquen las operaciones pluck, unique, etc...

use Illuminate\Pagination\LengthAwarePaginator;

protected function paginate(Collection $collection)
{
    // get current page -> to resolve which collection segment will be shown
    $page = LengthAwarePaginator::resolveCurrentPage();

    $perPage = 15;

    $results = $collection->slice(($page - 1) * $perPage, $perPage)->values();

    $paginated = new LengthAwarePaginator($results, $collection->count(), $perPage, $page, [
        'path' => LengthAwarePaginator::resolveCurrentPath(),
    ]);

    // the generation of the path, automatically removes the other parameters of the url
    // It must be asked to the paginator to add the list of parameters of the request (not including the page)
    $paginated->appends(request()->all());

    return $paginated;
}
protected function showAll(Collection $collection, $code = 200)
{
    // check if collection is empty
    if($collection->isEmpty())
    {
     return $this->successResponse(['data' => $collection], $code);
    }

    $transformer = $collection->first()->transformer;

    // filter before sorting
    $collection = $this->filterData($collection, $transformer);

    // sort by --> executed before transformer because that function returns a fractal instance not a collection
    $collection = $this->sortData($collection, $transformer);

    $collection = $this->paginate($collection);

    $collection = $this->transformData($collection, $transformer); // fractal adds by default 'data', is not needed to specify it

    return $this->successResponse($collection, $code);
}

En caso de ordenar los resultados,

Permitir tamaño de página personalizado

use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Validator;

protected function paginate(Collection $collection)
{
    $rules = [
        'per_page' => 'integer|min:2|max:50'
    ];

    Validator::validate(request()->all(), $rules);

    $perPage = 15;
    if(request()->has('per_page'))
    {
        $perPage = (int) request()->per_page;
    }

    // get current page -> to resolve which collection segment will be shown
    $page = LengthAwarePaginator::resolveCurrentPage();

    ...
}

Last updated

Was this helpful?