gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
We have a Recipe
entity and, on the frontend, a page that lists the recipes. We also saw how easy it is to create a layout, which instantly makes parts of this page configurable.
But now, looking at the homepage, I'm wondering if we can add more complex blocks, beyond just text. Could we, for example, add a block that renders a list of recipes? Something similar to what we have here right now... except instead of adding this via a Twig block, it's added entirely via layouts by an admin user? And, to go further, could we even let the admin user choose which recipes to show here?
Totally! If the first big idea of Layouts is allowing Twig template blocks to be rearranged and mixed with dynamic content, then the second big idea is allowing pieces of existing content - like recipes from our database - to be embedded onto our page by admin users.
How? Edit the Homepage Layout. In the blocks on the left, check out this one called "Grid". Add that after our "Hero" Twig block. The Grid allows us to add individual items to it... which could be anything. But, I don't see a way to do that!
Ok, so we know that a lot of blocks, like titles, maps, markdown, etc can be added to our pages in layouts out-of-the-box with no extra setup work. But the purpose of some blocks - like List, Grid, and the Gallery blocks down here (which are just fancy grids that have JavaScript behavior attached to them) - is to render a collection of "items" that are loaded from somewhere else, like our local database, CMS, or even your Sylius store. The "things" or "items" we can add to these blocks are called "value types". And... we currently have zero. If this were a Sylius project, we could install the Sylius and Layouts integration and instantly be able to select products. The same is true if you're using Ibexa CMS.
So here's our next big goal: to add our Recipe
Doctrine entity as a "value type" in layouts so that we can create lists and grids containing recipes.
Step one to adding a value type is to tell Layouts about it in a config file. Over in config/packages/netgen_layouts.yaml
, very simply, say value_types
, and below that, doctrine_recipe
. This is the internal name of the value type, and we'll refer to it in a few places. Give it a human-friendly name
- Recipe
- and for now, set manual_items
to false
... and make sure that has an "s" on the end:
netgen_layouts: | |
... lines 2 - 3 | |
value_types: | |
doctrine_recipe: | |
name: Recipe | |
manual_items: false |
We'll talk about manual_items
more later, but it's easier to set this to false
to start.
Head over, refresh our layouts page (it's okay to reload it)... and check out our Grid block! There's a new "Collection type" field and "Manual collection" is our only option right now. So... this still doesn't seem to be working. I can't change this to anything else... and I also can't select items manually.
There are two ways to add items to one of these "collection" blocks. The first is a dynamic collection where we choose from a pre-made query. We could choose a "Most Popular" query that would query for the most popular recipes or a "latest recipes" query, to give two examples. The second way to choose items is manually: the admin user literally selects which they want from a list.
We're going to start with the first type: the dynamic collection. We don't see "Dynamic collection" as an option yet because we need to create one of those pre-made queries first. Those pre-made queries are called query_types
. We could, for example, create a query type for Recipe
called "Most Popular" and another one called "Latest".
How do we create those? Head back to the config file, add query_types
and below that, let's say latest_recipes
. Once again, this is just an "internal name". Also give it a human-readable name
: Latest Recipes
:
netgen_layouts: | |
... lines 2 - 8 | |
query_types: | |
latest_recipes: | |
name: 'Latest Recipes' |
So... what do we do now? If we head back and refresh... we get a very nice error that tells us what to do next:
Query type handler for
latest_recipes
query type does not exist.
It's trying to tell us that we need to build a class that represent this query type! Let's do it!
Over in the src/
directory, I'm going to create a new Layouts/
directory: we'll organize a lot of our custom Layouts stuff inside here. Then add a new PHP class called... how about LatestRecipeQueryTypeHandler
. Make this implement QueryTypeHandlerInterface
:
... 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 | |
} |
Then go to "Code Generate" (or Command
+N
on a Mac), and select "Implement methods" to add the four we need:
... 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. | |
} | |
} |
Nice! Let's see... I'll leave buildParameters()
empty for a minute, but we'll come back to it soon:
... lines 1 - 9 | |
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface | |
{ | |
... lines 12 - 15 | |
public function buildParameters(ParameterBuilderInterface $builder): void | |
{ | |
} | |
... lines 19 - 40 | |
} |
The most important method is getValues()
. This is where we'll load and return the "items". If our recipes were stored on an API, we would make an API request here to fetch those. But since they're in our local database, we'll query for them.
To do that, go to the top of the class, add a __construct()
method with private 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 | |
} |
Then, down in getValues()
, return $this->recipeRepository
... and use a method that I already created inside of RecipeRepository
called ->createQueryBuilderOrderedByNewest()
. Also add ->setFirstResult($offset)
and ->setMaxResults($limit)
. The admin user will be able to choose how many items to show and they can even skip some. And so, Layouts passes us those values as $limit
and $offset
... and we use them in our query. Finish with ->getQuery()
and ->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 | |
} |
Perfect! Below, for getCount()
, let's do the exact same thing... except we don't need ->setMaxResults()
or ->setFirstResult()
. Instead, add ->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 | |
} |
I'm using recipe
because, over in RecipeRepository
... if we look at the custom method, it uses recipe
as the alias in the query:
... lines 1 - 17 | |
class RecipeRepository extends ServiceEntityRepository | |
{ | |
... lines 20 - 42 | |
public function createQueryBuilderOrderedByNewest(string $search = null): QueryBuilder | |
{ | |
$queryBuilder = $this->createQueryBuilder('recipe') | |
... lines 46 - 53 | |
} | |
} |
After that, update ->getResult()
to be ->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 | |
} |
Phew! That was a bit of work, but fairly straightforward. Oh, and for isContextual()
, return false
:
... lines 1 - 9 | |
class LatestRecipeQueryTypeHandler implements QueryTypeHandlerInterface | |
{ | |
... lines 12 - 36 | |
public function isContextual(Query $query): bool | |
{ | |
return false; | |
} | |
} |
We won't need it, but this method is kinda cool. If you return true
, then you can read information from the current page to change the query - like if you were on a "category" page and needed to list only products in that category.
Anyways, that's it. This is now a functional query type handler! But if you go over and refresh... it still doesn't work. We get the same error. That's because we need to associate this query type handler class with the latest_recipes
query type in our config. To do that, we need to give the service a tag... and there's a really cool way to do this thanks to Symfony 6.1.
Above the class, add an attribute called #[AutoconfigureTag()]
. The name of the tag we need is netgen_layouts.query_type_handler
: this is straight out of the documentation. We also need to pass an array with a type
key set to latest_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 | |
} |
This type
must match what we have in our config:
netgen_layouts: | |
... lines 2 - 8 | |
query_types: | |
latest_recipes: | |
... lines 11 - 12 |
It ties the two together.
And now... the page works! If we click on our Grid block... we can switch to "Dynamic collection". Awesome! I'll hit Apply and... everything immediately stops loading!
When you have an error in the admin section, there's a good chance it'll show up via an AJAX call. Often, Layouts will show you the error in a modal. But if it doesn't, no worries: just look down here on the web debug toolbar. Yup! We have a 400 error.
Let's fix that next by creating a value converter. Then we'll make our query even smarter.
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. :)