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 SubscribeInside HttpKernel
, we now have the controller. But before we run around excitedly and try to call that controller... we need to figure out what arguments to pass to it.
To help see this clearly, in ArticleController::show()
, we need to make one small change. Instead of having an argument type-hinted with Article
and allowing Symfony to automatically query for it by the slug, let's temporarily do this manually. Remove that argument and replace it with $slug
. Now add another arg: ArticleRepository $articleRepository
so that we can make the query: $article = $articleRepository->findOneBy(['slug' => $slug])
. And then, if not $article
, throw $this->createNotFoundException()
.
... lines 1 - 13 | |
class ArticleController extends AbstractController | |
{ | |
... lines 16 - 45 | |
public function show($slug, SlackClient $slack, ArticleRepository $articleRepository) | |
{ | |
$article = $articleRepository->findOneBy(['slug' => $slug]); | |
if (!$article) { | |
throw $this->createNotFoundException(); | |
} | |
... lines 53 - 60 | |
} | |
... lines 62 - 74 | |
} |
Functionally, this is identical to what we had before... but it will help us with our deep-dive. By the way, we know that this createNotFoundException()
line will result in a 404 page. If you hold Command or Ctrl and click into that method, it returns a NotFoundHttpException
. So... for some reason, this specific exception maps to a 404... while most other exceptions will cause a 500 page. By the end of this tutorial, we'll know exactly why this happens.
Go back to HttpKernel
. Now that we've figured out what the controller is, the next thing that happens is... we dispatch another event! This one is called KernelEvents::CONTROLLER
, which maps to the string kernel.controller
.
So let's look at everything we've done so far: we dispatched an event, found the controller, then dispatched another event. That's all.
There are no particularly important listeners to this event, from the perspective of how the framework operates. Refresh the article show page... and click to open the profiler. Go to the Events tab... and find kernel.controller
.
In this app, there are 6 listeners... but nothing critical. A few of them come from FrameworkExtraBundle
- a bundle that gives us a lot of magic shortcuts. These rely heavily on listeners... and we'll talk about how some of them work later.
One of the things that a listener to this event can do is change the controller. It's not very common, but you can see it down here: $controller = $event->getController()
. Hold Command or Ctrl to open the ControllerEvent
class. Here it is: a listener can call setController()
to completely change the controller to some other callable.
Ok, back in HttpKernel after the kernel.controller
"hook point", this next line is the missing piece: we need to know what arguments we should pass when we call the controller. To figure that out, it uses something called the "argument resolver". And it's pretty cool... we call getArguments()
, pass it the $request
and $controller
and it - somehow - figures out all the arguments that this controller should be passed.
Ok, you know the drill: let's open this thing up and see how it works! This time, the class is simple: I'll hit Shift+Shift and open a file called ArgumentResolver.php
. Find the getArguments()
method.
Okay, interesting. It first uses a foreach
to loop over $this->argumentMetadataFactory->createArgumentMetadata()
as $metadata
. This is actually looping over each argument to the controller function. So for the show page, this would loop 3 times: once for each argument.
Then, inside that loop, it does another: it does a foreach
over something called argumentValueResolvers
. Let's see what's going on here. Inside the first loop, dd()
the $metadata
variable: this should be something that, sort of, represents a single argument.
... lines 1 - 27 | |
final class ArgumentResolver implements ArgumentResolverInterface | |
{ | |
... lines 30 - 45 | |
public function getArguments(Request $request, callable $controller): array | |
{ | |
... lines 48 - 49 | |
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) { | |
dd($metadata); | |
... lines 52 - 81 | |
} | |
... lines 83 - 84 | |
} | |
... lines 86 - 96 | |
} |
Move over and refresh. Huh. Apparently this is an ArgumentMetadata
object, which holds the name of the argument - slug
... because that's the name of the first argument to the controller. It also holds the argument type
, which in this case is null
. For the second argument it would be SlackClient
. It has some other stuff too: like if the argument has a defaultValue
or isNullable
.
That's... really cool! It's all the metadata about that one argument. The next question is: what does this function do with that metadata?
Clear out the dd()
. Let's figure out what these $argumentValueResolvers
are. This argument is actually an iterator - it has an iterable
type... which is not important... except that we need to get fancy to see what's inside. dd(iterator_to_array($this->argumentValueResolvers))
.
... lines 1 - 27 | |
final class ArgumentResolver implements ArgumentResolverInterface | |
{ | |
... lines 30 - 45 | |
public function getArguments(Request $request, callable $controller): array | |
{ | |
... lines 48 - 49 | |
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) { | |
dd(iterator_to_array($this->argumentValueResolvers)); | |
... lines 52 - 81 | |
} | |
... lines 83 - 84 | |
} | |
... lines 86 - 96 | |
} |
Move over and... 8 items! Each object is being decorated by a TraceableValueResolver
. But if you look inside - I'll expand a few of these - you'll see the true object: RequestAttributeValueResolver
, RequestValueResolver
, and a SessionValueResolver
. These are the objects that figure out which value to pass to each controller argument.
Another way to see this - since this is a deep-dive tutorial - is to find your terminal and run:
php bin/console debug:container --tag=controller.argument_value_resolver
Because if you want to create your own argument value resolver - we'll do that later - you need to create a service and give it this tag. This gives us the same list - but it's a bit easier to see the true names: some request_attribute
resolver, request
resolver, session
resolver, user_value_resolver
and more.
We're going to walk through some of the most important value resolvers next.
But before we do, let's go back and... see how this system works! We loop over each argument... and then loop again over every argument value resolver and call a supports()
method on each. So, one-by-one, we're asking each argument value resolver:
Hey! Do you know what value to pass for a
$slug
argument with no type-hint?
Or, on the next loop,
Yo! Do you know what value to pass to a
$slack
argument with aSlackClient
type-hint?
If an argument resolver returns false
from supports()
, then it continues onto the next one. If it returns true
, it then calls $resolver->resolve()
to get the value.
So - hopefully - by the end of looping through all the argument value resolvers, one of them has figured out what value to pass to the argument.
Next, let's open up the most important argument value resolvers and figure out what they do. This will answer a cool question: what are all the possible arguments that a controller is allowed to have... and why?
"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
}
}