gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
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.
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.
É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 false
para 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.
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.
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!
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.
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; | |
'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.
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!
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.
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!
Hey @weaverryan!
Somehow I got rid of that error... Here is the steps I did:
latest_recipes
query type. Everything was working now.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
latest_recipes
query type. Expected, I was getting the error. 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.
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. :)
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!
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' }
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!
// 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
}
}
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:
and into config/packages/netgen_layouts.yaml:
I don't know what is the problem... I need help. :)