Rest Entity Manager
Un gestor de entidades rest orientado al mapeo de datos con webservices.
El principal problema que te encuentras cuando usas librerias orientadas a crear un WebService
es que todas están planteadas para flujos estandar en los que conectas directamente con tu BBDD.
Muchas APIS modernas basadas en la web, funcionan de intermediarias con otras APIS corporativas
y no corporativas. Asi surgio "Rest Entity Manager"., (*1)
Funcionamiento basico de la libreria
Esta libreria consta de dos serializadores estandar. El primero gestiona la transformación de
datos entre las entidades y los diferentes servicios rest. El segundo serializador se usa para
codificar y decodificar los contenidos que envian y reciben los clientes y asi popular las entidades
de tu API., (*2)
Internamente el gestor de entidades lee la configuración de las diferentes conexiones y realiza las peticiones a los
diferentes webservices., (*3)
Servicios
-
alc_rest_entity_manager.handler: Es el gestor de entidades rest. Se encarga de leer la configuración
de las diferentes conexiones y cargarla en el el cliente rest.
-
alc_rest_entity_manager.jms_event_subscriber: Se encarga de leer la configuracion de las entidades y
configurar el mapeo hacia los diferentes WS rest.
-
alc_rest_entity_manager.serializer: Es el serializador encargado de mapear la información que envian y reciben los clientes
en las diferentes entidades de la API.
-
alc_rest_entity_manager.logger: Servicio encargado de monitorizar y escribir los logs de las peticiones rest
del manager.
-
alc_rest_entity_manager.metadata_class_reader: Se encarga de leer la metainformación de las clases necesaria pra realizar los mapeos.
-
alc_rest_entity_manager.parameters_procesor: Se encarga de procesar los parametros de filtrado segun la configuración definida en la conexion rest.
-
alc_rest_request_parameters_name_override: Sobreescribe los parametros de la URL en el servicio "request" respetando la notacion de puntos. Ej: "poliza.id"
Consideraciones previas
Este es un bundle pensado para funcionar junto con "FOSRestBundle", pero tambien se puede usar sin el., (*4)
Instalación
Paso 1: Descargar el bundle
Abre la consola de comandos, entra en el directorio de tu proyecto y ejecuta el siguiente comando para descargar la
ultima version estable de este bundle., (*5)
$ composer require alberto-leon-crespo/rest-entity-manager
, (*6)
Paso 2: Activar el bundle
Activa el bundle añadiendo la siguiente linea al fichero app/AppKernel.php
, (*7)
<?php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new ALC\RestEntityManager\ALCRestEntityManagerBundle(),
);
// ...
}
}
Configuracion
Para poder usar las entidadedes orientadas a rest es necesario configurar previamente una conexion rest., (*8)
# app/config/config.yml
alc_rest_entity_manager:
default_manager: default # El nombre de la conexion que usara el manager por defecto.
managers:
default:
name: 'default' # El nombre de la conexion
host: 'https://jsonplaceholder.typicode.com/' # URL base del servicio rest que se va a consultar
session_timeout: 7200 # Tiempo de expiración de la sesion que hay entre el cliente rest y el webservice de destino
avanced:
filtering:
ignored_parameters: [_fields,_sort,_page,_limit] #Parametros ignorados del filtrado, no se consideran parametros de filtrado
parameters_map:
maps:
- { origin: _fields, destination: selection } # El campo de origen se mapea al WS como el campo destino indicado
- { origin: _page, destination: _page } # El campo de origen se mapea al WS como el campo destino indicado
- { origin: _limit, destination: _limit } # El campo de origen se mapea al WS como el campo destino indicado
- { origin: _sort, interceptor: ALC\WebServiceBundle\Interceptors\SortParametersInterceptor::parseSortFields } # El campo de origen se transforma por medio de un ParameterInterceptor
custom_params: # Bloque de parametros de configuración personalizables
secret_api_key: example1234
events_handlers: # Bloque de eventos sobre peticiones.
tokens_inject_handler: # Nombre del manejador del evento
event: before # Evento a interceptar
interceptor: 'ALC\WebServiceBundle\Interceptors\TokenInjector::injectToken' # Interceptor de las peticiones
Si quieres usar el serializador junto con FOSRestBundle es necesario que sobreescribas el serializador estandar con la siguiente configuración., (*9)
# app/config/config.yml
fos_rest:
service:
serializer: alc_rest_entity_manager.serializer
Anotaciones de configuración para las entidades.
-
ALC\RestEntityManager\Annotations\Resource:, (*10)
Indica el recurso rest al que accedera la entidad. En este caso "users", (*11)
-
ALC\RestEntityManager\Annotations\Repository:, (*12)
Indica el repositorio asociado a la entidad., (*13)
-
ALC\RestEntityManager\Annotations\Id:, (*14)
Indica que la propiedad precedida por el comentario se trata del identificador unico del recurso., (*15)
-
ALC\RestEntityManager\Annotations\Field:, (*16)
-
ALC\RestEntityManager\Annotations\Headers:, (*20)
Permite especificar un array de cabeceras http y los valores que se aplicaran a las solicitudes rest de la entidad., (*21)
<?php
namespace ALC\WebServiceBundle\Entity\Users;
use ALC\RestEntityManager\Annotations as Rest;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @Rest\Resource("users")
* @Rest\Headers({"content-type": "application/json","cache-control": "no-cache"})
* @Rest\Repository("ALC\WebServiceBundle\Entity\Users\UsersRepository")
*/
class Users
{
/**
* @Rest\Id()
* @Rest\Field(target="id",type="integer")
*/
private $idUsuario;
/**
* @Rest\Field(target="name",type="string")
* @Assert\NotNull()
* @Assert\NotBlank()
*/
private $nombre;
\\ Some class properties and methods
\\ ....
}
Acciones generales del manager rest
Buscar por id
Equivalente a GET /users/:id, (*22)
- id: identificador del recurso que se desea consultar.
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$objUsersRepository = $this
->get('alc_rest_entity_manager.handler')
->getManager('default')
->getRepository('AppBundle:Users\Users');
$arrResponse = $objUsersRespository->find( $userId, 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
Recuperar todos los registros de un listado
Equivalente a GET /users, (*23)
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$objUsersRepository = $this
->get('alc_rest_entity_manager.handler')
->getManager('default')
->getRepository('AppBundle:Users\Users');
$arrResponse = $objUsersRespository->findAll( 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
Recuperar un listado filtrado
Equivalente a GET /users?nombre=Alberto, (*24)
- filters: filtros a aplicar al listado.
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$arrFilters = array(
'nombre' => 'Jhon'
);
$objUsersRepository = $this
->get('alc_rest_entity_manager.handler')
->getManager('default')
->getRepository('AppBundle:Users\Users');
$arrResponse = $objUsersRespository->findBy( $arrFilters, 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
Recuperar el primer registro de un listado filtrado
Equivalente a GET /users?nombre=Alberto, (*25)
- filters: filtros a aplicar al listado.
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$arrFilters = array(
'nombre' => 'Jhon'
);
$objUsersRepository = $this
->get('alc_rest_entity_manager.handler')
->getManager('default')
->getRepository('AppBundle:Users\Users');
$arrResponse = $objUsersRespository->findOneBy( $arrFilters, 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
Guardar los cambios
Equivalente a POST /users o PUT /users, (*26)
Si el objeto de entidad tiene un valor asociado en el campo marcado como id, realizara un PUT, en caso contrario realizara un POST, (*27)
- object: instancia de la entidad que se quiere persistir.
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$objUser = new \AppBundle\Users();
$objUser->setNombre("Jhon");
$objUser->setApellido("Doe");
$em = $this
->get('alc_rest_entity_manager.handler')
->getManager('default');
$arrResponse = $em->persist( $objUser, 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
Comprobar si existe un registro, si existe lo actualiza, en caso contrario lo crea.
Equivalente a POST /users o PUT /users, (*28)
Si el objeto de entidad tiene un valor asociado en el campo marcado como id y ademas ese id existe, realizara un PUT, en caso contrario realizara un POST, (*29)
- object: instancia de la entidad que se quiere persistir.
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$objUser = new \AppBundle\Users();
$objUser->setNombre("Jhon");
$objUser->setApellido("Doe");
$em = $this
->get('alc_rest_entity_manager.handler')
->getManager('default');
$arrResponse = $em->merge( $objUser, 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
- object: instancia de la entidad que se quiere refrescar.
- format: formato de salida de la información.
- type: tipo de dato de salida si se indico en format
object
.
<?php
$objUser = new \AppBundle\Users();
$objUser->setIdUsuario(2);
$em = $this
->get('alc_rest_entity_manager.handler')
->getManager('default');
$arrResponse = $em->refresh( $objUser, 'object', 'ALC\\WebServiceBundle\\Entity\\Users\\Users' );
Acciones personalizadas del manager rest
Cuando por diferentes circustancias cualquiera de estas acciones estandar no funcional para tu caso de uso,
puedes realizar una llamada totalmente personalizada por medio de un repositorio o realizando la acción directamente en el controlador
como si se tratara de un cliente rest normal., (*30)
Con un repositorio
Es importante definir la anotacion "Repository" en la entidad rest., (*31)
<?php
// Repository
namespace AppBundle\Entity\Users;
use ALC\RestEntityManager\RestRepository;
class UsersRepository extends RestRepository
{
public function listadoUsuariosWithVocalA(){
$response = $this->get( 'users', array() );
$arrUsers = $this->serializer->deserialize( $response->getBody()->getContents(), 'array<ALC\WebServiceBundle\Entity\Users\Users>', 'json' );
foreach( $arrUsers as $key => $objUser ){
if( mb_strpos( $objUser->getNombre(), 'a', null, 'utf8' ) === false ){
unset( $arrUsers[$key] );
}
}
return array_values( $arrUsers );
}
}
// Controller
<?php
namespace ALC\WebServiceBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
class UsersController extends FOSRestController
{
public function getUsersAction(Request $objRequest){
$objEntityManager = $this->get('alc_rest_entity_manager.handler')->getManager();
/**
* @var $objUsersRespository \ALC\RestEntityManager\Services\RestEntityHandler\RestEntityHandler|\ALC\WebServiceBundle\Entity\Users\UsersRepository
*/
$objUsersRespository = $objEntityManager->getRepository('ALCWebServiceBundle:Users\Users');
$arrUsersWithAVocal = $objEntityManager->listadoUsuariosWithVocalA();
// If use FOSRestBundle
return $arrUsersWithAVocal;
// If dont use FOSRestBundle
$strUsersSerialized = $this->get('alc_rest_entity_manager.serializer')->serialize( $arrUsersWithAVocal, 'json' );
$objResponse = new Response( $strUsersSerialized, 200, ['Content-Type'=>'application/json'] );
return $objResponse;
Opciones avanzadas de filtrado
En ocasiones, los parametros que pasamos a la URL no son parametros de filtrado (ordenacion, paginado, etc..)., (*32)
Para ello solo hay que indicar que parametros seran ignorados del filtrado y cual es su equivalencia en el WS de destino., (*33)
Equivalencia directa
En este caso la equivalencia con el WS de destino es directa entre el campo de origen y de destino, (*34)
# app/config/config.yml
...
name: 'default'
host: 'https://jsonplaceholder.typicode.com/'
session_timeout: 7200
avanced:
filtering:
ignored_parameters: [_fields,_sort,_page,_limit] # Parametros especiales que deben de ser ignorados en los filtrados.
parameters_map: # Mapeo de parametros especiales origen - destino
maps:
- { origin: page, destination: _page } # Campor de origen page, campo de destino _page
- { origin: limit, destination: _limit } # Campor de origen limit, campo de destino _limit
De esta manera si la URL introducida en tu WS es {{URL_BASE}}/usuarios?page=1&limit=5 se solicitaria de la siguiente manera al WS de destino {{URL_BASE}}/users?_page=1&_limit=5, (*35)
Equivalencia indirecta
Puedes encontrar algunos casos en los que la equivalencia con un parametro especial de destino, no es directa, es decir, un parametro puede equivaler a dos como en el siguiente ejemplo:, (*36)
Origen |
Destino |
sort=+id |
_sort=id&order=asc |
En estos casos puedes crear un interceptor de parametros de la siguiente manera:, (*37)
<?php
namespace ALC\WebServiceBundle\Interceptors;
use ALC\RestEntityManager\ParameterInterceptor;
class SortParametersInterceptor extends ParameterInterceptor
{
public function parseSortFields($value)
{
$firstCharacter = $value[0];
$fieldName = substr($value, 1);
$order = null;
if ($firstCharacter === '-') {
$order = 'desc';
} elseif ($firstCharacter === '+' || $firstCharacter === ' ') {
$order = 'asc';
}
// Busca las equivalencias entre los parametros de entrada de tu WS y el WS de destino
$arrFinalParams = $this->getMetadataClassReader()->matchEntityFieldsWithResourcesFieldsRecursive( [ $fieldName => $order ] );
$arrayMatch = array();
foreach( $arrFinalParams as $campoOrdenar => $metodoOrdenacion ){
$arrayMatch['_sort'] = $campoOrdenar;
$arrayMatch['_order'] = $metodoOrdenacion;
};
return $arrayMatch;
}
}
Para darlo de alta solo tienes que dar de alta la siguiente configuración en la sección "parameters_map":, (*38)
# app/config/config.yml
...
name: 'default'
host: 'https://jsonplaceholder.typicode.com/'
session_timeout: 7200
avanced:
filtering:
ignored_parameters: [_fields,_sort,_page,_limit] # Parametros especiales que deben de ser ignorados en los filtrados.
parameters_map: # Mapeo de parametros especiales origen - destino
maps:
- { origin: _sort, interceptor: ALC\WebServiceBundle\Interceptors\SortParametersInterceptor::parseSortFields }
Interceptores de eventos
Si fuera necesario interceptar un evento de la petición para injectar el token de una api en las cabeceras o alguna otra acción similar, solo tienes que crear un interceptor de solicitudes de la siguiente manera:, (*39)
<?php
namespace ALC\WebServiceBundle\Interceptors;
use ALC\RestEntityManager\Interceptors\RequestInterceptor;
use GuzzleHttp\Event\BeforeEvent;
// La clase debe de extender al objecto ALC\RestEntityManager\Interceptors\RequestInterceptor
class TokenInjector extends RequestInterceptor
{
public function injectToken(BeforeEvent $event, array $arrManagerConfig){
$apiKey = $arrManagerConfig['custom_params']['secret_api_key'];
$event->getRequest()->setHeader('X-Api-Key', $apiKey);
return $event;
}
}
Una vez creado el interceptor, debes de darlo de alta con la siguiente configuración:, (*40)
# app/config/config.yml
...
name: 'default'
host: 'https://jsonplaceholder.typicode.com/'
session_timeout: 7200
avanced:
...
events_handlers:
tokens_inject_handler:
event: before
interceptor: 'ALC\WebServiceBundle\Interceptors\TokenInjector::injectToken'
Debes de indicar que evento quieres interceptar y la clase y el metodo que interceptaran la solicitud., (*41)
Los eventos soportados que se pueden interceptar son los mismos que acepta Guzzle Http Client., (*42)
Ejemplos
Para ver mas ejemplos puedes consultar el bundle de ejemplo ALC\WebServiceBundle, (*43)