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
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?