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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.8", // 1.8.2
        "doctrine/doctrine-bundle": "^2.1", // 2.1.0
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.1
        "doctrine/orm": "^2.7", // v2.7.3
        "knplabs/knp-markdown-bundle": "^1.8", // 1.8.1
        "knplabs/knp-time-bundle": "^1.11", // v1.12.0
        "sensio/framework-extra-bundle": "^5.5", // v5.6.1
        "sentry/sentry-symfony": "^3.4", // 3.5.2
        "stof/doctrine-extensions-bundle": "^1.4", // v1.4.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.9.0
        "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.7.3
        "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.3.1
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.20.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.1.0
    }
}