Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Criterios: Filtrar colecciones de relaciones

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

En la página de presentación de categorías, hacemos un bucle sobre todas las galletas de la suerte de esa categoría. Veamos la plantilla: templates/fortune/showCategory.html.twig. Aquí está: hacemos un bucle sobre category.fortuneCookies y mostramos algunas cosas.

... lines 1 - 20
{% for fortuneCookie in category.fortuneCookies %}
<tr class="hover:bg-slate-200">
<td class="border p-4">
{{ fortuneCookie.fortune }}
</td>
<td class="border p-4">
{{ fortuneCookie.numberPrinted }} printed since {{ fortuneCookie.createdAt|date('M jS Y') }}
</td>
</tr>
{% endfor %}
... lines 31 - 40

Pero... hay un problema. Abre la entidad FortuneCookie. Tiene una banderabool $discontinued. De vez en cuando, tenemos que dejar de producir una galleta de la fortuna específica... por una razón u otra. Como aquella vez que teníamos una galleta de la suerte que decía "Serás feliz... hasta que te des cuenta de que la realidad es una ilusión". Esa se escapó del control de calidad. Cuando esto ocurre, ponemos discontinued en verdadero.

En este momento, hacemos un bucle con todas las galletas de la suerte de una categoría: ¡incluidas las galletas actuales y las descatalogadas! Pero a la dirección sólo le interesan las galletas de la suerte actuales. Necesitamos una forma de ocultar las descatalogadas. ¿Cómo podemos hacerlo?

En el controlador de esta página - FortuneController - podríamos crear una consulta independiente de $fortuneCookieRepository con

WHERE categoría = :categoría y descatalogado = false.

Pero... ¡eso es penoso! ¡Hacer un bucle sobre category.fortuneCookies es tan fácil! ¿Realmente necesitamos volver al controlador, crear una consulta personalizada y pasar los resultados como una nueva variable Twig? ¿No podríamos utilizar de algún modo el objeto category... pero filtrando las cookies descatalogadas? ¡Por supuesto! Y si lo hacemos correctamente, podemos hacerlo de forma realmente eficiente.

El primer paso es opcional, pero en el controlador, vuelve a cambiar->findWithFortunesJoin() por sólo ->find(). Hago esto -que elimina la unión- sólo para que sea más fácil ver el resultado final de lo que vamos a hacer.

... lines 1 - 12
class FortuneController extends AbstractController
{
... lines 15 - 30
public function showCategory(int $id, CategoryRepository $categoryRepository, FortuneCookieRepository $fortuneCookieRepository): Response
{
$category = $categoryRepository->find($id);
... lines 34 - 44
}
}

Hacer esto no cambia la página... excepto que nuestras consultas pasan a ser tres. Es decir, una consulta para el Category, nuestra consulta personalizada que estamos haciendo, y luego una consulta para todas las fortunas dentro de este Category.

Añadir un método de entidad personalizado para las cookies descatalogadas

Recuerda el objetivo: queremos poder llamar a algo en el objeto Category para recuperar las galletas de la fortuna relacionadas... pero ocultando las descatalogadas.

Abre la entidad Category y busca getFortuneCookies(). Ahí lo tienes. A continuación, añade un nuevo método llamado getFortuneCookiesStillInProduction(). Éste, al igual que el método normal, devolverá una Doctrine Collection. Y... sólo para ayudar a mi editor, copia el documento @return anterior para decir que se trata de un Collection de objetosFortuneCookie.

... lines 1 - 10
class Category
{
... lines 13 - 60
/**
* @return Collection<int, FortuneCookie>
*/
public function getFortuneCookiesStillInProduction(): Collection
{
}
... lines 68 - 97
}

Entonces... ¿qué hacemos dentro? Podríamos hacer un bucle sobre$this->fortuneCookies as $fortuneCookie y crear una matriz de objetos que no estén discontinuados. ¡Fácil!

Pero... en cuanto empecemos a trabajar con $this->getFortuneCookies(), eso hará que Doctrine consulte cada galleta de la suerte relacionada. ¿Ves el problema? Puede que estemos pidiendo a Doctrine que consulte y prepare 100 objetos FortuneCookie... aunque esta colección final $inProduction sólo contenga 10. ¡Qué desperdicio!

Lo que realmente queremos hacer es decirle a Doctrine que cuando realice la consulta de las galletas de la suerte relacionadas, añada un WHERE discontinued = falseextra a esa consulta.

Hola Criterios

Pero... ¿cómo demonios lo hacemos? Doctrine realiza esa consulta de forma automática y... mágica en algún lugar en segundo plano. Aquí es donde resulta útil el sistema de criterios.

Funciona así $criteria = Criteria:: - el deDoctrine\Common\Collections - create().

... lines 1 - 7
use Doctrine\Common\Collections\Criteria;
... lines 9 - 11
class Category
{
... lines 14 - 64
public function getFortuneCookiesStillInProduction(): Collection
{
$criteria = Criteria::create()
... lines 68 - 70
}
... lines 72 - 101
}

Este objeto es un poco como el de QueryBuilder, pero no exactamente igual. Podemos decir ->andWhere() y luego volver a utilizar Criteria:: con expr()->. Esteexpr() o "expresión" nos permite, más o menos, construir la cláusula WHERE. Tiene métodos como in, contains o gt para "mayor que". Queremos eq() para "igual a". Dentro, digamos discontinued, false.

... lines 1 - 64
public function getFortuneCookiesStillInProduction(): Collection
{
$criteria = Criteria::create()
->andWhere(Criteria::expr()->eq('discontinued', false));
... lines 69 - 70
}
... lines 72 - 103

Vale, esto, por sí mismo, sólo crea un objeto que "describe" una cláusula WHERE que podría añadirse a alguna otra consulta. Para utilizarlo,return $this->fortuneCookies->matching($criteria).

... lines 1 - 64
public function getFortuneCookiesStillInProduction(): Collection
{
... lines 67 - 69
return $this->fortuneCookies->matching($criteria);
}
... lines 72 - 103

Genial, ¿eh? Estamos diciendo:

¡Eh, Doctrine! Toma esta colección, pero devuelve sólo las que coincidan con este criterio.

Y como veremos dentro de un minuto, ¡esto modificará la consulta para obtener esas galletas de la suerte!

Para utilizar este método, en showCategory.html.twig, sustituye el buclecategory.fortuneCookies por category.fortuneCookiesStillInProduction.

... lines 1 - 2
{% block body %}
... lines 4 - 20
{% for fortuneCookie in category.fortuneCookiesStillInProduction %}
... lines 22 - 29
{% endfor %}
... lines 31 - 38
{% endblock %}

¡Vamos a hacerlo! Actualiza, y... En realidad, no sé si alguno de ellos está descatalogado, ¡pero pasó de tres a dos! ¿Y lo mejor? ¡Echa un vistazo a la consulta! Aquí está la primera para la categoría, aquí está la nuestra personalizada... pero fíjate en esta última consulta. Cuando preguntamos por las "galletas de la suerte aún en producción", consulta desde fortune_cookie, donde category = nuestra categoría y dondet0.discontinued ¡es falso! Así que ha hecho la consulta más eficiente para obtener sólo las galletas de la suerte que necesitamos. Es asombroso.

Organizar tu código de criterios en el repositorio

Ahora, un pequeño inconveniente es que... Normalmente me gusta mantener mi lógica de consulta dentro de un repositorio... no en medio de una entidad. Afortunadamente, podemos moverla allí.

Como esto trata de galletas de la suerte, abreFortuneCookieRepository y, en cualquier lugar, añade un nuevo public static function llamado... ¿qué tal createFortuneCookiesStillInProductionCriteria(). Esto devolverá un objetoCriteria.

Ahora, coge la declaración $criteria de la entidad... y devuélvela.

... lines 1 - 8
use Doctrine\Common\Collections\Criteria;
... lines 10 - 19
class FortuneCookieRepository extends ServiceEntityRepository
{
... lines 22 - 26
public static function createFortuneCookiesStillInProductionCriteria(): Criteria
{
return Criteria::create()
->andWhere(Criteria::expr()->eq('discontinued', false));
}
... lines 32 - 100
}

¿El método es estático?

Y sí, se trata de un método static... que no utilizo demasiado a menudo. Hay dos razones para ello. En primer lugar, estos objetos Criteria en realidad no hacen consultas... y no dependen de ningún dato o servicio. Por tanto, este método puede ser estático. En segundo lugar, y más importante, no tenemos acceso al objeto repositorio desde dentro de Category. Así que... si queremos llamar a un método de un repositorio, tiene que ser static. Esto es algo especial que suelo hacer en mis repositorios sólo para esta situación de criterios.

Volviendo a la entidad, digamos que $criteria es igual aFortuneCookieRepository::createFortuneCookiesStillInProductionCriteria().

... lines 1 - 5
use App\Repository\FortuneCookieRepository;
... lines 7 - 12
class Category
{
... lines 15 - 65
public function getFortuneCookiesStillInProduction(): Collection
{
$criteria = FortuneCookieRepository::createFortuneCookiesStillInProductionCriteria();
return $this->fortuneCookies->matching($criteria);
}
... lines 72 - 101
}

Centralización lógica, ¡comprobado! Ah, e incluso podemos reutilizar estos objetos Criteria dentro de un QueryBuilder. Veamos... No tengo un buen ejemplo... así que... en este método, arriba, hagamos de cuenta que estoy creando un QueryBuilder con$this->createQueryBuilder('fortune_cookie'). Para añadir los criterios es...->addCriteria(self::createFortuneCookiesStillInProduction()).

Así que, aunque el sistema de criterios es un poco diferente del QueryBuilder normal, podemos reutilizarlos en todas partes. Ah, y comprobemos que todo sigue funcionando. ¡Ya está!

Utilizar el sistema de criterios en el controlador + EXTRA_LAZY Fetch

En la página de inicio, tenemos un problema similar. Aquí dice "Proverbios(3)", pero si hacemos clic en él, aparecen dos. ¿Qué ocurre aquí? Enhomepage.html.twig... veamos... ah, sí. Estamos haciendo un bucle sobre categories, y luego llamando a category.fortuneCookies|length que, como sabemos, devuelve todas las galletas de la fortuna. Cámbialo a fortuneCookiesStillInProduction.

... lines 1 - 2
{% block body %}
... lines 4 - 7
{% for category in categories %}
... line 9
<span class="fa {{ category.iconKey }}"></span> <span class="font-bold text-lg">{{ category.name }}</span> ({{ category.fortuneCookiesStillInProduction|length }})
... lines 11 - 13
{% endfor %}
... line 15
{% endblock %}

De vuelta a la página de inicio, observa este "(3)". Debería bajar a 2, y... lo hace. Pero eso ni siquiera es lo mejor. Abre la consulta para ello. Recuerda que, gracias a nuestro fetch EXTRA_LAZY, como sólo estamos contando el número de galletas de la suerte, sabe hacer una consulta superrápida COUNT. Y gracias al sistema de criterios, está seleccionando COUNT FROM fortune_cookies WHERE la category = nuestra categoría ydiscontinued = false. ¡Vaya!

Siguiente: Queremos ocultar las galletas de la suerte descatalogadas de todas partes de nuestro sitio. ¿Hay alguna forma de engancharnos a Doctrine y añadir esa cláusula WHERE automáticamente... en todas partes? La hay. Se llama filtros.

Leave a comment!

0
Login or Register to join the conversation
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