Cómo filtrar por un campo en una aplicación Symfony dependiendo del usuario conectado

symfony_1_1.jpg
Solucionex
17
Abr 19
Cómo hacer un filtro

¿Para qué sirve un filtro de Doctrine?

Un filtro nos puede venir muy bien, por ejemplo, si tenemos una aplicación web en la que tenemos un sistema de usuarios en la que cada usuario solo puede acceder a los elementos de su propio proyecto u organización.

Planteamos entonces un supuesto donde tendremos una entidad Usuario que tendrá un campo proyecto para identificar a cuál pertenece y por lo tanto solo podrá acceder a elementos de dicho proyecto.

Pongamos que la aplicación web es una nube de blogs. Con lo que cada proyecto será un blog distinto, y por lo tanto tendremos también una entidad Post que, al igual que el usuario, tendrá un campo proyecto.

De esta manera ya tenemos relacionados los posts con los usuarios dentro un mismo blog.

¿Por qué filtrar?

Cuando accedamos a la aplicación con un usuario querremos ver solo el listado de posts al que se tiene acceso.

Esto es sencillo y no hace falta hacer ningún tipo de filtro que nos saque de nuestra comodidad a la hora de desarrollar en Symfony. Se puede hacer una consulta a la base de datos y recuperar solo el listado de posts en el que el proyecto coincide con el del usuario conectado.

Seguridad

Llegado a este punto, te invitamos a que abandones esa práctica y utilices un filtro de doctrine, porque así nos aseguramos que en todas las consultas a los posts, se va a relacionar con el proyecto del usuario conectado, esto implica: lectura, edición, borrado...

Implementación

EventListener

Nos crearemos un Listener en el fichero src/EventListener/OnRequestListener.php:

use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Core\Security; class OnRequestListener { private $em; private $security; public function __construct(EntityManagerInterface $em) { $this->em = $em; $this->security = $security; } public function onKernelRequest(GetResponseEvent $event, Security $security) { if ($this->security->getUser() instanceof User) { // Enable the filter $filter = $this->em->getFilters()->enable('project'); $filter->setParameter('project', $user->getProject()) } } }

Declarar el Listener en el services.yaml

Services: on_request_listener: class: App\EventListener\OnRequestListener tags: - {name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

Después de estos dos pasos cada vez que recarguemos una página el flujo va a pasar por el OnRequestListener.php y va a habilitar el filtro "project", además, se le va a asignar el parámetro "project" con el valor del del usuario conectado.

Escribir la Query

Symfony y Doctrine son listos, pero no tanto como para relacionar ese filtro que hemos habilitado y ese parámetro asignado con la consulta que queremos hacer a la base de datos, así que vamos a explicarle a nuestra aplicación qué queremos que haga con esos datos.

En el fichero src/Library/Filters/ProjectFilter.php escribiremos nuestro código:

namespace App\Library\Filters; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter; class ProjectFilter extends SQLFilter { public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ($targetEntity->hasAssociation('project') && $this->hasParameter('project')) { return '('.$targetTableAlias.'.project_id = '.$this->getParameter('project').')'; } return ''; } }

Habilitar el filtro en la configuración de Doctrine:

Ya solo nos queda el último paso y este será añadir el filtro que acabamos de crear a la configuración de doctrine, que en Symfony 4 está en config/packages/doctrine.yaml:

doctrine: filters: project: App\Library\Filters\ProjectFilter enabled: true

Ahora sí, después de esto siempre que intentemos hacer una consulta a una entidad con un campo "project", automáticamente doctrine lo va a relacionar con el del usuario conectado.

Esto nos da una gran seguridad ya que evitaremos que un usuario pueda no solo acceder a un listado de posts, si no a un post en concreto, pero igual para eliminarlo o editarlo, ya que llevado a SQL, al final de cada consulta se va a añadir "Where project = x", siendo x el proyecto del usuario conectado.

¿Y si quiero hacer una excepción?

Muy bien, ahora que tenemos un sistema muy fuerte en seguridad, hay una nueva feature en la que un usuario tiene la necesidad de acceder a los posts de otro proyecto.

Tenemos la opción de modificar el código que hemos tocado, que es bastante maleable, donde en el Listener podemos meter la lógica por la cual se rige la nueva feature, y en la consulta también se puede cambiar el "=" por un "IN", o lo que necesitemos...

Pero también si es para una situación muy puntual, como una cierta operación, esa operación, antes de hacer la consulta puede deshabitar el filtro:

$this->em->getFilters()->disable('project');

Después de esto, a la consulta que tengamos que hacer no se le añadirá lo implementado en ProjectFilter.php

Saber más sobre filtros de Doctrine y Symfony

En la documentación oficial de Doctrine puedes encontrar los filtros: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/filters.html

Si has utilizado alguna vez el Softdeletable, a lo mejor lo has hecho automáticamente, sin pensar mucho qué hace por detrás, pero es un buen ejemplo de filtro de Symfony y Doctrine, aquí tienes el código: GitHub