Symfony. Creando un filtro para Doctrine
Aprende a añadir un filtro en todas las consultas que realices sobre una entidad
Hoy os quiero hablar de una característica de Doctrine que puede resultar muy útil para implementar cierta funcionalidad: añadir una condición de forma automática a todas las consultas que realicemos.
Os contaré un posible caso de uso para esta característica: imagina que estás desarrollando un blog en donde los artículos pueden ser archivados, de modo que no deben listarse en ningún sitio salvo dentro del panel de administración.
Igual dices… ¡menudo problema! Añado a todas la consultas algo de este estilo:
->andWhere('post.isArchived = :isArchived')
->setParameter('isArchived', false)
y marchando.
El problema es que estarás conmigo en que añadir esa condición a absolutamente todas las consultas que realices hará que la aplicación comience a ser más difícil de mantener. De hecho, está el problema añadido de las relaciones:
class Category {/**
* @ORM\OneToMany(targetEntity="Article", mappedBy="category")
*/
private Collection $articles;}
Si por lo que sea hacemos:
$articles = $category->getArticles();
$firstTitle = $articles->first()->getTitle();
La consulta que Doctrine lanzará por debajo (recuerda que las relaciones se cargan por defecto de forma Lazy) no tendrá en cuenta esa condición.
Necesitamos algo más potente, y es aquí donde hacen aparición los filtros de Doctrine. ¡Veamos cómo implementarlos!
Creación de un filtro para Doctrine
Vamos a partir de la base de que nuestra entidad Article
tiene una propiedad isArchive
definida de la forma habitual:
/**
* @ORM\Column(type="boolean")
*/private bool $isArchived;
El objetivo es que todas las consultas filtren por defecto los artículos marcados como archivados.
Para ello crearemos la clase ArchivedFilter
en la carpeta src/Doctrine
:
<?phpdeclare(strict_types=1);
namespace App\Doctrine;use App\Entity\Article;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;class ArchivedFilter extends SQLFilter
{
public function addFilterConstraint(
ClassMetadata $entity,
$targetTableAlias
) { if ($entity->getReflectionClass()->name !== Article::class) {
return '';
}
return sprintf('%s.is_archived = false', $targetTableAlias);
}
}
Esta clase debe implementar el método addFilterConstraint
el cual nos va a permitir añadir la condición que necesitemos a la consulta. Recibe dos argumentos:
ClassMetadata $entity
, con la información de la entidad que se está consultando.$targetTableAlias
, que contiene el alias que se ha asignado a la tabla dentro de la consulta SQL.
y devuelve un string con lo que queramos añadir al final de la consulta.
Puesto que queremos lanzar este filtro únicamente cuando estamos consultando artículos, es necesario comprobar por medio de la propiedad ClassMetadata $entity
que la consulta está referida a la clase Article
:
if ($entity->getReflectionClass()->name !== Article::class) {
return '';
}
Por otra parte, la condición que queremos añadir debe estar escrita en SQL. Es decir, deberemos escribir el nombre de la columna ( is_archived
) y no el de la propiedad como haríamos si fuera DQL ( isArchived
).
Con esto ya tenemos nuestro filtro configurado y listo para activarlo.
Activación del filtro en Doctrine
La forma más rápida de activar el filtro que acabo de crear es mediante el archivo de configuración de Doctrine: config/packages/doctrine.yaml
.
Dentro de la propiedad orm
añadiremos la clave filters
:
doctrine:
...
orm:
filters:
article_filter_archived:
class: App\Doctrine\ArchivedFilter
enabled: true
lo cual bastará para activar el filtro de manera global en todas las consultas que involucren a la entidad Article
.
Además, podremos referirnos al filtro para activarlo o desactivarlo mediante el nombre article_filter_archived
.
Desactivación del filtro
¿Cómo podemos desactivar el filtro? Por ejemplo, si queremos construir una vista donde se muestren los artículos archivados necesitaremos desactivar ese filtro para nuestra consulta.
Para ello basta con realizar lo siguiente antes de realizar la consulta:
/** @var EntityManagerInterface $em */
$em = $this->getDoctrine()->getManager();
$em->getFilters()->disable('soft-article_filter_archived');
Esto desactivará el filtro para el resto de la petición. Si quisiéramos volver a activarlo realizaríamos el proceso inverso:
/** @var EntityManagerInterface $em */
$em = $this->getDoctrine()->getManager();
$em->getFilters()->enable('soft-article_filter_archived');
Pasando parámetros al filtro
Una característica que nos ofrecen los filtros de Doctrine es la posibilidad de definir parámetros que podremos emplear dentro del método addFilterConstraint
.
Es decir, en vez de forzar la comprobación contra false
en el filtro:
return sprintf('%s.is_archived = false', $targetTableAlias);
podemos hacer que este valor se recoja de un parámetro.
El primer paso es emplear el método getParameter
que proporciona la clase SQLFilter
de la que estamos extendiendo:
return sprintf('%s.is_archived = %s', $targetTableAlias, $this->getParameter('isArchived'));
A continuación iremos al archivo de configuración de doctrine: config/packages/doctrine.yaml
y añadiremos lo siguiente
doctrine:
...
orm:
filters:
article_filter_archived:
class: App\Doctrine\ArchivedFilter
enabled: true
parameters:
isArchived: false
Además, más adelante podremos especificar el valor del filtro del siguiente modo:
/** @var EntityManagerInterface $em */
$em = $this->getDoctrine()->getManager();
$filter = $em->getFilters()->enable('soft-article_filter_archived');
$filter->setParameter('isArchived', false');
Fácil, ¿verdad?
Conclusiones
Como has visto, los filtros es una característica muy interesante de Doctrine que nos permiten desarrollar funcionalidades que a priori pueden resultar más “difíciles” de implementar.
Además, si nuestra aplicación requiere de un borrado “soft” de entidades, los filtros nos permitirán implementar esta funcionalidad en apenas unos pasos. De hecho es la forma en que funciona la extensión SoftDeleteable:
❗️ Filtros de Doctrine y Soft Delete
Si bien los filtros como hemos visto son una herramienta poderosa, tienen una limitación cuando implementamos “soft delete” en nuestras entidades. Te aconsejo que revises esta issue donde se explica detalladamente antes de que te lances a programar y más tarde te topes con el problema que aquí se cuenta:
¿Quieres recibir más artículos como este?
Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇
Apóyame en Patreon
🧡🧡🧡 Gracias a: Joseba, Óscar, Alex y Jorge.