Hooking into Symfony with an Event Subscriber

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Before we dive into the core code, let's hook into the request-response process. Let's create our own listener to this kernel.request event. To do that, in the src/ directory, I already have an EventListener/ directory. It doesn't matter where we put this class, but inside here, let's create a new class called UserAgentSubscriber.

All event subscribers must implement EventSubscriberInterface. I'll go to the Code -> Generate menu on PhpStorm - or Command + N on a Mac - and select "Implement Methods" to generate the one method this interface requires: getSubscribedEvents(). Inside, return an array of all the events we want to listen to, which will just be one.

... lines 1 - 4
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class UserAgentSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
}
}

Now... you might be expecting me to say 'kernel.request' => 'onKernelRequest'. This would mean that when the kernel.request event happens, I want Symfony to call an onKernelRequest() method on this class that we will create in a minute. This would work, but starting in Symfony 4.3, instead of using this made-up kernel.request string, you can pass the event class name, which in this case is RequestEvent::class.

... lines 1 - 5
use Symfony\Component\HttpKernel\Event\RequestEvent;
... line 7
class UserAgentSubscriber implements EventSubscriberInterface
{
... lines 10 - 14
public static function getSubscribedEvents()
{
return [
RequestEvent::class => 'onKernelRequest'
];
}
}

More and more, you'll see documentation that tells you to listen to an event class like this, instead of a random string.

Now, create the function: public function onKernelRequest(). Inside, dump and die it's alive!!!.

... lines 1 - 9
public function onKernelRequest()
{
dd('it\'s alive!!!');
}
... lines 14 - 22

Cool! With any luck, Symfony will call our event listener very early on and it will kill the page. Close the profiler, refresh and... it's alive! Well actually, it's dead, but ya know... that's what we wanted!

Logging in the Listener and Controller

To make the class more interesting, let's log something! You know the drill: add public function __construct() with LoggerInterface $logger. I'll hit Alt+Enter and go to initialize fields as a lazy way to create the property and set it down here.

... lines 1 - 4
use Psr\Log\LoggerInterface;
... lines 6 - 8
class UserAgentSubscriber implements EventSubscriberInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 17 - 28
}

In the method, add $this->logger->info() with:

I'm logging SUPER early on the request!

... lines 1 - 17
public function onKernelRequest()
{
$this->logger->info('I\'m logging SUPER early on the request!');
}
... lines 22 - 30

To compare this to logging in a controller, go back to ArticleController. On the homepage action, autowire a $logger argument and say $logger->info():

Inside the controller!

... lines 1 - 8
use Psr\Log\LoggerInterface;
... lines 10 - 13
class ArticleController extends AbstractController
{
... lines 16 - 28
public function homepage(ArticleRepository $repository, LoggerInterface $logger)
{
$logger->info('Inside the controller!');
... lines 32 - 36
}
... lines 38 - 64
}

We expect that the listener will be called first because the RequestEvent, also known as kernel.request, happens before the controller is executed. Refresh the page. It works... and once again, open the profiler in a new tab, click Logs and... perfect! First our listener log and then the controller.

And you can now see our subscriber inside the performance section! Make sure you have the threshold set to 0. Let's see... there it is: UserAgentSubscriber. And then down... way after that... is the controller.

The Event Argument

One of the other "laws" of Symfony's event system is that a listener will always be passed a single argument: an event object. What type of object is it? This is where the new "event class names as event names" comes in handy. We're listening to RequestEvent, which means - surprise! - Symfony will pass us a RequestEvent object! Let's just dd($event).

... lines 1 - 6
use Symfony\Component\HttpKernel\Event\RequestEvent;
... line 8
class UserAgentSubscriber implements EventSubscriberInterface
{
... lines 11 - 17
public function onKernelRequest(RequestEvent $event)
{
dd($event);
... line 21
}
... lines 23 - 29
}

Ok, move back over, close the profiler again, refresh and... there it is! Each event you listen to will be passed a different event object... and each event object will have different super-powers: giving you whatever information you might need for that particular situation, and often, allowing you to change things.

For example, this event contains the Request object... because if you're listening to this very early event in Symfony... there's a good chance that you might want to use the Request object to do something.

In fact, let's do exactly that. Clear out our method and say $request = $event->getRequest(). And then we'll grab the $userAgent off of the request with $request->headers->get('User-Agent'). Finally, let's log this: $this->logger->info() and I'll use sprintf() to say

The User-Agent is %s

Pass $userAgent for the placeholder.

... lines 1 - 17
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
$userAgent = $request->headers->get('User-Agent');
$this->logger->info(sprintf('The User-Agent is "%s"', $userAgent));
}
... lines 25 - 33

Let's check it out! Move over, refresh, open the profiler in a new tab, go down to Logs and... we got it! We're logging the user agent before the controller is called.

Ok! Now that we've hooked into Symfony, let's take a step back and start tracing through everything that happens from the start of the request, line-by-line. We'll even see where the RequestEvent is dispatched and eventually where the controller is executed.

Let's start that journey next.

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
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "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.9", // v1.9.10
        "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
    }
}