Buy

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:

24 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

24 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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():

24 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

24 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

24 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

24 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

34 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

34 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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'):

34 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

39 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

39 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

46 lines app/config/services.yml
... 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:

43 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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():

43 lines src/AppBundle/EventSubscriber/AddNiceHeaderEventSubscriber.php
... 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:

50 lines app/config/services.yml
... 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!

  • 2017-08-29 Mike

    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!