Doctrine y Symfony

Implementando Filtros Reutilizables en Symfony + Doctrine.

Un uso práctico de los patrones estrategia y cadena de responsabilidad.

Matías Navarro Carter
8 min readJun 12, 2018

--

“Nada nuevo hay bajo el sol” es una máxima bíblica que puede aplicarse sin problema al desarrollo de Software, especialmente a la POO. Es por esta verdad que los patrones de diseño existen: debido a que nada nuevo hay bajo el sol, otros se han enfrentado a los mismos problemas y dificultades que nosotros enfrentamos ahora en nuestros proyectos, y han propuesto inteligentes soluciones para estos problemas comunes.

Quizás sea bueno establecer antes de empezar que un patrón de diseño no es per sé la solución al problema, sino una forma de solucionarlo. Es una especie de receta: no es el platillo en sí mismo, pero son instrucciones de cómo poder cocinar un buen platillo. El aplicar las instrucciones de forma correcta para llegar al resultado deseado, corresponde al criterio y experticia del programador. No hay panaceas en el Desarrollo de Software.

Doctrine: Entidades y Repositorios

Doctrine es una libería hermosa y extremadamente eficiente, y hace una muy buena pareja con Symfony. Sin embargo, si has usado ambos por el tiempo suficiente te habrás dado cuenta de un problema común: las clases de repositorios tienden a volverse clases muy infladas, donde es difícil reutilizar lógica y donde hay demasiada repetición de código (por ejemplo, cuando se usan query filters). Este es un problema que muchos otros han tratado de solucionar, por ejemplo, aquí de no tan buena forma, y aquí de una mejor forma (usando el patrón Composición).

Este ha sido un problema molesto para mí por mucho tiempo. No me gusta cuando no puedo reutilizar código, o se me hace difícil hacerlo. No sólo por una cuestión de pereza, sino porque el código duplicado es difícil de mantener: es una de las reglas de oro de escribir código.

Buscando la API perfecta

Me decidí a eliminar el problema de Doctrine y sus repositorios. De hecho, lo que quería era lograr abstraerme de Doctrine en mis controladores por completo. Así que me puse a diseñar.

Nótese que dije diseñar, y no programar. Esto es muy importante. Uno de los consejos más buenos que he escuchado sobre desarrollo y modularización en POO es el siguiente: “escribe la api que te gustaría utilizar” y luego, impleméntala.

Así que comencé con el diseño. ¿Qué me gustaría escribir en mis controladores para poder obtener una colección páginada y filtrada de cierta entidad? Y terminé con esto:

Simple y limpio. Quiero una colección paginada de usuarios, fetchCollection es más que suficiente, específicando el nombre de la entidad de la cual quiero una colección.

De inmediato supe que debía extraer esto a su propia clase (que sería un servicio), y que además debería tener un método similar pero para obtener un sólo objeto. Asi que, pronto la interfaz se volvió obvia.

Bien, la interfaz estaba lista. Este servicioEntityFetcher estaba en camino de ser implementado. Pero ahora la parte más difícil: pensar en la implementación concreta de esta funcionalidad.

La Implementación del EntityFetcher

Otra de las reglas de POO es que, a mayor abstracción, menor control. Mientras más abstraemos cosas, las hacemos más simples y, por tanto, tomamos muchas desiciones por la clase cliente que usará la interfaz, reduciendo la capacidad de hacer cosas más custom. Generalmente, las mejores liberías son las que exponen una api para poder realizar cosas muy fácilmente, pero aún permiten acceder a las funciones de más bajo nivel. Es por eso que Doctrine tiene métodos para realizar queries fácilmente, mientras que aún te permite escribir SQL puro.

Esta es la implementación concreta del método fetchCollection :

El signature cambia un poco desde la interfaz y añade dos parámetros más. $repositoryMethod permite añadir el nombre de un método en el repositorio en vez de crear un objeto QueryBuilder automáticamente. El segundo es un parámetro que se pasa a la instancia de Paginator para personalizar asuntos relativos a los join.

Los dos últimos métodos antes de la creación de la instancia del Paginator setean los resultados máximos que la Query mostrará, además del offset dónde comenzará. $this->getMaxResults() y $this->getFirstResult() son métodos privados de la clase que acceden a la Request (inyectada en el constructor) y obtenen de los parámetros query size y page los valores correspondientes.

Esta clase entonces, llama al manager de determinada entidad usando el FQCN, crea un objeto QueryBuilder y devuelve todos los items que se encuentran en la base de datos para esos registros. Por supuesto, esta devolución de hace de manera controlada, como corresponde, páginando la consulta apropiadamente, como siempre debería hacerse en consultas de colecciones.

Sin embargo, ¿qué hacemos si no queremos devolver todos los registros de una colección dada cierta condición? ¿O si queremos aplicar filtros usando paramétros query en la request?

Aquí es cuando los dos métodos $this->applyExtensions($qb, $classMeta) y $this->applyFilters($qb, $classMeta) entran en nuestra historia.

Aplicando Filtros usando Cadena de Responsabilidad

Ambos métodos mencionados, no hacen otra cosa que iterar sobre un arreglo de objetos que implementan cierta interfaz, para determinar si filtros y extensiones deben ser aplicados a la Query actual:

Estas interfaces implementan el patrón Estrategia. Básicamente, funciona así: puedo poner toda clase de lógica de filtro en esas interfaces, mientras sigan las convenciones establecidas y posean un supports() que devuelva un booleano y un applyAlgo() que modifique los objetos que le entrego, como el QueryBuilder .

Este patrón, combinado con el de Cadena de Responsabilidad, hace de estos filtros una utilidad poderosa. En cadena de responsabilidad, un mensaje se enruta a través de varios handlers, y cada uno de ellos decide si puede o no procesar el mensaje. En este caso, el EntityFetcher itera sobre todos los filtros en esta cadena, y son los filtros mismos los que deciden si pueden procesar el mensaje utilizando lógica booleana y los parámetros que decidas enviarles. Luego, si efectivamente pueden, se llama al método apply de la interfaz.

Como nota aparte, el método de setRootAlias() no es crucial, sino que sólo entrega un poco de azúcar sintactica sobre un filtro abstracto que sirve como base.

Tomemos como ejemplo el siguiente filtro implementando una de las interfaces:

El método supports() devuelve true sólo si la request contiene un parámetro query llamado contains y si éste no está vacío. Si supports() devuelve true, esto le dice a la clase que procesa los filtros, que este filtro cumple con las condiciones para ser aplicado. La lógica de applyFilter() simplemente se encarga de aplicar ese filtro.

Podemos crear todo tipo de expresiones en supports() incluso para hacer que un filtro funcione sólo en una entidad en específico:

La extensiones siguen el mismo principio, pero en vez de usar información de la Request para modificar la Query, utilizan información interna, como los servicios de autenticación o autorización. Una permite al cliente filtrar la query, la otra permite a la aplicación hacerlo. Aquí hay una extensión de ejemplo:

Un poco más de azúcar

Dijimos que este EntityFetcher es un servicio. Por ende, necesitamos inyectarlo en nuestros controladores para poder usarlo. Vamos a usar una técnica que nos permite poner un poco de azúcar en nuestros controladores para que puedan acceder a sus métodos como si su scope fuera el mismo.

Para esto, vamos a usar una de las funcionalidades más útiles de PHP: los traits. Los traits son unas especies de clases que se utilizan para la reutilización de código de manera horizontal: cuando tienes comportamiento que otras clases quieres que adopten, pero sin tener que extender desde otra clase porque la herencia es malvada, puedes usar un trait. Simplemente, implementamos un trait que use el container para obtener el servicio y llamar a su método. Así, el trait se transforma en una especie de proxy del servicio:

Así se usaría este trait en los controladores:

¿Y cuáles son las ventajas de esto?

Una de las ventajas más evidentes que hemos logrado es el poder contar con una API sencilla (muy fácil de usar) y universal (puede ser usada en cualquier parte donde se use el ORM) sobre Doctrine para poder obtener colecciones páginadas de recursos o recursos individuales. Esto es muy útil si estás desarrollando una REST API, por ejemplo.

Las mayores ventajas de esta nueva funcionalidad se encuentran en la sencilla paginación por defecto y en la capacidad de poder definir filtros y extensiones que se pueden reutilizar fácilmente, dada su modularización (cada uno ocupa su propia clase). Hemos separado concerns, hemos quebrado un problema varias partes que trabajan bien entre sí y le hemos dado una abstracción mayor a todo para hacer nuestro trabajo más rápidamente.

Por último, la opción de pasar un método de repositorio al obtener la colección es muy útil, debido a que el desarrollador aún puede utilizar queries más personalizadas que ejecuten muchos joins y cosas así, desde el repositorio.

Parte del código que he mostrado aquí forma parte de un bundle de Symfony oficial de Option SpA (desarrollado por mí) para precisamente abstraer un poco doctrine y permitirnos crear y filtrar colecciones con facilidad.

Extra: Añadiendo los filtros y las extensiones al servicio

Como dije, parte del código mostrado aquí es propietario (esto no significa que no puedas usar la idea), pero por ello no mostraré ejemplos concretos de cómo añadir los filtros y las extensiones definidas en runtime a tu EntityFetcher . Tenemos que guardarnos algunos secretos, ¿no?

Lo único que diré para ayudarte es que debes utilizar el container builder para definir una interfaz autoconfigurable, con una etiqueta. Luego, debes hacer algo llamado CompilerPass que injecte los servicios taggeados en la definición de tu EntityFetcher .

--

--

Matías Navarro Carter

Christian, Husband, Programmer, Historian, Theology Student.