Laravel/OData V1
+
Protocolo de datos Abiertos para Laravel
Laravel/OData es un paquete que implementa el protocolo de datos abierto en Laravel 5.8 o superior.
OData es la mejor forma de implementar API REST en las aplicaciones, es usado por Microsoft en su lenguaje de programación C#. define un conjunto de mejores practicas a la hora de construir una API para que los desarroladores se concentren en la lógica comercial y así poder olvidarse de las preocupaciones por códigos de estado HTTP, convenciones de url, manejo de consultas y demas. https://www.odata.org/
Este paquete expone una serie de funciones usadas por OData pero adaptadas a la metodologia que usa laravel, para exponer modelos, usar filtros , manejar consultas y demas usando Eloquent.
Las ventajas que se obtienen al usar un protocolo de datos abiertos a la hora de contruir API Rest es que este agiliza el desarrolo en el backend al evitar el uso de consultas complejas y super anidadas en los controladores, ademas de que esto mantiene limpio los controladores de logica super extensa que daña las buenas practicas al violar el principio de que los modelos deben mantener la logica de negocio y los controladores solo deben devolver datos.
OData te permite exponer un modelo atravez de un controlador y de esta manera te olvidas de todas las posibles consultas que pueden salir apartir de ese modelo, ya que es posible solicitarle a la API que filtre los datos expuestos por el modelo por uno o varios campos. usando condicionales and u or de está manera se evita tener que programar cada una de las funciones que expone un sistema que pueden llegar a ser muchas.
tome como ejemplo un modelo de usuarios App\User este expone las siguientes consultas en su sistema:
○ Lista de usuarios.
○ Lista de usuarios que pertenecen a un pais.
○ Lista de usuarios que pertenecen a una ciudad.
○ Lista de usuarios que se registrarón este mes.
○ Lista de usuarios que pertenecen a cierto rol.
entre otras ...
normalmente usted puede pensar en crear un Endpoint por cada consulta, o filtrar manualmente los usuarios desde una unica ruta basandose en los diferentes parámetros que se le envían a travez de la url.
uno de los problemas que se tienen al hacer esto es que usted mismo deberá programar la logica para cada consulta ademas de que si tiene un número bastante amplio de consultas solo para el modelo de usuarios. tratar de poner toda la logica en un solo controlador no escala y dañará las practicas de su código al asignar logica demasiado extensa a un controlador.
Si decide optar por la solución de poner la lógica de cada consulta en una ruta diferente esto solucionará el problema de las malas practicas en sus controladores, pero de igual forma si tiene muchas consultas tendrá un número grande de rutas solo para consultar un solo modelo, en este caso el App\User.
Con el OData solo tendrá que exponer su modelo App\User o cualquier otro desde un unico controlador y la aplicación o las aplicaciones que consumen está api pueden solicitarle diferentes consultas al modelo de usuarios usando unicamente parámetros por url. de esto se trata el OData, de una API Rest que solamente expone recursos en este caso Modelos y de está manera los desarrolladores solo tienen que preocuparse por consumir los recursos y construir su aplicación en el front.
Ademas el OData se encarga de otros procesos comunes en las API, como paginación, alinemiento de datos, exportación de datos, relationships, consultas multitabla entre otros ...
Para instalar el paquete puede hacerlo usando composer ejecutando el siguiente comando desde su terminal:
composer require apilaravelodata/odata
una vez instalado el paquete asegurese de tener migradas sus tablas en la base de datos:
php artisan migrate
una vez que ya tenga migradas las tablas en la base de datos, el paquete de OData necesitará mapear cada una de sus tablas y almacenar cada uno de los campos en cache, esto se hace para tener un Esquema de la base de datos en cache y evitar realizar consultas a la base de datos para verificar si existen campos que se solictan por la url, de está manera el OData ya tiene un Esquema en cache y puede verificar que campos son validos y cuales no los son usando procesos desde memoria. para crear el Esquema de la base de datos en cache ejecute el siguiente comando:
php artisan odata:config
Tenga en cuenta de que cada vez que modifique algúna de sus tablas agregando uno o mas campos, deberá ejecutar este comando para actualizar la cache del OData.
El siguiente paso que debe hacer es configurar el odata para poder exponerlo desde sus controladores, para esto se recomienda usar un ApiController controlador para su api donde incluya el uso del OData. por ejemplo puede crear un app/Http/Controllers/ApiController.php con el siguiente contenido:
namespace
App\Http\Controllers;
use
ODataResponse;
class
ApiController
extends
Controller
{
use
ODataResponse;
}
Como puede darse cuenta se trata de una simple clase que hereda a Controller y esta implementa el trait de ODataResponse que se encarga de cargar todos los metodos de OData dentro de la clase.
Una vez ya tenga la clase con los metodos del OData cargados, lo siguiente que debe hacer es agregar esta clase a sus controladores para empezar a usar los metodos expuestos por el OData. para el ejemplo usaremos un app/Http/Controllers/UserController.php controlador con el siguiente contenido:
namespace
App\Http\Controllers;
use App\Http\Controllers\
ApiController;
class
UserController
extends
ApiController
{
}
Ahora simplemente dentro del controlador creamos un metodo index que retorna un odataJsonResponse basado en un modelo de Eloquent.
namespace
App\Http\Controllers;
use App\Models\
User;
use App\Http\Controllers\
ApiController;
class
UserController
extends
ApiController
{
public function index
( )
{
return
$this->odataJsonResponse(
new User( ) );
}
}
Por ultimo es importante que establesca la propiedad table dentro de su modelo como public. esta propiedad la usa el OData para identificar la tabla a la cual hace referencia el modelo.
class User extends Model
{
public $table = "users";
}
Listo, esto es lo que necesitamos para implementar el OData en una aplicación de laravel. lo siguiente que debe hacer es crear una ruta que apunte al metodo index de tu controlador y una vez realices una solicitud HTTP a esta ruta el OData ya se encargará de generar la estructura de la respuesta en json, el código de estado HTTP junto con la colección de datos. un ejemplo de respuesta sería esta:
{
"data" : [
"data" : [
{
"id" : 1,
"name" : "example user"
},
{
"id" : 2,
}
],
"length" :
2
],
"code" :
200
}
Esta es la estructura que por defecto se devolverá para serializer las respuestas de la API, si desea modificarla puede obtener el resultado que devuelve el odataJsonResponse y luego armar un array con base en los valores que obtuvo. y de está manera puede agregar propiedades adicionales que necesite retornar en cada una de las repuestas de su API.
Otra opción que tiene es usar el metodo odataModelCollection que devuelve la coleción de datos del modelo, igualmente esta coleción toma los filtros que se le indiquen a la API por la url. esto lo puede usar para personalizar la respuesta de su api pero mantienendo el uso del OData en su modelo. también puede agregarle a su respuesta el odataProperties metodo que se encarga de devolver un array con los valores de la respuesta como: código de estado HTTP, cantidad de datos encontrados en la respuesta y demas.
namespace
App\Http\Controllers;
use App\Models\
User;
use App\Http\Controllers\
ApiController;
class
UserController
extends
ApiController
{
public function index
( )
{
return [
"collection" =>
$this->odataModelCollection(
new User( ) ),
"api_info" =>
$this->odataProperties(
new User( ) ),
];
}
}
Los transformadores se usan para estructurar cada uno de los objetos que se devuelven en las colecciónes de cada una de las respuestas de la API, en el caso del modelo de usuarios el OData por defecto devolverá la colección que devuelve laravel en sus colecciones por ejemplo al llamar un User::all().
otra caracteristica que tiene el OData es que por defecto devolverá la coleción completa sin ningún tipo de filtro ni paginación. esto obviamente se puede solicitar desde la url, pero el punto aquí es que devolver todos los datos del modelo puede no ser lo que usted tiene planeado. por ejemplo no queremos que la contraseña del usuario se vea en la respuesta de la API, o puede que necesitemos agregar otras propiedades mas al modelo con información adicional que no necesariamente debe estar almacenada en la base de datos.
para esto se puede usar un transformador personalizado desde el módelo, lo unico que tiene que hacer es declarar un transform metodo dentro del modelo que devuelva un array. si el OData detecta que existe este metodo dentro del modelo lo usará para serializer los objetos que se devuelven en la coleción de datos.
esto puede ser util para asegurarse de que la respuesta devuelva unicamente los datos que usted necesita incluyendo otros datos adicionales ejemplo:
class User extends Model
{
public function transform
( )
{
return [
"id" => $this->id,
"name" => $this->name,
"role" => $this->role_id,
"photo" => $this->getPhoto( )
];
}
}
Puede que la lógica de su transformador sea mucho mas robusta y necesite tener varios metodos que retornan un resultado procesado para agregarlo como una propiedad en cada uno de los objetos de la coleción. en este caso no es una buena idea asignar toda esta lógica al modelo y lo mejor es separar la lógica de la transformación de datos en una clase por separado.
puede hacer esto creando una clase común y corriente con un metodo transform que devuelva un array con los datos que desea mostrar por objeto, y puede usar el resto de la clase para agregar metodos que menejen y procesen la logica que necesita para las propiedades adicionales de su modelo ejemplo:
namespace App\Transformers;
class UserTransformer
{
public function getPhoto
( $user )
{
}
public function transform
( $user )
{
return [
"id" => $user->id,
"name" => $user->name,
"role" => $user->role_id,
"photo" => $this->getPhoto ( $user ),
];
}
}
Una vez ya creada la clase con la lógica del transformador deberá agregar la propiedad transformer a su modelo para indicarle al OData que use esta clase para serializer los objetos de la coleción que irá en la respuesta:
class User extends Model
{
public $transformer = "App\Transformers\UserTransformer";
}
Laravel/Fractal ya ofrece una librería para transformar datos https://fractal.thephpleague.com/transformers/, Laravel/OData es compatible con este caso de uso. puede agregar la propiedad transformer igualmente en su modelo especificando en lugar de una clase común una clase de transformador.
use App\Transformers\
UserTransformer;
class User extends Model
{
public $transformer = UserTransformer::class;
}
Conociendo que el OData devuelve todos los datos del modelo sin nigún tipo de filtro, es lógico pensar que puede que esto no se ajuste a su caso de uso.
puede que necesite ocultar cierta información a sus usuarios dependiendo del rol que tengan asignado, un ejemplo pordría ser las lista de ordenes donde cada usuario solo puede consultar las ordenes que ha realizado junto con su historial. y en el caso de tratarse del administrador este puede ver todas las ordenes.
# Usando un scope
puede definir la consulta por defecto que manejará el OData declarando un scope dentro de su modelo, el scope en este caso deberá llamarse defaultQuery y este debe retornar una consulta. si el OData detecta que este scope existe dentro del modelo, aplicará por defecto la consulta que devuelve el scope a la colección en la respuesta de la API.
class Order extends Model
{
public function scopeDefaultQuery ( $query )
{
if ( auth( )->user( )->isAdmin( ) )
{
return $query;
}
else {
return $query->where( "user_id" , auth( )->user( )->id );
}
}
}
Los desarroladores que consuman su API igualmente pueden filtrar la coleción desde la url, pero si aplico la consulta por defecto desde su modelo solo podrán filtrar los datos apartir del resultado por defecto que se le indico al OData. no podrán deshacer el filtro por defecto desde la url, solo podrán agregar mas filtros apartir del que ya está definido.
Si bien usar transformadores es una buena idea para armar los objectos de la coleción en sus respuestas de la API puede ver se en el problema de N+1 que es muy común a la hora de trabajar con un ORM. este problema le puede causar retrazos de velocidad en las repuestas de su API si no tiene en cuenta el número de consultas que realiza a la base de datos por cada objeto.
si tiene una colección de 50 usuarios y en el transformador de cada objeto llama a el rol, el estado, la cuidad, el departamento y su subscripción actual, y esto lo hace por cada usuario tendrá un resultado de 50 * 5 = 250 consultas a su base de datos lo que puede volver lenta la respuesta de la API.
si bien laravel ya nos ofrecen una solución usando el metodo with que ya viene con Eloquent y este se encarga de realizar las consultas a la base de datos necesarias para armar su colección y luego la arma desde memoria, para el ejemplo anterior esto va ir reduciendo el número de consultas de 250 a 6. igual debe saber que entre mas compleja sea la consulta mas dificil será de mantener usando un ORM por lo que en casos complejos es mejor optar por utilizar Consultas multitabla, Vistas o Procedimientos almacenados. sin embargo puede hacer uso del scope defaultQuery del OData para llamar el metodo with de eloquent si así lo requiere:
class User extends Model
{
public function scopeDefaultQuery ( $query )
{
$query =
$query->
with(
"role",
"state",
"city.department",
"subscriptions"
);
if ( auth( )->user( )->isAdmin( ) )
{
return $query;
}
else {
return $query->where( "user_id" , auth( )->user( )->id );
}
}
}
Si bien el metodo with nos ahorra el tener que hacer las consultas a la base de datos una por una y por cada objeto, esto no será util si se encuentra con una consulta compleja que relaciona muchas tablas, en estos casos tenga en cuenta que si su transformador tiene un metodo que hace 10 o mas consultas a la base de datos y eso lo hacer por cada objeto, puede verse en un problema de velocidad en su respuesta.
Para esto puede especificar un serializador dentro de su modelo declarando un metodo llamado collectionSerializer, si el OData detecta que este metodo existe en su modelo lo usará para serializer la colección antes de pasarselo al transformador. puede usar esta ventaja para realizar allí las consultas que necesita a su base de datos e ir armando la colección y una vez armada simplemente se la pasamos a los transformadores.
de está manera los transformadores ya no se verán obligados a realizar consultas por cada objeto si no simplemente a pintar las propiedades que ya vienen en la colección. un ejemplo de esto podría ser llamar un procedimiento almacenado o una consulta multitabla que devuelva todos los campos de las tablas que se necesitan en la consulta e ir armando la coleción con base en estos valores ejemplo:
class User extends Model
{
public function collectionSerializer( $collection )
{
$query =
collect( \
DB::select(
"select users.id as 'user_id', subscriptions.* , user_subscriptions.quantity_subscriptions from users, subscriptions, user_subscriptions where ..."
) );
$users =
$collection->
map(
function ( $user ) use ( $query ) {
$user->quantity_subscriptions = $query->where( "user_id" , $user->id )->quantity_subscriptions;
return $user;
} );
return $users;
}
}
Si no desea asignar la logica del serializador a su modelo, puede igualmente usar una clase para personalizar este trabajo, solo tiene que crear una clase común que tenga igualmente un metodo llamado collectionSerializer. esto le sirve para trabajar con multiples metodos dentro de su clase y separar la logica de serialización de datos de su modelo.
namespace App\Serializers;
class UserSerializer
{
public function collectionSerializer( $collection )
{
$query =
collect( \
DB::select(
"select users.id as 'user_id', subscriptions.* , user_subscriptions.quantity_subscriptions from users, subscriptions, user_subscriptions where ..."
) );
$users =
$collection->
map(
function ( $user ) use ( $query ) {
$user->quantity_subscriptions = $query->where( "user_id" , $user->id )->quantity_subscriptions;
return $user;
} );
return $users;
}
}
Una vez construida su clase deberá asignar la propiedad serializer a su modelo especificando la clase que debe usar para serializer las colecciones de datos. si especifico esta propiedad en su modelo el OData la usará para serializer los datos de su colección.
class User extends Model
{
public $serializer = "App\Serializers\UserSerializer";
}
Uno de los factores mas influyentes en la velocidad de respuesta de su API está en la paginación, toda aplicación está propensa a crecer desmesuradamente, puede que al inicio tenga que trabajar por ejemplo con un modelo de ordenes, y este tenga en la base de datos almacenedas 50 ordenes. lo cual traer 50 ordenes en una petición no supone ningún problema y no debería tener ningún retrazo en la velocidad de respuesta.
Sin embargo a medida que crece su aplicación y los usuarios empiezan a registrar ordenes puede encontrarse con el caso de tener 50.000 ordenes registradas en su base de datos, traerse estas 50.000 ordenes en una sola petición no solo supone un retrazo en la respuesta de la API si no que renderizar 50.000 datos en una tabla desde el FrontEnd puede también causar un problema en la Memoria de la computadorá del usuario.
Para evitar este tipo de problemas lo que mas se recomienda es usar paginación desde el inicio y por supuesto que puede hacerlo usando el OData. para implementar la paginación usando el OData deberá pasar los parámetros top y page a la url de la ruta donde expuso su modelo usando el metodo odataJsonResponse.
https://myapi.com/api/users?top=20&page=1
El parámetro top indica el número maximo de registros por petición que debe devolver la API en este caso 20 y el parámetro page indica la página que debe traer en este caso la primera. por lo que la anterior consulta deberá traer los usuarios del 1 al 20, el siguiente ejemplo consulta los usuarios del 21 al 40:
https://myapi.com/api/users?top=20&page=2
La siguiente el 41 al 60:
https://myapi.com/api/users?top=20&page=3
y así sucesivamente ...
Como se menciono al inicio, las rutas de su API no solamente devuelven los datos de una base de datos si no que también deben filtrar estos datos según la necesidad de la logica de negocio. por ejemplo si está exponiendo un modelo de usuarios puede que necesite filtrarlos ya sea por ciudad, por rol, por estado o por muchas mas opciones, esto lo puede llevar a tener un monton de rutas con filtros especificos solo para los usuarios lo cual extiende mucho mas los tiempos de desarrollo.
Con el OData puede mantener una sola ruta para su modelo y empezar a agregar filtros desde la url, esto le permite olvidarse de tener que programar cada filtro que necesite en su API a demas de que le da la posibilidad a los desarroladores Frontend jugar un poco mas con las rutas de su API y no depender de usted como desarrolador Backend cada vez que necesiten un filtro especifico.
Para empezar a aplicar filtros no olvide que debe crear un ruta que apunte a un metodo en su controlador y este metodo debe exponer el odataJsonResponse. una vez tenga esto ya estará listo para empezar agregar filtros desde su api.
Por ultimo debe saber que el OData filtrará los datos con base en los campos de la tabla en su base de datos mas no en los campos que declaro en su transformador, esto se mantiene así para poder escalar usando consultar mas complejas que incluyen inner joins entonces es una muy buena idea que el nombre de los campos en su transformador sean fieles a los nombre de los campos que tiene en la base de datos, ya que podría confundir a los desarroladores que consumen su API.
Para ayudarlo a comprender mejor los siguientes casos de uso tome como ejemplo un modelo de usuarios que se expone usando el metodo odataJsonResponse a travez de la ruta https://myapi.com/api/users y que devuelve una respuesta como esta:
{
"data" : [
"data" : [
{
"id" : 1,
"name" : "example user",
"role_id" : 2 ,
"state_id" : 1 ,
"city_id" : 12 ,
},
{
"id" : 2,
}
],
"length" :
2
],
"code" :
200
}
Para filtrar datos de que cumplan con una condicional exacta puede espeficiar el nombre del atributo segudo de : y seguido del valor con el que debe cumplir. un ejemplo de un filtro que trae los usuarios que vivien en la ciuedad con id 2.
https://myapi.com/api/users?filter=city_id:2
Esto lo puede aplicar con cualquier atributo de su tabla, pero tenga en cuenta de que si usa el operador : se filtrarán solo los datos que cumplan con la condición exacta por lo que si desea filtrar usando por ejemplo el nombre deberá pasar el nombre exacto incluyendo mayuscula y minusculas ejemplo:
https://myapi.com/api/users?filter=name:Pepito Peréz
Si desea aplicar la invarsa de igual que para en lugar de obtener los datos que cumplan con una condición en especifico, mas bien anular los registros que no cumplan con una condición especifica puede usar el operador ! que en este caso funciona igual que el operador != que se usa por ejemplo en el if. un ejemplo de una consulta de usuario donde el id sea diferente a 13, esto nos traera todos los usuarios menos el del id 13:
https://myapi.com/api/users?filter=id!13
Con la condicional igual que podemos filtrar datos que cumplen una condición exacta pero si lo que desea es filtrar por coincidencia en lugar de usar el operador : puede usar el operador / esto lo que hará es filtrar los usuarios por coincidencia por ejemplo en el caso de Pepito Peréz podemos filtrarlo pasando nomas pepito y en minuscula obviamente esto nos traerá otros usuarios que tengan igualmente en su nombre pepito ejemplo:
https://myapi.com/api/users?filter=name/pepito
Puede filtrar datos númeroricos que sean mayores a un valor especifico usando el operador > por ejemplo esta sería una consulta que trae todos los usuarios con el id mayor a 10:
https://myapi.com/api/users?filter=id>10
también puede aplicar este filtro usando fechas:
https://myapi.com/api/users?filter=created_at>2023-04-25
al igual que el operador > puede usar el operador < . funciona igual solo que en lugar de filtrar datos mayores a cierto valor filtrará los datos que sean menores a lo especificado:
https://myapi.com/api/users?filter=id<10
igualmente puede aplicar fechas de esta manera:
https://myapi.com/api/users?filter=created_at<2023-04-25
Puede filtrar datos que se encuentren en un rango de valores especificos usando la condificional in , un ejemplo podría ser una consulta de los usuarios que tengan en su id un número del uno al 10 ejemplo:
https://myapi.com/api/users?filter=id[1-2-3-4-5-6-7-8-9-10]
en este caso se usa el separador - para separar los valores que se tendrán en cuenta al filtrar los datos. no se usa , ya que la coma tiene otra funcionalidad reservada. igualmente puede filtrar no solamente usando números si no también letras ejemplo:
https://myapi.com/api/users?filter=name[Pepito Peréz-Pepito Lopéz]
Si bien puede filtrar los datos de su API usando diferentes tipos de operadores es logico pensar que necesite aplicar mas de un filtro a la vez, un ejemplo podría ser una consulta de los usuarios que viven en la ciudad con id 5 y que tengan el rol con id 3. para aplicar una consulta como esta solo tiene que separar los filtros que ya aprendio anteriormente por una , entonces el filtro nos quedaría de esta manera:
https://myapi.com/api/users?filter=city_id:5,role_id:3
pueda agregar tantos filtros al tiempo como guste solo debe separarlos por , y el OData filtrará los datos que cumplan todas las condicionales que se le pasarón por url, un ejemplo mas:
https://myapi.com/api/users?filter=id[1-2-3],created_at>2023-04-25,role_id!2
al igual que con and puede aplicar multiples filtros a la vez pero en lugar de separlos usando , tendrá que separarlos usando | . en este caso el OData no filtrará los usuarios que cumplan con todos los filtros pasados a la url si no que filtrará los usuarios que cumplan con alguno de ellos, no es necesario que cumpla con todas las reglas le bastará solo con cumplir una. un ejemplo podría ser una consulta a los usuarios que viven en la ciudad con id 1 o con id 2:
https://myapi.com/api/users?filter=city_id:1|city_id:2
igualmente puede agregar tantos filtros como desee en la url simplemente debe separarlos por | , un ejemplo mas:
https://myapi.com/api/users?filter=role_id:1|role_id:2|city_id:5|city_id:7|id[1-2-3-4-5]
Si bien puede separar sus filtros usando and u or debe saber que no debe mezclar estás 2 condicionales en una sola consulta por ejemplo la siguiente consulta no funcionará:
https://myapi.com/api/users?filter=id:1|id:2,role_id:3
En este caso se están aplicando tanto el separador , como el | lo cual generará conflictos en la consulta a la base de datos, esto no es un problema del OData si no mas bien un problema de separación de condicionales en SQL. si realiza este tipo de consulta no obtendrá ningún tipo de error pero puede que el filtro simplemente falle y no traiga los datos que usted solicito.
puede separar sus filtros usando el parámetro select que actua como un filtrado del resultado de filter por lo que primero se filtrarán los datos usando las condicionales pasadas a filter y encima de ese resultado se aplica los filtros enviados por select esto en SQL es equivalente a hacer un select * from (select ...) es decir una consulta sobre otra consulta.
select funciona exactamente igual que filter usa los mismos operadores y los mismos separadores and y or ejemplo:
https://myapi.com/api/users?select=id[1-2-3],role_id:5
https://myapi.com/api/users?select=city_id:7|id<10
puede usar esta ventaja para poner sus filtros tipo and por ejemplo en el parámetro filter y los filtros tipo or en el parámetro select ejemplo:
https://myapi.com/api/users?filter=role_id:3,state_id:1&select=id:1|id:2
o puede hacerlo al reves para que filter maneje los filtros topo or y select los filtros tipo and ejemplo:
https://myapi.com/api/users?filter=id:1|id:2&select=role_id:3,state_id:1
el punto es que de está manera separamos las condicionales, evitamos conflictos de parentesis en las consultas de la base de datos y nos aseguramos de que el filtro funcione correctamente y devuelva los datos según lo que se necesita.
si bien select es un sobre filtro del resultado que arroja filter debe tener en cuenta que filter es mas poderoso que select y que con filter podemos hacer consultas incluyendo inner joins lo cual no se puede hacer con select. tenga esto en cuenta al momento de decidir a quien asignarle la responsabilidad del filtro por tipo or o and.
Uno de los casos mas comunes a la hora filtrar datos en una base de datos es hacer consultas multitabla o Inner joins para poder filtrar por ejemplo usuarios con base en datos almacenados en otras tablas, un ejemplo podría ser una tabla de peliculas que tiene las siguientes relaciones en la base de datos:
| movies |
| id |
| name |
| description |
| category_id |
| categories |
| id |
| name |
| state_id |
En este caso la tabla movies se relaciona con categories atravéz de la foranea category_id y categories se relaciona con states atravéz de la foranea state_id. aunque en el OData oficialmente los inner joins no están disponibles es posible solictar inner joins en este paquete usando un filtro desde la url.
En el caso de que quiera filtrar las peliculas por categoría puede hacerlo normalmente usando la foranea category_id aplicandola en un filtro normal ejemplo:
https://myapi.com/api/movies?filter=category_id:1
Sin embargo en el caso de que quiera filtrar las peliculas por el nombre de la categoría tendrá que aplicar un inner join para poder acceder al nombre de la categoría. en este caso el filtro se aplica igual con la diferencia de que debe especificar el nombre de las tablas concatenadas por un punto ejemplo:
https://myapi.com/api/movies?filter=categories.name:Infantil
Es mas común que intente filtrar por nombre usando coincidencia mas no equivalencia entonces el filtro mas cumún podría verse así para el caso de un buscador:
https://myapi.com/api/movies?filter=categories.name/Infan
Puede anidar tantas tablas como necesite solo debe separarlas pur un punto, una consulta mas anida por ejemplo podía ser filtrar las peliculas cuya categoría tenga el estado con nombre habilitado en este caso la url nos quedaría así:
https://myapi.com/api/movies?filter=categories.states.name:habilitado
Si bien el OData en este caso nos permite hacer consultas mas complejas utilizando inner joins es verdad que el hecho de tener que pasar el nombre de la tabla por url puede ser confuso para los desarroladores Frontend ya que lo mas probable es que ellos no conozcan el nombre de las tablas de su base de datos y por ende no sepan como realizar el filtro.
para solucionar este problema usted puede agregarle alias a sus tablas para que el OData identifique la tabla a la que se hace referencia incluso si se pasa un nombre completamente distinto desde la url, sin embrago debe tener en cuenta que de igual manera los desarroladores Frontend deben tener una referencia de las relaciones en la respuesta de su API, por ejemplo continuando con el ejemplo anterior usted podría devolver por cada objeto de su transformador algo como esto:
[
{
"id" :
1,
"name" :
"movie 1",
"description" :
"descripction of movie",
"category_id" :
1,
"category" : {
"id" :
1,
"name" :
"acción",
"state_id" :
1,
"state" : {
"id" : 1
"name" : "habilitado"
}
}
},
{
}
]
En este caso lo mas probable es que el desarrolador Frontend intente filtrar según lo que el ve en la respuesta de la API. entonces si quiere filtrar por el nombre del estado de la categoría lo que tendría mas sentido es que la url quedará de esta manera:
https://myapi.com/api/movies?filter=category.state.name:habilitado
Si se da cuenta es lo mismo que nombrar las tablas aniadadas como lo hicimos anteriormente filter=categories.states.name:habilitado sin embargo para que esto funcione en singular como está en la respuesta de su API deberá agregar alias a sus tablas. para hacer esto cree un config/odata.php en su proyecto que devulva un array con la configuración que requiere el OData para usar los alias, no olvide que la carpeta config ya viene por defecto creada en su proyecto junto con otros archivos de configuración como app.php , auth.php etc ...
en este caso solo tendrá que crear un archivo llamado odata.php junto con la siguiente configuración de ejemplo:
return [
"singular" => [
"category" => "categories" ,
"state" => "states" ,
]
];
y listo esto es todo lo que tiene que hace para agregar alias a sus tablas, por ultimo no olvide que puede igualmente usar todos los operadores y las condicionales and y or que usa en sus filtros normales en los inner joins un ejemplo:
https://myapi.com/api/movies?filter=category.state.name:habilitado|category.state.name:activado|category.name:acción
Aparte de poder filtrar los datos de su API atravéz de la url también puede realizar otro tipo de operaciones adicionales sobre sus datos para mantener de mejor manera los resultados de las consultas a su API.
Puede solicitarle a la API que ordene los resultados con base en uno de los campos del modelo, para esto puede pasarle el parámetro orderBy en la url y este parámetro debe indicar el campo por el que se desea ordenar los resultados ejemplo:
https://myapi.com/users?orderBy=id
En el caso de tratarse de un entero se ordenarán los datos de menor a mayor, en el caso de un string se ordenarán alfabeticamente de la A-Z y en el caso de las fechas se ordenarán los registros del mas viejo al mas nuevo ejemplo:
https://myapi.com/users?orderBy=id
https://myapi.com/users?orderBy=name
https://myapi.com/users?orderBy=created_at
Al igual que el parámetro orderBy puede usar el parámetro orderByDesc para ordenar los resultados de la API este parámetro funciona igual que orderBy con la diferencia de que ordenará los datos a la inverza ejemplo:
https://myapi.com/users?orderByDesc=id
https://myapi.com/users?orderByDesc=name
https://myapi.com/users?orderByDesc=created_at
Por defecto las repuesta de la API devolverán una colección de datos. en algunos casos puede que necesite traerse la información de un solo modelo lo cual no es viable obtener la coleción de datos y luego llamar el registro de la posisión cero por ejemplo users[0]. para solucionar esto usted puede pensar en crearse un endpoint que por url le pase el id del modelo que quiere obtener y devolver la información de este registro en un objeto en lugar de una coleción de datos por ejemplo:
https://myapi.com/users/1
Sin embargo esto ya es posbile hacerlo usando el OData no tendrá que agregar un nuevo endpoint solo deberá pasar el parámetro first a la url que ya devuelve la coleción de datos y este en lugar de devolver una coleción como esta por ejemplo:
{
"data" : {
"data": [
{
"id" : 1,
},
{
"id" : 2,
},
],
"length":
20
},
"code" :
200
}
Le devolverá un solo objeto en este caso el primero de la coleción:
{
"data" : {
"data": {
"id" : 1,
}
"length":
1
},
"code" :
200
}
Si desea obtener un objeto diferente al primero simplemente aplique un filtro por id para obtener unicamente el dato por este id y aplique igualmente el parámetro first para que traiga ese dato en un objeto en lugar de una coleción ejemplo:
https://myapi.com/users?filter=id:5&first
Esta sería la respuesta que recibiriamos:
{
"data" : {
"data": {
"id" : 5,
}
"length":
1
},
"code" :
200
}
Si bien ya puede filtrar los datos de la API usando cualquiera de los campos de su modelo puede que necesite filtrar los datos por el Bearer token es decir por el usuario actualmente loguado. aunque puede pasar el id del usuario por parámetro normalmente por ejemplo si quiere filtrar las ordenes del usuario podría usar un filtro de este tipo:
https://myapi.com/orders?filter=user_id:1
Sin embargo el tener que pasar el id del usuario por la url obliga al desarrollador Frontend a realizar un proceso extra para obtener el id del usuario logueado y luego pasarlo por parámetro. con el OData usted puede evitar este proceso extra indicando el alias {auth.user.id} en su url, de esta manera el OData usará el id del usuario logueado obtenido de la función auth()->user() de laravel ejemplo:
https://myapi.com/orders?filter=user_id:{auth.user.id}
No solamente puede usar el id del usuario, puede usar las demas propiedades también ya que {auth.user} es una alias para el objeto del usuario actual. entonces por ejemplo podemos usar un filtro también por el nombre de usuario de esta manera:
https://myapi.com/orders?filter=user.name:{auth.user.name}
Una de las funcionalidades mas comunes en una API es la exportación de datos ya sea en CSV , Excel o cualquier otro medio donde se puedan descargar. maatwebsite/excel ofrece una librería muy completa para manejar la exportación de datos en laravel, este paquete de OData es completamente compatible con laravel-excel. puede usar esta ventaja para exportar colecciones en CSV por ejemplo pero manteniendo el uso del OData para poder filtrar y ordenar los datos en su archivo de EXCEL.
Para empezar instale el paquete de maatwebsite/excel en su proyecto y luego cree una clase de tipo export para su modelo de esta manera:
php artisan make:export UserExport
Esto debio crearle un app/Exports/UserExport.php archivo en su proyecto. lo siguiente que debe hacer es agregar un constructor dentro de la clase que resiva la coleción que le pasará a el OData y usar esta coleción para armar las propiedades de su archivo. la coleción que pasa el OData al constructor es la coleción procesada con los filtros, paginación y demas instrucciones que hayan pasado a la url.
namespace App\Exports;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\FromCollection;
class UserExport implements
FromCollection,
ShouldAutoSize,
WithTitle,
WithMapping,
WithHeadings
{
use Exportable;
private collection;
public function __construct( Collection $collection )
{
$this->collection = $collection;
}
public function collection( )
{
return $this->collection;
}
public function title( ) :
string
{
return "Usuarios";
}
public function map( $user ) :
array
{
}
public function headings( ) :
array
{
}
}
Una vez creada su clase de exportación simplemente agregue la propiedad export a su modelo indicando la clase que debe usar para exportar los datos en un archivo excel ejemplo:
use App\Exports\
UserExport;
class User extends Model
{
public $export = [
"instance" => UserExport::class,
"fileName" => "Usuarios.xlsx"
];
}
Con esto configurado lo unico que tiene que hacer es usar la ruta que apunta a un metodo de su controlador que ya expone el metodo de odataJsonResponse y a esta url debe agregarle el parámetro export para indicarle al OData de que devuelva los datos en un archivo excel ejemplo:
https://myapi.com/api/users?export
de igual manera puede aplicar, filtros, paginación, ordenamiento de datos y demas para indicarle al OData que datos debe cargar en el archivo excel un ejemplo aplicando un filtro:
https://myapi.com/api/users?export&filter=role_id:2,state_id:1
Por ultimo debe saber que no debe sentirse atado a usar el metodo odataJsonResponse y retornarlo directamente desde su controlador, si desea puede usar el metodo odataModelCollection que ya se menciono anteriormente, para obtener unicamente la coleción del modelo con todos los filtros paginación y demas aplicados sobre este. de esta manera usted puede personalizar sus respuestas de la API, los paquete que usa para exportar datos y demas.
Este paquete puedes encontrarlo en github como Gabriel1777/laravel-odata.