Custom Global Controller Arguments

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

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

Login Subscribe

Now that we understand a lot more about its flow, we're on a mission to find weird, crazy things that we can do in Symfony. For the next one, pretend that, for some reason, we need to know whether or not a visitor is using a Mac or not. In fact, we need this info so often, that we want the ability to add an $isMac argument to any controller, like this.

Let's dump($isMac)... and then try it. No surprise, it explodes!

... lines 1 - 13
class ArticleController extends AbstractController
{
... lines 16 - 45
public function show($slug, SlackClient $slack, ArticleRepository $articleRepository, $isMac)
{
dump($isMac);
... lines 49 - 61
}
... lines 63 - 75
}

Controller show() requires that you provide a value for the $isMac argument.

I'll go back to a real article page, though that won't make any difference.

Custom Arguments Via Request Attributes

So: how can we make this work? There are actually two answers, and we're going to try both. The first is a, kind of, lower-level way of doing it. We know that if we have a {slug} route wildcard, we are allowed to have a $slug argument. So, in theory, if we had an {isMac} wildcard, we could have an $isMac argument, though that's not what we want.

But it's not really that we're allowed to have a $slug argument because there's a {slug} wildcard. Nope, we're allowed to have a $slug argument because there is a slug key in the $request->attributes. The router puts slug into attributes because of the wildcard, but when it comes to figuring out what arguments to pass to a controller, it's all about the $request->attributes.

Inside of our listener, let's say $isMac = stripos($userAgent, 'Mac') !== false. Now, to make isMac available as an argument to any controller, add $request->attributes->set('isMac', $isMac).

... lines 1 - 10
class UserAgentSubscriber implements EventSubscriberInterface
{
... lines 13 - 19
public function onKernelRequest(RequestEvent $event)
{
... lines 22 - 33
$isMac = stripos($userAgent, 'Mac') !== false;
$request->attributes->set('isMac', $isMac);
}
... lines 37 - 43
}

And... that's it! Try the page now. It works! And for me, it's set to true.

Custom ArgumentValueResolver

The second way to add a custom controller argument is a bit more direct: create a custom ArgumentValueResolver. When we were deep-diving into how Symfony determines what arguments to pass to a controller, we found out that there are various classes that determine this called "argument value resolvers". And we can create our own.

Inside of the src/ directory - it doesn't matter, let's put it in Service/ - create a new class called: IsMacArgumentValueResolver. The only rule is that this class must implement ArgumentValueResolveInterface. I'll go to the Code -> Generate menu - or Command + N on a Mac - and select "Implement Methods" to generate the two methods that we need.

... lines 1 - 4
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
class IsMacArgumentValueResolver implements ArgumentValueResolverInterface
{
public function supports(Request $request, ArgumentMetadata $argument)
{
// TODO: Implement supports() method.
}
public function resolve(Request $request, ArgumentMetadata $argument)
{
// TODO: Implement resolve() method.
}
}

Without doing anything else, this class is already being used by Symfony as an argument value resolver. When we talked about that system, I hinted that the way you get an argument value resolver into the system is by creating a service and tagging it with controller.argument_value_resolver. Find your terminal and, once again, run:

php bin/console debug:container --tag=controller.argument_value_resolver

And now... if you look at the service ids, one of them is for our App\Service\IsMacArgumentValueResolver. It's wrapped in another class because Symfony is decorating the services with TraceableValueResolver, but this is our service being used. Our new service already has the tag thanks to Symfony's auto-configuration feature.

Filling in the ArgumentValueResolver Logic

Let's go fill in the logic. Here's the plan: very simply, if the argument's name exactly matches $isMac, we'll fill in our value. So for supports(), return $argument->getName() === 'isMac'.

... lines 1 - 10
public function supports(Request $request, ArgumentMetadata $argument)
{
return $argument->getName() === 'isMac';
}
... lines 15 - 23

For resolve(), go grab the $userAgent code from the subscriber, paste it, and then also copy the stripos() logic. Delete the last two lines from the subscriber so that it stops setting this global argument.

... lines 1 - 15
public function resolve(Request $request, ArgumentMetadata $argument)
{
$userAgent = $request->headers->get('User-Agent');
... lines 19 - 20
}
... lines 22 - 23

Finish up the resolver by saying return stripos($userAgent, 'Mac') !== false.

... lines 1 - 15
public function resolve(Request $request, ArgumentMetadata $argument)
{
... lines 18 - 19
yield stripos($userAgent, 'Mac') !== false;
}
... lines 22 - 23

Let's try it! Find your browser, refresh and.. boo!

Can use "yield from" only with arrays and Traversables

That's a funny way of saying that I forgot to yield instead of return from this method: resolve() returns a traversable. Try it now and... it works! We still see true for the dump.

Next, let's uncover one last mystery about controller arguments. Back in ArticleController::show(), we originally had an $article argument that was type-hinted with an Article entity class. How did that work? Who was making that automatic query for us?

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.2
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "doctrine/doctrine-bundle": "^2.0", // 2.0.7
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.1.2
        "doctrine/orm": "^2.5.11", // v2.7.1
        "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.8.1
        "knplabs/knp-paginator-bundle": "^5.0", // v5.1.1
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.0
        "knplabs/knp-time-bundle": "^1.8", // v1.11.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.3.0
        "nexylan/slack-bundle": "^2.1", // v2.2.2
        "oneup/flysystem-bundle": "^3.0", // 3.4.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.1
        "sensio/framework-extra-bundle": "^5.1", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.4
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "5.0.*", // v5.0.4
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/mailer": "5.0.*", // v5.0.4
        "symfony/messenger": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.5", // v3.5.0
        "symfony/security-bundle": "5.0.*", // v5.0.4
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "5.0.*", // v5.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.7.3
        "symfony/yaml": "5.0.*", // v5.0.4
        "twig/cssinliner-extra": "^2.12", // v2.12.5
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.3.0
        "fzaninotto/faker": "^1.7", // v1.9.1
        "symfony/browser-kit": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/maker-bundle": "^1.0", // v1.14.3
        "symfony/phpunit-bridge": "5.0.*", // v5.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "5.0.*" // v5.0.4
    }
}