# 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.&#x20;

**Info adicional**

<http://fractal.thephpleague.com/>

* **Descargar paquete de fractal para laravel**

```php
composer require spatie/laravel-fractal
```

* **Registrar el service provider en config/app.php**

```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**

```php
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

```php
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

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

    // convert transformation to array (more comprehensible by laravel)
    return $transformation->toArray();
}
```

```php
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

```php
'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

![](/files/-Ld9leL8XT6gszhjGQ77)

* **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
php artisan make:middleware TransformInput
```

Registrar en app/Http/Kernel.php

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

Personalizar middleware

```php
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**

![](/files/-Ld9leLATQr03rCcnjHu)

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

```php
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
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
<?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;
    }
}
```

## ![](/files/-Ld9leLEU6PrIsstR6mF)

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

```php
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

```php
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;
}
```

```php
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);
}
```

![](/files/-Ld9leLIkw6-twk2ZCvb)

* **Filtrar resultados según múltiples parámetros**

```php
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;
}
```

```php
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);
}
```

![](/files/-Ld9leLK5nkwpO7rgc4x)

* **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...

```php
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;
}
```

```php
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);
}
```

![](/files/-Ld9leLMfe6RNqdUYWnn)

En caso de ordenar los resultados,

![](/files/-Ld9leLOlsTCZc7R3Sra)

### **Permitir tamaño de página personalizado**

```php
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();

    ...
}
```

![](/files/-Ld9leLQ8DLaKqh_0TzZ)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://laura-schiatti-siso.gitbook.io/api-restful-con-laravel-5-4/transformaciones.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
