If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOnce 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 | |
} | |
} |
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.
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?
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!
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?
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.
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!
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?
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!
// 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
}
}
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 :)