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
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
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:
<?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
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
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();
...
}