Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Añadir listas: Tipo de valor

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Tenemos una entidad Recipe y, en el frontend, una página que enumera las recetas. También hemos visto lo fácil que es crear un diseño, que hace que partes de esta página sean configurables al instante.

¿Añadir listas de contenido existente a través de maquetas?

Pero ahora, viendo la página de inicio, me pregunto si podemos añadir bloques más complejos, más allá del simple texto. ¿Podríamos, por ejemplo, añadir un bloque que muestre una lista de recetas? ¿Algo similar a lo que tenemos aquí ahora... excepto que en lugar de añadirlo a través de un bloque Twig, se añade completamente a través de diseños por un usuario administrador? Y, para ir más lejos, ¿podríamos incluso dejar que el usuario administrador eligiera qué recetas mostrar aquí?

¡Totalmente! Si la primera gran idea de Layouts es permitir que los bloques de plantillas Twig se reorganicen y se mezclen con contenido dinámico, entonces la segunda gran idea es permitir que los usuarios administradores incrusten en nuestra página piezas de contenido existente, como las recetas de nuestra base de datos.

¿Cómo? Edita el diseño de la página de inicio. En los bloques de la izquierda, fíjate en este llamado "Rejilla". Añádelo después de nuestro bloque Twig "Héroe". La cuadrícula nos permite añadir elementos individuales a ella... que podrían ser cualquier cosa. Pero, ¡no veo la forma de hacerlo!

Vale, sabemos que muchos bloques, como los títulos, los mapas, el markdown, etc., pueden añadirse a nuestras páginas en los diseños de forma inmediata, sin ningún trabajo de configuración adicional. Pero el propósito de algunos bloques -como el de Lista, el de Cuadrícula y el de Galería aquí abajo (que no son más que cuadrículas extravagantes que tienen un comportamiento de JavaScript asociado a ellas)- es representar una colección de "elementos" que se cargan desde otro lugar, como nuestra base de datos local, el CMS o incluso tu tienda Sylius. Las "cosas" o "elementos" que podemos añadir a estos bloques se llaman "tipos de valores". Y... actualmente tenemos cero. Si se tratara de un proyecto de Sylius, podríamos instalar la integración de Sylius y Layouts y al instante podríamos seleccionar productos. Lo mismo ocurre si utilizas Ibexa CMS.

Añadir un tipo de valor

Éste es nuestro siguiente gran objetivo: añadir nuestra entidad Doctrine Recipe como "tipo de valor" en los diseños para poder crear listas y cuadrículas que contengan recetas.

El primer paso para añadir un tipo de valor es informar a Layouts sobre él en un archivo de configuración. En config/packages/netgen_layouts.yaml, de forma muy sencilla, di value_types, y debajo, doctrine_recipe . Este es el nombre interno del tipo de valor, y nos referiremos a él en algunos lugares. Dale un nombre amigable para los humanos name -Recipe - y por ahora, pon manual_items a false... y asegúrate de que tiene una "s" al final:

netgen_layouts:
... lines 2 - 3
value_types:
doctrine_recipe:
name: Recipe
manual_items: false

Hablaremos más tarde de manual_items, pero es más fácil ponerlo en falsepara empezar.

Dirígete, actualiza nuestra página de diseños (no pasa nada por recargarla)... ¡y echa un vistazo a nuestro bloque Grid! Hay un nuevo campo "Tipo de colección" y "Colección manual" es nuestra única opción ahora mismo. Entonces... parece que esto sigue sin funcionar. No puedo cambiar esto por otra cosa... y tampoco puedo seleccionar elementos manualmente.

Consultas dinámicas vs manuales

Hay dos formas de añadir elementos a uno de estos bloques de "colección". La primera es una colección dinámica en la que elegimos a partir de una consulta preelaborada. Podríamos elegir una consulta "Más populares" que buscara las recetas más populares o una consulta "últimas recetas", por poner dos ejemplos. La segunda forma de elegir elementos es manual: el usuario administrador selecciona literalmente los que quiere de una lista.

Añadir un tipo de consulta

Vamos a empezar con el primer tipo: la colección dinámica. Todavía no vemos la opción "Colección dinámica" porque primero tenemos que crear una de esas consultas prefabricadas. Esas consultas prefabricadas se llaman query_types. Podríamos, por ejemplo, crear un tipo de consulta para Recipe llamado "Más popular" y otro llamado "Más reciente".

¿Cómo las creamos? Vuelve al archivo de configuración, añade query_types y debajo, digamos, latest_recipes. Una vez más, esto es sólo un "nombre interno". También dale un nombre legible para los humanos name: Latest Recipes:

netgen_layouts:
... lines 2 - 8
query_types:
latest_recipes:
name: 'Latest Recipes'

Entonces... ¿qué hacemos ahora? Si volvemos atrás y refrescamos... obtenemos un error muy bonito que nos dice qué hacer a continuación:

El gestor de tipos de consulta para el tipo de consulta latest_recipes no existe.

¡Está intentando decirnos que tenemos que construir una clase que represente este tipo de consulta! ¡Hagámoslo!

La clase manejadora del tipo de consulta

En el directorio src/, voy a crear un nuevo directorio Layouts/: aquí organizaremos muchas de nuestras cosas de Layouts personalizados. A continuación, añade una nueva clase PHP llamada... qué tal LatestRecipeQueryTypeHandler. Haz que esto implementeQueryTypeHandlerInterface:

... lines 1 - 2
namespace App\Layouts;
... lines 4 - 5
use Netgen\Layouts\Collection\QueryType\QueryTypeHandlerInterface;
... lines 7 - 8
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 11 - 29
}

Luego ve a "Generar código" (o Command+N en un Mac), y selecciona "Implementar métodos" para añadir los cuatro que necesitamos:

... lines 1 - 4
use Netgen\Layouts\API\Values\Collection\Query;
... line 6
use Netgen\Layouts\Parameters\ParameterBuilderInterface;
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
public function buildParameters(ParameterBuilderInterface $builder): void
{
// TODO: Implement buildParameters() method.
}
public function getValues(Query $query, int $offset = 0, ?int $limit = null): iterable
{
// TODO: Implement getValues() method.
}
public function getCount(Query $query): int
{
// TODO: Implement getCount() method.
}
public function isContextual(Query $query): bool
{
// TODO: Implement isContextual() method.
}
}

¡Bien! Veamos... Dejaré buildParameters() vacío por un momento, pero volveremos a él pronto:

... lines 1 - 9
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 12 - 15
public function buildParameters(ParameterBuilderInterface $builder): void
{
}
... lines 19 - 40
}

El método más importante es getValues(). Aquí es donde cargaremos y devolveremos los "artículos". Si nuestras recetas estuvieran almacenadas en una API, haríamos aquí una petición a la API para obtenerlas. Pero como están en nuestra base de datos local, las consultaremos.

Para ello, ve a la parte superior de la clase, añade un método __construct() conprivate RecipeRepository $recipeRepository:

... lines 1 - 4
use App\Repository\RecipeRepository;
... lines 6 - 9
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
public function __construct(private RecipeRepository $recipeRepository)
{
}
... lines 15 - 40
}

A continuación, baja a getValues(), return $this->recipeRepository... y utiliza un método que ya he creado dentro de RecipeRepository llamado->createQueryBuilderOrderedByNewest(). Añade también ->setFirstResult($offset)y ->setMaxResults($limit). El usuario administrador podrá elegir cuántos elementos mostrar e incluso podrá saltarse algunos. Y así, Layouts nos pasa esos valores como $limit y $offset... y los utilizamos en nuestra consulta. Terminamos con->getQuery() y ->getResult():

... lines 1 - 9
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 12 - 19
public function getValues(Query $query, int $offset = 0, ?int $limit = null): iterable
{
return $this->recipeRepository->createQueryBuilderOrderedByNewest()
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
}
... lines 28 - 40
}

¡Perfecto! A continuación, para getCount(), vamos a hacer exactamente lo mismo... excepto que no necesitamos ->setMaxResults() ni ->setFirstResult(). En su lugar, añadimos->select('COUNT(recipe.id)'):

... lines 1 - 9
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 12 - 28
public function getCount(Query $query): int
{
return $this->recipeRepository->createQueryBuilderOrderedByNewest()
->select('COUNT(recipe.id)')
->getQuery()
... line 34
}
... lines 36 - 40
}

Estoy utilizando recipe porque, en RecipeRepository... si miramos el método personalizado, utiliza recipe como alias en la consulta:

... lines 1 - 17
class RecipeRepository extends ServiceEntityRepository
{
... lines 20 - 42
public function createQueryBuilderOrderedByNewest(string $search = null): QueryBuilder
{
$queryBuilder = $this->createQueryBuilder('recipe')
... lines 46 - 53
}
}

Después, actualiza ->getResult() para que sea ->getSingleScalarResult():

... lines 1 - 9
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 12 - 28
public function getCount(Query $query): int
{
return $this->recipeRepository->createQueryBuilderOrderedByNewest()
->select('COUNT(recipe.id)')
->getQuery()
->getSingleScalarResult();
}
... lines 36 - 40
}

¡Uf! Ha sido un poco de trabajo, pero bastante sencillo. Ah, y paraisContextual(), return false:

... lines 1 - 9
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 12 - 36
public function isContextual(Query $query): bool
{
return false;
}
}

No lo vamos a necesitar, pero este método es bastante chulo. Si devuelve true, puedes leer la información de la página actual para cambiar la consulta, como si estuvieras en una página de "categoría" y necesitaras listar sólo los productos de esa categoría.

Etiquetar la clase manejadora del tipo de consulta

De todos modos, eso es todo. ¡Esto es ahora un manejador de tipos de consulta funcional! Pero si vuelves a actualizarlo... sigue sin funcionar. Nos da el mismo error. Esto se debe a que tenemos que asociar esta clase de manejador de tipos de consulta con el tipo de consulta latest_recipes en nuestra configuración. Para ello, tenemos que dar una etiqueta al servicio... y hay una forma muy interesante de hacerlo gracias a Symfony 6.1.

Sobre la clase, añade un atributo llamado #[AutoconfigureTag()]. El nombre de la etiqueta que necesitamos es netgen_layouts.query_type_handler: está sacado de la documentación. También necesitamos pasar un array con una clave type establecida enlatest_recipes:

... lines 1 - 8
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('netgen_layouts.query_type_handler', ['type' => 'latest_recipes'])]
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface
{
... lines 14 - 42
}

Este type debe coincidir con lo que tenemos en nuestra configuración:

netgen_layouts:
... lines 2 - 8
query_types:
latest_recipes:
... lines 11 - 12

Esto une a los dos.

Y ahora... ¡la página funciona! Si hacemos clic en nuestro bloque Grid... podemos cambiar a "Colección dinámica". ¡Espectacular! Le doy a Aplicar y... ¡todo deja de cargarse inmediatamente!

Cuando tengas un error en la sección de administración, es muy probable que aparezca a través de una llamada AJAX. A menudo, los diseños te mostrarán el error en un modal. Pero si no lo hace, no te preocupes: sólo tienes que mirar aquí abajo en la barra de herramientas de depuración web. ¡Sí! Tenemos un error 400.

Vamos a solucionarlo creando un convertidor de valores. Luego haremos nuestra consulta aún más inteligente.

Leave a comment!

9
Login or Register to join the conversation

Hey,
I am getting the same error "Query type handler for "latest_recipes" query type does not exist." even when I add into src/Layouts/LatestRecipeQueryTypeHandler.php:

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('netgen_layouts.query_type_handler', ['type' => 'latest_recipes'])]

and into config/packages/netgen_layouts.yaml:

netgen_layouts:
  pagelayout: 'base.html.twig'

  value_types:
    doctrine_recipe:
      name: Recipe
      manual_items: false

  query_types:
    latest_recipes:
        name: Latest Recipes
        

I don't know what is the problem... I need help. :)

Reply

Hey @StjepanPetrovic!

Hmm. To debug, let's first ask our app to show us all of the services that have the netgen_layouts.query_type_handler tag on it:

php bin/console debug:container --tag=netgen_layouts.query_type_handler

In my app, I see something like this:

 -------------------------------------------------------------------- ----------------------- ------------------------------------------------------------------------------------
  Service ID                                                           type                    Class name
 -------------------------------------------------------------------- ----------------------- ------------------------------------------------------------------------------------
  App\Layouts\LatestRecipeQueryTypeHandler                             latest_recipes          App\Layouts\LatestRecipeQueryTypeHandler
   (same service as previous, another tag)
  netgen_layouts.contentful.collection.query_type_handler.search       contentful_search       Netgen\Layouts\Contentful\Collection\QueryType\Handler\ContentfulSearchHandler
  netgen_layouts.contentful.collection.query_type.handler.references   contentful_references   Netgen\Layouts\Contentful\Collection\QueryType\Handler\ContentfulReferencesHandler
 -------------------------------------------------------------------- ----------------------- ------------------------------------------------------------------------------------

Do you see your service there? If not is your service being autowired (most are unless you have some special setup)? Does that AutoconfigureTag class exist in your app (I'm making sure your Symfony version is new enough for this).

Cheers!

Reply

Hey @weaverryan, here I am!

When I run: php bin/console debug:container --tag=netgen_layouts.query_type_handler I got the same error:
Query type handler for "latest_recipes" query type does not exist., so I don't see service here.

Also, when I run: php bin/console cache:clear I got the same error.

I have taken your starting source code and I am following steps in order like in your tutorials - you can check my code on my Github.
AutoconfigureTag exists in my app - my Symfony version is 5.4.22 and Symfony CLI version is 5.5.2.

Reply

Hey @StjepanPetrovic!

Ah, my apologies - I see now that the error comes from during container compilation! So you're right that even the debugging tools won't work to help us :). You can try this instead:

A) Comment-out the latest_recipes query type. Then verify that running bin/console cache:clear once again works. We're temporarily removing the problem so that our debugging tools work.

B) NOW try php bin/console debug:container --tag=netgen_layouts.query_type_handler. Do you see your service in this list?

You can also try running php bin/console debug:container LatestRecipeQueryTypeHandler - that should find your service (we're not giving the full service name, but it should search and find your service). Do you see it? And does the service definition have any tags on it?

Let me know!

Cheers!

Reply

Hey @weaverryan!

Somehow I got rid of that error... Here is the steps I did:

  • Like you said, I commented-out the latest_recipes query type. Everything was working now.
  • Like you said, I run php bin/console debug:container --tag=netgen_layouts.query_type_handler but I didn't saw my service in the list. Also I didn't find my servie with php bin/console debug:container LatestRecipeQueryTypeHandler
  • I uncomment the latest_recipes query type. Expected, I was getting the error.
  • Then I tried tagging the Query Type Handler Class on different way which I saw here in comment by @Tasatko. In config/services.yaml I added this code below:
      class: 'App\Layouts\LatestRecipeQueryTypeHandler'
      tags:
        - { type: 'latest_recipes', name: 'netgen_layouts.query_type_handler' }
    

    and in src/Layouts/LatestRecipeQueryTypeHandler.php I commented-out this:

    use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
    #[AutoconfigureTag('netgen_layouts.query_type_handler', ['type' => 'latest_recipes'])]
    

    After adding this I was not getting the error, everything was working and when I clicked on Grid block I could chose between manual or dynamic collection.

  • Because I wanted to do tagging with newest way, I again tried uncommenting:
    use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
    #[AutoconfigureTag('netgen_layouts.query_type_handler', ['type' => 'latest_recipes'])]
    

    and commenting-out:

      class: 'App\Layouts\LatestRecipeQueryTypeHandler'
      tags:
        - { type: 'latest_recipes', name: 'netgen_layouts.query_type_handler' }
    

aaaand in some reason everythink is working. I don't know what was the problem. :)

Reply

Haha, wow. That was very procedural to get that going! My best guess is that you had some phantom caching issue. When you create a new file/class, Symfony's container should notice the new class and rebuild the container so that the new class can be registered as a service. For some reason, I think that wasn't happening for you. By updating services.yaml, that triggered a container rebuild... where it probably, finally, noticed your new class/service. So, later, when you re-added the #[AutoconfigureTag] attribute, now it saw that and applied it.

Just a guess - I wouldn't worry too much about it - glad it's working!

Cheers!

1 Reply

Wow. Thanks to your lessons, I learn a lot about the Symfony itself. Some feature adds like year ago, but I didn't knew about.

#[AutoconfigureTag('netgen_layouts.query_type_handler', ['type' => 'latest_recipes'])]

instead of:

  LatestRecipeQueryTypeHandler:
    class: 'App\Layouts\LatestRecipeQueryTypeHandler'
    tags:
      - { type: 'latest_recipes', name: 'netgen_layouts.query_type_handler' }
Reply

Haha, I know, right? These features have been coming so fast and steady that it's hard to keep up (even for me, and it's my job). I LOVE AutoconfigureTag

Cheers!

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.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.13.3
        "easycorp/easyadmin-bundle": "^4.4", // v4.4.1
        "netgen/layouts-contentful": "^1.3", // 1.3.2
        "netgen/layouts-standard": "^1.3", // 1.3.1
        "pagerfanta/doctrine-orm-adapter": "^3.6",
        "sensio/framework-extra-bundle": "^6.2", // v6.2.8
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/console": "5.4.*", // v5.4.14
        "symfony/dotenv": "5.4.*", // v5.4.5
        "symfony/flex": "^1.17|^2", // v2.2.3
        "symfony/framework-bundle": "5.4.*", // v5.4.14
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "5.4.*", // v5.4.6
        "symfony/runtime": "5.4.*", // v5.4.11
        "symfony/security-bundle": "5.4.*", // v5.4.11
        "symfony/twig-bundle": "5.4.*", // v5.4.8
        "symfony/ux-live-component": "^2.x-dev", // 2.x-dev
        "symfony/ux-twig-component": "^2.x-dev", // 2.x-dev
        "symfony/validator": "5.4.*", // v5.4.14
        "symfony/webpack-encore-bundle": "^1.15", // v1.16.0
        "symfony/yaml": "5.4.*", // v5.4.14
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "5.4.*", // v5.4.11
        "symfony/maker-bundle": "^1.47", // v1.47.0
        "symfony/stopwatch": "5.4.*", // v5.4.13
        "symfony/web-profiler-bundle": "5.4.*", // v5.4.14
        "zenstruck/foundry": "^1.22" // v1.22.1
    }
}
userVoice