Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Automatic Controller Queries: Param Converter

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 $12.00

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

Login Subscribe

Once again, I have a confession: I've still be making us do too much work. Dang!

Head over to QuestionController and find the show() action. Instead of manually querying for the Question object via findOneBy(), Symfony can make that query for us automatically.

... lines 1 - 13
class QuestionController extends AbstractController
{
... lines 16 - 72
/**
* @Route("/questions/{slug}", name="app_question_show")
*/
public function show($slug, EntityManagerInterface $entityManager)
{
... lines 78 - 81
$repository = $entityManager->getRepository(Question::class);
/** @var Question|null $question */
$question = $repository->findOneBy(['slug' => $slug]);
... lines 85 - 98
}
}

Automatic Queries

Here's how: replace the $slug argument with Question $question. The important thing here is not the name of the argument, but the type-hint: we're type-hinting the argument with an entity class.

And... we're done! Symfony will see the type-hint and automatically query for a Question object WHERE slug = the {slug} route wildcard value.

This means that we don't need any of the repository logic down here... or even the 404 stuff. I explain why in a minute. We can also delete my EntityManagerInterface argument... and, actually, we haven't needed this MarkdownHelper argument for awhile.

... lines 1 - 13
class QuestionController extends AbstractController
{
... lines 16 - 72
/**
* @Route("/questions/{slug}", name="app_question_show")
*/
public function show(Question $question)
{
if ($this->isDebug) {
$this->logger->info('We are in debug mode!');
}
$answers = [
'Make sure your cat is sitting `purrrfectly` still ?',
'Honestly, I like furry shoes better than MY cat',
'Maybe... try saying the spell backwards?',
];
return $this->render('question/show.html.twig', [
'question' => $question,
'answers' => $answers,
]);
}
}

Before we chat about what's going on, let's try it. Refresh the homepage, then click into one of the questions. Yes! It works! You can even see the query in the web debug toolbar. It's exactly what we expect: WHERE slug = that slug.

How... Does this Work?

This magic is actually provided by a bundle that we already have installed called SensioFrameworkExtraBundle. When that bundle sees a controller argument that's type-hinted with an entity class, it tries to query for that entity automatically by using all of the wildcard values.

... lines 1 - 13
class QuestionController extends AbstractController
{
... lines 16 - 72
/**
* @Route("/questions/{slug}", name="app_question_show")
*/
public function show(Question $question)
{
... lines 78 - 91
}
}

So this works because our wildcard is called slug, which exactly matches the property name. Quite literally this makes a query where slug equals the {slug} part of the URL. If we also had an {id} wildcard in the URL, then the query would be WHERE slug = {slug} AND id = {id}.

It even handles the 404 for us! If we add foo to the slug in the URL... we still get a 404!

This feature is called a param converter and I freakin' love it. But it doesn't always work. If you have a situation where you need a more complex query... or maybe for some reason the wildcard can't match your property name... or you have an extra wildcard that is not meant to be in the query, then this won't work. Well, there is a way to get it to work - but I don't think it's worth the trouble.

And... that's fine! In those cases, just use your repository object to make the query like you normally would. The param converter is an awesome shortcut for the most common cases.

Next: let's add some voting to our question. When we do that, we're going to look closer at the methods inside of the Question entity, which right now, are just getter and setter methods. Are we allowed to add our own custom methods here? And if so, when should we?

Leave a comment!

7
Login or Register to join the conversation
Hugo N. Avatar
Hugo N. Avatar Hugo N. | posted 2 years ago

Completely bluffed by this Param Converter possibilities. Is it not more demanding though, performance-wise, than to do a simple query on the slug ? I don't really know what's behind, but I am curious :)

1 Reply

Hey Hugo,

ParamConverter do the same thing as you why do it manually: takes the slug, get the repository, and find the entity by slug throwing an 404 exception if not found. As a result, when you use this param converter feature - you write less code, and so your controller remains more clean and easy to read. No any performance impact in theory, but you always can do some performance tests to know for sure ;)

Cheers!

1 Reply
Joel R. Avatar
Joel R. Avatar Joel R. | posted 1 year ago

Since the lessons for Symfony 6 hasn't got this far yet, I've been working through this batch using my Symfony 6 installation. Not too many issues with it but I notice @5:10, points 3 and 4 don't appear to be true anymore. Any idea why?

Reply
Jewel F. Avatar
Jewel F. Avatar Jewel F. | posted 2 years ago

I can't autowire

Question

entity. Shows error I have added below. Please help.. I am stucked

Cannot autowire argument $question of "App\Controller\QuestionController::show()": it references class "App\Entity\Question" but no such service exists.

Reply

Hey Jewel F.

You may be missing some configuration in your route. Check this post about how to use the param converter annotations https://symfony.com/doc/cur...

Cheers!

Reply
Facundo ariel P. Avatar
Facundo ariel P. Avatar Facundo ariel P. | posted 2 years ago | edited

I've found the ParamConverter to be a good solution when instead of typehinting with the entity class itslef, an interface is used as a type hint. So I wrote a param converter that follows the doctrine.orm.resolve_target_entities value.

Is there any other better way to do this?

Reply

Hey facundo,

Do your entities implement interfaces? :) Do you have a few entities that should have the same method signatures in it? If so, yes, it makes sense to use interfaces, but then how would you know which exactly entity to query if a few entities are implementing the same interface? :)

But every your entity implements a separate unique interface - then it's overkill, and you use interfaces incorrectly :)

It's difficult to imagine your setup for me, so difficult to advice something. But still, typehinting real entity class names is probably the best solution here, simple and robust.

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.1", // 2.1.1
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.7", // 2.8.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "stof/doctrine-extensions-bundle": "^1.4", // v1.5.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.5.0
    }
}
userVoice