Envolver respuestas de API REST en Json con Lumen

Envolver respuestas de API REST en Json con Lumen

A la hora de desarrollar un servicio web con PHP, una opción interesante a elegir entre de los diferentes frameworks disponibles es Lumen. Se trata del “hermano pequeño” de Laravel, y dispone de la mayoría de las funcionalidades propias de éste, omitiendo las correspondientes al frontend. Es este el framework usado para este artículo.

En una aplicación REST la base es devolver representaciones (ya sea en XML, Json, u otros formatos) de objetos, o listados de éstos.

Así, por ejemplo, en una aplicación de coches, una respuesta posible sería la siguiente:

[{
   manufacturer: 'Porsche',
   model: '911',
   price: 135000,
   wiki: 'http://en.wikipedia.org/wiki/Porsche_997',
   img: '2004_Porsche_911_Carrera_type_997.jpg'
},{
   manufacturer: 'Nissan',
   model: 'GT-R',
   price: 80000,
   wiki:'http://en.wikipedia.org/wiki/Nissan_Gt-r',
   img: '250px-Nissan_GT-R.jpg'
},{
   manufacturer: 'BMW',
   model: 'M3',
   price: 60500,
   wiki:'http://en.wikipedia.org/wiki/Bmw_m3',
   img: '250px-BMW_M3_E92.jpg'
},{
   manufacturer: 'Audi',
   model: 'S5',
   price: 53000,
   wiki:'http://en.wikipedia.org/wiki/Audi_S5#Audi_S5',
   img: '250px-Audi_S5.jpg'
},{
   manufacturer: 'Audi',
   model: 'TT',
   price: 40000,
   wiki:'http://en.wikipedia.org/wiki/Audi_TT',
   img: '250px-2007_Audi_TT_Coupe.jpg'
}]

Pero hay casos en los que por diferentes razones tenemos que incluir otros datos en la respuesta.

Un ejemplo práctico de esto es las respuestas ‘paginadas’.

Si tuvieramos una llamada a una api en la que se nos devolverían cientos o miles de elementos, es posible que se quiera dividir la respuesta, de forma que se devuelva un número máximo de elementos en cada llamada.

Pero con cada respuesta, nos interesará saber también el número total de elementos existentes, el número de ‘página’ devuelto, la url para la siguiente página o para la anterior, etc.

Para esto, se usa lo que se denomina “envolturas” de resultados o en inglés “wraps”.

Existen discusiones y diferentes opiniones sobre si estos ‘meta datos’ deberían ir en el cuerpo de la respuesta, o de alguna forma en la cabecera.

No obstante, vamos a suponer que por algún tipo de requerimiento, se exige que vayan en el cuerpo.

Vamos a envolver la respuesta del ejemplo anterior de forma que quede de la siguiente manera:

{

   status: ‘success’,

   code: 200,

   data: [{
       manufacturer: 'Porsche',
       model: '911',
       price: 135000,
       wiki: 'http://en.wikipedia.org/wiki/Porsche_997',
       img: '2004_Porsche_911_Carrera_type_997.jpg'
   },{
       manufacturer: 'Nissan',
       model: 'GT-R',
       price: 80000,
       wiki:'http://en.wikipedia.org/wiki/Nissan_Gt-r',
       img: '250px-Nissan_GT-R.jpg'
   }]

}

Para que a lo largo de todos los endpoints de la api las respuestas sean todas estandar, conviene sacar esta funcionalidad para envolverlas fuera de los controladores. Por ejemplo a un Middleware en Lumen.


 Así, crearemos el archivo app/Http/Middleware/ResponseWrapper.php con el siguiente contenido:

class ResponseWrapper

{

	public function handle($request, Closure $next)

	{

	    $response = $next($request);


	        if ($response instanceof JsonResponse) {

	            if(!isset($response->getData()->status))

	            {

	                $newResponseData['status'] = 'success';

	                $newResponseData['code'] = $response->getStatusCode();

	                $newResponseData['data'] = $response->getData();

	   

	                $response->setData($newResponseData);

	            }

	    }


	    return $response;

	}

}

Lo registramos a bootstrap/app.php como middleware global para que se ejecute en todas las llamadas a la api:

$app->middleware([

‘'AppHttpMiddlewareResponseWrapper',

]);

Lo que hace este middleware es:

  1. Primero ejecuta todas las instrucciones previstas, al estar al principio la instrucción

    $response = $next($request);

si se devuelve $next($request); al final del código del middleware, el middleware se ejecutará al principio. Al estar la instrucción al principio, el middleware se ejecuta al final de la llamada.

  1. Una vez se han ejecutado todas las instrucciones correspondientes a la llamada (autenticación, llamadas a bases de datos, tratamiento de los datos, etc.) la respuesta inicial de la API (sin envoltura) estará guardada en la variable “$response”

  2. Generamos un array con los campos “status”, “”

A parte de las respuestas esperadas, también puede ser conveniente envolver los mensajes de error.

La mayor parte de los errores que se pueden dar en una aplicación (entidad no encontrada, autenticación necesaria, no se pudo conectar con servicio externo, etc.) lanzan una aplicación en PHP y en Laravel en concreto. Estas exceptiones pueden ser (aparte de la Exception nativa de PHP) de varios tipos: HttpException, NotFoundHttpException, ModelNotFoundException, AccessDeniedException, ClientException, entre muchas otras.

A la hora de homogeneizar la devolución de errores, también podemos implementar un Exception handler que recoja cada una de las excepciones lanzadas por la aplicación, y genere una respuesta adecuada para cada una de ellas, con su mensaje de error, código de estado HTTP y demás parámetros correspondientes.

Para ello, siguendo la documentación de Lumen (https://lumen.laravel.com/docs/5.4/errors#the-exception-handler) dentro de app/Exception/Handler.php implementamos el método render de la siguiente manera, definiendo las respuestas concretas para cada una de las excepciones:

class Handler extends ExceptionHandler

{

	public function render($request, Exception $e)

	{

	    $rendered = parent::render($request, $e);


	    if(env('RENDER_EXCEPTIONS',true))

	    {

	        if($e instanceOf ValidationException)                // Excepción por no pasar validación

	        {

	            $code= 400;

	            $responseData['status'] = 'fail';

	            $responseData['data'] = $e->response->getData();

	            $responseData['code'] = $code;

	        }

	        else if ( ($e instanceOf HttpException)

	                || ($e instanceOf NotFoundHttpException)

	                || ($e instanceOf ModelNotFoundException) ) // Excepción por url mal formada

	        {

	            $code = 404;

	            $responseData['status'] = 'error';

	            $responseData['code'] = $code;

	            $responseData['message'] = 'Not found';

	        }

	        else if ($e instanceOf AccessDeniedException)        // Excepción por Acceso Denegado

	        {

	            $code = 401;

	            $responseData['status'] = 'error';

	            $responseData['code'] = $code;

	            $responseData['message'] = 'Not authenticated';


	            $headers['WWW-Authenticate'] = 'Bearer';

	        }

	        else if ($e instanceOf ClientException)              //Guzzle ClientException

	        {

	            //var_dump($e);

	            $code = 503;

	            $responseData['status'] = 'error';

	            $responseData['code'] = $code;

	            $responseData['message'] = 'Error when connecting with external service';

	            $responseData['data']['message'] = $e->getMessage();


	        }

	        else if ($e instanceOf Google_Exception)

	        {

	            $code = 503;

	            $responseData['status'] = 'error';

	            $responseData['code'] = $code;

	            $responseData['message'] = 'Error when connecting with external service';

	            $responseData['data']['message'] = $e->getErrors();

	        }

	        else                                                 // Excepción desconocida

	        {

	            $code = 500;

	            $responseData['status'] = 'error';

	            $responseData['code'] = $code;

	            $responseData['message'] = 'Undefined Exception';

	            $responseData['data']['exception_type'] = get_class($e);

	        }


	        return response()->json($responseData, isset($code)? $code : 200, isset($headers) ? $headers : []);

	    }


	    return parent::render($request, $e);

	}

}

De esta forma conseguiremos que todos los errores tengan un formato de respuesta homogeneo como el siguiente ejemplo:

{

status: ‘error’,

code: 401,

message: 'Not authenticated'

}

de forma que el cliente que consume la api pueda procesar todas las respuestas de la misma forma y emitir los correspondientes mensajes de estado.

No se nos tiene que olvidar registrar este handler, en bootstrap/app.php:

$app->singleton(

IlluminateContractsDebugExceptionHandler::class,

AppExceptionsHandler::class

);

Así, tendremos todas las respuestas de la API envueltas de una forma uniforme, que aplicaciones externas puedan interpretar.


Comentarios

Sin comentarios
Ha habido un error en el envío
Comentario enviado. Será revisado por la moderación antes de ser publicado.

Deja tu comentario

Tu nombre:
Tu email:
Tu comentario: