Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

RAD with Symfony 3.3

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

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

Login Subscribe

We've done a lot of work, and I showed you the ugliest parts of the new system so that you can solve them in your project. That's cool... but so far, coding hasn't been much fun!

And that's a shame! Once you're done upgrading, using the new configuration system is a blast. Let's take it for a legit test drive.

Creating an Event Subscriber

Here's the goal: create an event listener that adds a header to every response. Step 1: create an EventSubscriber directory - though this could live anywhere - and a file inside called AddNiceHeaderEventSubscriber:

... lines 1 - 2
namespace AppBundle\EventSubscriber;
... lines 4 - 8
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 11 - 22
}

Event subscribers always look the same: they must implement EventSubscriberInterface:

... lines 1 - 2
namespace AppBundle\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
... lines 6 - 8
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 11 - 22
}

I'll go to the Code-Generate menu, or Command+N on a Mac - and select "Implement Methods" to add the one required function: public static getSubscribedEvents():

... lines 1 - 2
namespace AppBundle\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
... lines 6 - 8
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 11 - 16
public static function getSubscribedEvents()
{
... lines 19 - 21
}
}

To listen to the kernel.response event, return KernelEvents::RESPONSE set to onKernelResponse:

... lines 1 - 2
namespace AppBundle\EventSubscriber;
... lines 4 - 6
use Symfony\Component\HttpKernel\KernelEvents;
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 11 - 16
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => 'onKernelResponse'
];
}
}

On top, create that method: onKernelResponse() with a FilterResponseEvent object: that's the argument passed to listeners of this event:

... lines 1 - 5
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
... lines 7 - 8
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
public function onKernelResponse(FilterResponseEvent $event)
{
... lines 13 - 14
}
... lines 16 - 22
}

Inside, add a header: $event->getResponse()->headers->set() with X-NICE-MESSAGE set to That was a great request:

... lines 1 - 5
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
... lines 7 - 8
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
public function onKernelResponse(FilterResponseEvent $event)
{
$event->getResponse()
->headers->set('X-NICE-MESSAGE', 'That was a great request!');
}
... lines 16 - 22
}

Ok, we've touched only one file and written 23 lines of code. And... we're done! Yep, this will already work. I'll open up my network tools, then refresh one more time. For the top request, click "Headers", scroll down and... there it is! We just added an event subscriber to Symfony... by just creating the event subscriber. Yea... it kinda makes sense.

The new class was automatically registered as a service and automatically tagged thanks to autoconfigure.

Grab some Dependencies

But what if we want to log something from inside the subscriber? What type-hint should we use for the logger? Let's find out! Find your terminal and run:

php bin/console debug:container --types

And search for "logger". Woh, nothing!? So, there is no way to type-hint the logger for autowiring!?

Actually... since the autowiring stuff is new, some bundles are still catching up and adding aliases for their interfaces. An alias has been added to MonologBundle... but only in version 3.1. In composer.json, I'll change its version to ^3.1:

68 lines composer.json
{
... lines 2 - 15
"require": {
... lines 17 - 22
"symfony/monolog-bundle": "^3.1",
... lines 24 - 30
},
... lines 32 - 66
}

Then, run:

composer update

to pull down the latest changes.

If you have problems with other bundles that don't have aliases yet... don't panic! You can always add the alias yourself. In this case, the type-hint should be Psr\Log\LoggerInterface, which you could alias to @logger:

services:
    # ...
    Psr\Log\LoggerInterface: '@logger'

You always have full control over how things are autowired.

Ok, done! Let's look at the types list again:

php bin/console debug:container --types

There it is! Psr\Log\LoggerInterface.

Back in the code, add public function __construct() with a LoggerInterface $logger argument:

... lines 1 - 4
use Psr\Log\LoggerInterface;
... lines 6 - 9
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 12 - 13
public function __construct(LoggerInterface $logger)
{
... line 16
}
... lines 18 - 32
}

I'll hit Option+Enter and initialize my field:

... lines 1 - 4
use Psr\Log\LoggerInterface;
... lines 6 - 9
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
... lines 18 - 32
}

That's just a shortcut to add the property and set it.

In the main method, use the logger: $this->logger->info('Adding a nice header'):

... lines 1 - 9
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 12 - 18
public function onKernelResponse(FilterResponseEvent $event)
{
$this->logger->info('Adding a nice header!');
... lines 22 - 24
}
... lines 26 - 32
}

Other than the one-time composer issue, we've still only touched one file. Find your browser and refresh. I'll click one of the web debug toolbar links at the bottom and then go to "Logs". There it is! Autowiring passes us the logger without any configuration.

Adding more Arguments

Let's keep going! Instead of hard-coding the message, let's use our MessageManager. Add it as a second argument, then create the property and set it like normal:

... lines 1 - 4
use AppBundle\Service\MessageManager;
... lines 6 - 10
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... line 13
private $messageManager;
public function __construct(LoggerInterface $logger, MessageManager $messageManager)
{
... line 18
$this->messageManager = $messageManager;
}
... lines 21 - 37
}

In the method, add $message = $this->messageManager->getEncouragingMessage(). Use that below:

... lines 1 - 10
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 13 - 21
public function onKernelResponse(FilterResponseEvent $event)
{
... lines 24 - 25
$message = $this->messageManager->getEncouragingMessage();
$event->getResponse()
->headers->set('X-NICE-MESSAGE', $message);
}
... lines 31 - 37
}

Once again, autowiring will work with zero configuration. The MessageManager service id is equal to its class name... so autowiring works immediately:

... lines 1 - 5
services:
... lines 7 - 41
AppBundle\Service\MessageManager:
arguments:
- ['You can do it!', 'Dude, sweet!', 'Woot!']
- ['We are *never* going to figure this out', 'Why even try again?', 'Facepalm']

Refresh to try it! Click the logs icon, go to "Request / Response", then the "Response" tab. Yea! This is another way to see our response header.

Say hello to the new workflow: focus on your business logic and ignore configuration. If you do need to configure something, let Symfony tell you.

Manually Wiring when Necessary

Let's see an example of that: add a third argument: $showDiscouragingMessage. I'll use Alt+Enter again to set this on a new property:

... lines 1 - 10
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 13 - 14
private $showDiscouragingMessage;
public function __construct(LoggerInterface $logger, MessageManager $messageManager, $showDiscouragingMessage)
{
... lines 19 - 20
$this->showDiscouragingMessage = $showDiscouragingMessage;
}
... lines 23 - 41
}

This argument is not an object: it's a boolean. And that means that autowiring cannot guess what to put here.

But... ignore that! In onKernelResponse(), add some logic: if $this->showDiscouragingMessage, then call getDiscouragingMessage(). Else, call getEncouragingMessage():

... lines 1 - 10
class AddNiceHeaderEventSubscriber implements EventSubscriberInterface
{
... lines 13 - 23
public function onKernelResponse(FilterResponseEvent $event)
{
... lines 26 - 27
$message = $this->showDiscouragingMessage
? $this->messageManager->getDiscouragingMessage()
: $this->messageManager->getEncouragingMessage();
... lines 31 - 33
}
... lines 35 - 41
}

Just like before, we're focusing only on this class, not configuration. And this class is done! So, let's try it! Error!

Cannot autowire service AddNiceHeaderEventSubscriber: argument $showDiscouragingMessage of method __construct() must have a type-hint or be given a value explicitly.

Yes! Symfony can automate most configuration. And as soon as it can't, it will tell you what you need to do.

Copy the class name, then open services.yml. To explicitly configure this service, paste the class name and add arguments. We only need to specify $showDiscouragingMessage. So, add $showDiscouragingMessage: true:

... lines 1 - 5
services:
... lines 7 - 46
AppBundle\EventSubscriber\AddNiceHeaderEventSubscriber:
arguments:
$showDiscouragingMessage: true

Refresh now! The error is gone! And in the profiler... yep! The message is much more discouraging. Boooo.

Ok guys that is it! Behind the scenes, the way that you configure services is still the same: Symfony still needs to know the class name and arguments of every service. But before Symfony 3.3, all of this had to be done explicitly: you needed to register every single service and specify every argument and tag. But if you use the new features, a lot of this is automated. Instead of filling in everything, only configure what you need.

And there's another benefit to the new stuff. Now that our services are private - meaning, we no longer use $container->get() - Symfony will give us more immediate errors and will automatically optimize itself. Cool!

Your turn to go play! I hope you love this new stuff: faster development without a ton of WTF moments! And let me know what you think!

All right guys, see you next time.

Leave a comment!

8
Login or Register to join the conversation
Mike P. Avatar
Mike P. Avatar Mike P. | posted 5 years ago

I really like that you remind us of old principles by example how auto wiring works, even if you thought it several videos before. Nice!

1 Reply
Default user avatar
Default user avatar NothingWeAre | posted 5 years ago

There is only one drawback with EventSubscribers against EventListeners.
EventListener can be defined as lazy, but EventSubscriber will be instantiated on every request.

Reply

Hey NothingWeAre!

That's *almost* correct :). Well, it IS correct, but with one small clarification: Symfony event subscribers are listeners are both lazy - there is zero difference. However, if you are making a Doctrine listener, then Doctrine event subscribers are indeed *always* instantiated. Doctrine event listeners *can* be made lazy, but actually are *not* lazy by default: you need to add a lazy: true option to the tag. Actually, this was fixed recently, and Doctrine listeners will always be lazy in Symfony 4.2 (https://github.com/symfony/... - but indeed, Doctrine event subscribers still won't be lazy (they can't be, unfortunately, because of the way the Doctrine event system is designed).

This is not a point most people think about - I'm glad you are ;).

Cheers!

1 Reply
Default user avatar

In my experience Symfony kernel event bus always instantiate all listeners and subscribers, eventually, on every request where at least one event was fired.
Because I had events that depend on services, which depended on even more services, initialization graph had grown too much to my liking, regardless of the fact that each service was rather small. While some dependencies was cut in process of optimisation, in cases where it was not possible we decided to rely on lazy event listeners using ProxyManager (https://symfony.com/doc/cur.... And, with this approach event subscribers cannot be lazy, because they need to call getSubscribedEvents. On the other hand event listeners would be fully instantiated only when related event is fired and on..... function is called.
While this can also be achieved with Service Subscriber and accessing related service in function, we prefer Proxy approach with dependencies in __constructor, because it simplifies injecting them during testing.

Reply

Hey NothingWeAre!

Ahhhh! Now I understand better! Yea, event listeners (or subscribers) are a key place where the dependency graph can become a problem. It's really a problem whenever you are listening to an event that happens on every request - like kernel.request. The trick is that event subscribers *are* lazy: your object is not instantiated *unless* the event they are listening to is fired. But, if you're listening to something like kernel.request, then your event subscriber WILL be instantiated on every request because it needs to be called. That is especially a problem if your listener/subscriber needs to be called on every request, but then you only perform some actions ever 100th request (e.g. maybe a listener checks some info on the user, and if some rare condition is met, more action is taken). In these cases, (A) your listener/subscriber is instantiated which (of course) cases (B) all of your dependent services to be instantiated, but then (C) on most requests, you don't even use most of the services that were instantiated.

Phew! But, what I'm not sure about from your comment is how making your event listeners lazy via the ProxyManager would help with this. Event subscribers are naturally already lazy: they are only instantiated when they specific event they listen to is fired (if you're not seeing this behavior, it may be possible you're on an older version of Symfony where this wasn't as optimized). Event listeners are also naturally lazy... you shouldn't need to use the proxy with them. If you're seeing something different, definitely let me know what version of Symfony you are on. As I mentioned above, *usually* you need to worry about making the services that your listener/subscriber depends on lazy, not the listener/subscriber themselves.

Ok... too long of a answer/question from me :). Let me know what your situation is!

Cheers!

Reply
Default user avatar

Thanks for reply.
I do not know what am I doing wrong, but I have tried to watch constructor of event listener with debugger and class that listens/subscribes for "security.authentication.failure" is instantiated on every request, regardless if concerned event is fired. While function call really are executed only on related event.

I think that it is related to deprecated class ContainerAwareEventDispatcher, which is used for listeners/subscribers management in our Symfony instalation (we are still using 3.3).

Reply

Hey NothingWeAre!

Yes, I think you're correct! And, after doing some digging, it looks like the event subscribes were not made fully lazy until Symfony 3.4 (and part of the reason it wasn't lazy before was due to ContainerAwareEventDispatcher). Here is the PR that made them finally lazy: https://github.com/symfony/...

So, that at least explains it :). In 3.4, your subscribers are lazy, but they may indeed not be before.

Cheers!

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 5 years ago

This is great!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.3.0-RC1", // v3.3.0-RC1
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^3.1", // v3.2.0
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.7.1
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice