Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Filtros: Modificar consultas automáticamente

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $6.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Gracias a nuestro nuevo y genial método, podemos filtrar las galletas de la suerte descatalogadas. Pero, ¿y si queremos aplicar un criterio como éste de forma global a todas las consultas de una tabla? Por ejemplo, diciéndole a Doctrine que siempre que busque galletas de la suerte, añada WHERE discontinued = false a esa consulta.

Parece una locura. Y, sin embargo, es totalmente posible. Para demostrarlo, volvamos a dejar nuestras dos plantillas como estaban antes. Y ahora... si entramos en "Proverbios"... ¡sí! Vuelven a aparecer las 3 fortunas.

Hola Filtros

Para aplicar una cláusula WHERE "global", podemos crear un filtro Doctrine. En el directorio src/, añade un nuevo directorio llamado Doctrine/ para la organización. Dentro de él, añade una nueva clase llamada DiscontinuedFilter. Haz que ésta extienda SQLFilter... luego ve a Código -> Generar (o "comando" + "N" en un Mac) y selecciona "Implementar Métodos" para generar el único método que necesitamos addFilterConstraint().

... lines 1 - 4
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class DiscontinuedFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
// TODO: Implement addFilterConstraint() method.
}
}

Una vez que tengamos todo listo, Doctrine llamará a addFilterConstraint() cuando esté construyendo cualquier consulta y nos pasará información sobre la entidad que estamos consultando: esto es ClassMetadata. También nos pasará el$targetTableAlias, que necesitaremos dentro de un minuto para modificar la consulta.

Ah, y para evitar un aviso de obsoleto, añade un tipo de retorno string al método.

Para ver mejor lo que ocurre, hagamos nuestra cosa favorita ydd($targetEntity, $targetTableAlias).

... lines 1 - 9
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
dd($targetEntity, $targetTableAlias);
}
... lines 14 - 15

Activar el filtro

Pero... cuando nos dirigimos y actualizamos la página... ¡no pasa nada! A diferencia de otras cosas, los filtros no se activan automáticamente con sólo crear la clase. Activarlo es un proceso de dos pasos.

En primer lugar, en config/packages/doctrine.yaml, tenemos que decirle a Doctrine que el filtro existe. En cualquier lugar directamente debajo de la clave orm, añade filters y luegofortuneCookie_discontinued. Esa cadena puede ser cualquier cosa... y verás cómo la utilizamos en un minuto. Pon esto en la clase: App\Doctrine\DiscontinuedFilter.

doctrine:
... lines 2 - 7
orm:
... lines 9 - 18
filters:
fortuneCookie_discontinued: App\Doctrine\DiscontinuedFilter
... lines 21 - 47

Muy fácil.

Esto ya está registrado en Doctrine... pero como puedes ver aquí, todavía no se llama. El segundo paso es activarlo donde quieras. En algunos casos, puede que quieras que este DiscontinuedFilter se utilice en una sección de tu sitio, pero no en otra.

Abre el controlador... allá vamos... dirígete a la página principal y autoconectaEntityManagerInterface $entityManager. Luego, justo encima, di$entityManager->getFilters() seguido de ->enable(). Luego pásale la misma clave que usamos en doctrine.yaml - fortuneCookie_discontinued. Ve a cogerla... y pégala.

... lines 1 - 13
class FortuneController extends AbstractController
{
... line 16
public function index(Request $request, CategoryRepository $categoryRepository, EntityManagerInterface $entityManager): Response
{
$entityManager->getFilters()
->enable('fortuneCookie_discontinued');
... lines 21 - 30
}
... lines 32 - 48
}

Con un poco de suerte, todas las consultas que hagamos después de esta línea utilizarán ese filtro. Dirígete a la página principal y... ¡sí! ¡Ha dado en el clavo!

Y ¡woh! Este ClassMetadata es un gran objeto que lo sabe todo sobre nuestra entidad. Aquí abajo, aparentemente, para cualquier consulta que estemos haciendo primero, el alias de la tabla -el alias que se está utilizando en la consulta- es c0_. ¡De acuerdo! ¡Manos a la obra!

Añadir el filtro Logoc

Como ya he dicho, esto se llamará para cada consulta. Así que tenemos que tener cuidado de añadir nuestra cláusula WHERE sólo cuando estemos consultando galletas de la suerte. Para ello, di if $targetEntity->name !== FortuneCookie::class, thenreturn ''.

... lines 1 - 8
class DiscontinuedFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if ($targetEntity->getReflectionClass()->name !== FortuneCookie::class) {
return '';
}
... lines 16 - 17
}
}

Este método devuelve un string... y esa cadena se añade básicamente a una cláusula WHERE. En la parte inferior, return sprintf('%s.discontinued = false'), pasando $targetTableAlias por el comodín.

... lines 1 - 10
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
... lines 13 - 16
return sprintf('%s.discontinued = false', $targetTableAlias);
}
... lines 19 - 20

¿Listo para comprobarlo? En la página de inicio, el recuento de "Proverbios" debería pasar de 3 a 2. Y... ¡así es! Compruébalo en la consulta. ¡Sí! Tienet0.discontinued = false dentro de cada búsqueda de galletas de la fortuna. ¡Es increíble!

Pasar parámetros a los filtros

Una cosa complicada de estos filtros es que no son servicios. Así que no pueden tener un constructor... simplemente no está permitido. Si necesitamos pasarle algo -como alguna configuración- tenemos que hacerlo de otra manera. Por ejemplo, supongamos que a veces queremos ocultar las cookies descatalogadas... pero otras veces, queremos mostrar sólo las descatalogadas - a la inversa. Básicamente, queremos poder cambiar este valor de false a true.

Para ello, cambia esto a %s y rellénalo con $this->getParameter()... pasando una cadena que me estoy inventando: discontinued. Verás cómo se utiliza en un minuto.

... lines 1 - 10
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
... lines 13 - 16
return sprintf('%s.discontinued = %s', $targetTableAlias, $this->getParameter('discontinued'));
}
... lines 19 - 20

Ahora bien, normalmente no añado %s a mis consultas... porque eso puede permitir ataques de inyección SQL. En este caso, está bien, pero sólo porque el método getParameter()está diseñado para escapar del valor por nosotros. En cualquier otra situación, evítalo.

Si nos dirigimos y lo intentamos ahora... ¡obtendremos un error gigantesco! ¡Sí!

El parámetro 'discontinued' no existe.

¡Es cierto! En cuanto leas un parámetro, tienes que pasarlo cuando actives el filtro. Hazlo con ->setParameter('discontinued')... y digamosfalse.

... lines 1 - 13
class FortuneController extends AbstractController
{
... line 16
public function index(Request $request, CategoryRepository $categoryRepository, EntityManagerInterface $entityManager): Response
{
$entityManager->getFilters()
->enable('fortuneCookie_discontinued')
->setParameter('discontinued', false);
... lines 22 - 31
}
... lines 33 - 49
}

Si recargamos ahora... ¡funciona! ¿Qué ocurre si lo cambiamos portrue? Recarga de nuevo y... ¡sí! ¡El número ha cambiado! ¡Ya gobernamos!

Activar esto globalmente

Aunque... probablemente estés pensando:

Ryan, tío, sí, esto mola... pero ¿no puedo activar este filtro globalmente... sin necesidad de poner este código en cada controlador?

¡Por supuesto! Vuelve al controlador y comenta esto.

Al hacerlo, el número vuelve a ser 3. Para activarlo globalmente, vuelve a la configuración: vamos a complicarlo un poco más. Pon esto en una nueva línea, ponlo en class y luego pon enabled en true.

Y así de fácil, esto estará habilitado en todas partes... aunque aún podrías deshabilitarlo en controladores específicos. Ah, pero ya que tenemos el parámetro, también necesitamos parameters, con discontinued: false.

doctrine:
... lines 2 - 7
orm:
... lines 9 - 18
filters:
fortuneCookie_discontinued:
class: App\Doctrine\DiscontinuedFilter
enabled: true
parameters:
discontinued: false
... lines 25 - 51

Y... ¡ya está! Los filtros molan.

Lo siguiente: Hablemos de cómo utilizar el práctico operador IN con una consulta.

Leave a comment!

5
Login or Register to join the conversation
Markchicobaby Avatar
Markchicobaby Avatar Markchicobaby | posted hace 1 mes

Hi, really great explanation of filters, thanks. I got them working but I'm trying to filter by the current user, so that each table can be automatically filtered to show only records owned by that user. I can't work out how to send in a parameter value that changes (ie $this->getUser() ) and enable this globally. (I can get it working if I enable per-call but then I feel I may as well just add the WHERE clause to each query). I'm trying to make the application multi-tenant, but using global filters. What do you recommend?

Reply

Hey @Markchicobaby

IIRC these filters are the same services as any in the Symfony, so in theory, you can inject any service into the filter to get access to any parameter.

Cheers!

Reply
Markchicobaby Avatar
Markchicobaby Avatar Markchicobaby | sadikoff | posted hace 1 mes

But about half way in it says "Now, one tricky thing about these filters is that they are not services" so I didn't think that was possible.

Reply

bah... my bad =) sorry for that. Yeah, that is a pretty tricky situation. However, there still are some ways to go.

For example you can use request event

namespace App\EventSubscriber;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;

class DoctrineFilterSubscriber implements EventSubscriberInterface
{
    private $em;
    private $security;

    public function __construct(EntityManagerInterface $em, Security $security)
    {
        $this->em = $em;
        $this->security = $security;
    }

    public function onKernelRequest(RequestEvent $event)
    {
        if(!$user = $this->security->getUser())
        {
            return;
        }

        $filter = $this->em->getFilters()->enable('yourFilter');
        $filter->setParameter('user', $user->getId());
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::REQUEST => 'onKernelRequest',
        ];
    }
}

Sorry again for my inattention!

Cheers!

1 Reply
Markchicobaby Avatar
Markchicobaby Avatar Markchicobaby | sadikoff | posted hace 1 mes

OK cool I’ll give events a closer look. Thanks for the tip!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "beberlei/doctrineextensions": "^1.3", // v1.3.0
        "doctrine/doctrine-bundle": "^2.7", // 2.9.1
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.15.1
        "symfony/asset": "6.2.*", // v6.2.7
        "symfony/console": "6.2.*", // v6.2.10
        "symfony/dotenv": "6.2.*", // v6.2.8
        "symfony/flex": "^2", // v2.2.5
        "symfony/framework-bundle": "6.2.*", // v6.2.10
        "symfony/proxy-manager-bridge": "6.2.*", // v6.2.7
        "symfony/runtime": "6.2.*", // v6.2.8
        "symfony/twig-bundle": "6.2.*", // v6.2.7
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.10
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
        "symfony/maker-bundle": "^1.47", // v1.48.0
        "symfony/stopwatch": "6.2.*", // v6.2.7
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.10
        "zenstruck/foundry": "^1.22" // v1.32.0
    }
}
userVoice