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 SubscribeIn ArticleController::show()
we're using the $slug
argument to manually query for the Article
object and triggering a 404 page if needed. But originally this code looked different: instead of a $slug
argument, we had an $article
argument type-hinted with the Article
entity class. We didn't need to make the query because something else was doing it for us. The same is true for the 404 logic.
I love this feature! But the question for us is: how does this work? What is allowing us to have this $article
argument. Something is noticing that we're type-hinting an argument with an entity class and is automatically querying for it based on the {slug}
wildcard. But where is that code?
At first, you might think this is another argument value resolver. But, there's nothing in that list that mentions "doctrine" or "entity". In reality, this is working via a different system.
If we refresh now, the page still works. But if you change the URL to a slug that won't be found, the error gives us a hint:
App\Entity\Article
object not found by the@ParamConverter
annotation
This error is coming from a class called DoctrineParamConverter
, which if you look further down the stack trace, is called by some ParamConverterListener
.
Let's start by checking out that class. I'll hit Shift + Shift to open ParamConverterListener.php
. Ah - this class comes from SensioFrameworkExtraBundle
. The first thing I want you to notice is that this implements EventSubscriberInterface
. Yep! This is an event listener and it listens to the kernel.controller
event: the event that's dispatched after the controller is determined, but before the controller is called. And also before the arguments are determined.
That makes sense! If this class is going to do some magic on the arguments to our controller, it's going to need to know which controller is about to be called so it can look at its arguments.
We're not going to study the details of this class too closely, but we can the basic idea pretty easily. This function loops over all of the parameters - all of the arguments of the controller. That $param
is a ReflectionParameter
which holds info about that argument. Most importantly, it knows what class the argument is type-hinted with.
Anyways, that method collects info about each argument and then, eventually - if you look at the stacktrace - it executes something called DoctrineParamConverter
. That's where the magic happens. I'll hit Shift + Shift to open that file: DoctrineParamConverter.php
.
Hmm. This starts by getting the $class
: that's the type-hint on the argument. Then, it tries different ways of querying for that. For example, $this->find()
tries to use the id
: it tries to see if the primary key is a wildcard in the URL, gets the entity manager for that class and ultimately tries to call a method to make that query.
This feature has a lot of options and can query for your entity in a lot of different ways. But we're not going to get into the weeds about all of that now.
The really cool part is that - no matter how it finds your entity - if we follow the logic to the bottom of apply()
, eventually it takes that Article
object and sets it onto the $request->attributes
! The $name
variable is the name of the argument - article
for us - and $object
will be the full entity that was fetched from the database.
SensioFrameworkExtraBundle is full of magic like this and all that magic works via listeners. If you want to know more about how one of its features works, find the listener that it's using. Oh, and if you're wondering why this DoctrineParamConverter
doesn't just use the "argument value resolver" system, the answer is that it pre-dates it. It may, some day, be converted to use it.
Next, let's start talking about a fascinating topic that we've already seen a few times. I want to talk about sub requests.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-iconv": "*",
"antishov/doctrine-extensions-bundle": "^1.4", // v1.4.3
"aws/aws-sdk-php": "^3.87", // 3.133.20
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2.0", // 2.2.3
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.2.2
"doctrine/orm": "^2.5.11", // 2.8.2
"easycorp/easy-log-handler": "^1.0", // v1.0.9
"http-interop/http-factory-guzzle": "^1.0", // 1.0.0
"knplabs/knp-markdown-bundle": "^1.7", // 1.9.0
"knplabs/knp-paginator-bundle": "^5.0", // v5.4.2
"knplabs/knp-snappy-bundle": "^1.6", // v1.7.1
"knplabs/knp-time-bundle": "^1.8", // v1.16.0
"league/flysystem-aws-s3-v3": "^1.0", // 1.0.24
"league/flysystem-cached-adapter": "^1.0", // 1.0.9
"league/html-to-markdown": "^4.8", // 4.9.1
"liip/imagine-bundle": "^2.1", // 2.5.0
"oneup/flysystem-bundle": "^3.0", // 3.7.0
"php-http/guzzle6-adapter": "^2.0", // v2.0.2
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^5.1", // v5.6.1
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.9", // v1.17.5
"symfony/form": "5.0.*", // v5.0.11
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/mailer": "5.0.*", // v5.0.11
"symfony/messenger": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.5", // v3.6.0
"symfony/property-access": "5.0.*|| 5.1.*", // v5.1.11
"symfony/property-info": "5.0.*|| 5.1.*", // v5.1.10
"symfony/routing": "5.1.*", // v5.1.11
"symfony/security-bundle": "5.0.*", // v5.0.11
"symfony/sendgrid-mailer": "5.0.*", // v5.0.11
"symfony/serializer": "5.0.*|| 5.1.*", // v5.1.10
"symfony/twig-bundle": "5.0.*", // v5.0.11
"symfony/validator": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.4", // v1.11.1
"symfony/yaml": "5.0.*", // v5.0.11
"twig/cssinliner-extra": "^2.12", // v2.14.3
"twig/extensions": "^1.5", // v1.5.4
"twig/extra-bundle": "^2.12|^3.0", // v3.3.0
"twig/inky-extra": "^2.12", // v2.14.3
"twig/twig": "^2.12|^3.0" // v2.14.4
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.4.0
"fakerphp/faker": "^1.13", // v1.13.0
"symfony/browser-kit": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/maker-bundle": "^1.0", // v1.29.1
"symfony/phpunit-bridge": "5.0.*", // v5.0.11
"symfony/stopwatch": "^5.1", // v5.1.11
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/web-profiler-bundle": "^5.0" // v5.0.11
}
}