Buy Access to Course
18.

Tags, Compiler Passes & Other Nerdery

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Let's review: we gave our service a tag. And now, we want to tell Symfony to find all services in the container with this tag, and pass them as the first argument to our KnpUIpsum service. Like I mentioned in the previous chapter, if you only need to support Symfony 3.4 or higher, there's a shortcut. But if you need to support lower versions or want to geek out with me about compiler passes, well, you're in luck!

First question: how can we find all services that have the knpu_ipsum_word_provider tag? If you look in the extension class, you might think that we could do some magic here with the $container variable. And... yea! It even has a method called findTaggedServiceIds()!

But... you actually can't do this logic here. Why? Well, when this method is called, not all of the other bundles and extensions have been loaded yet. So if you tried to find all the services with a certain tag, some of the services might not be in the container yet. And actually, you can't even get that far: the ContainerBuilder is empty at the beginning of this method: it doesn't contain any of the services from any other bundles. Symfony passes us an empty container builder, and then merges it into the real one later.

Compiler Pass

The correct place for any logic that needs to operate on the entire container, is a compiler pass. In the DependencyInjection directory - though it doesn't technically matter where this class goes - create a Compiler directory then a new class called WordProviderCompilerPass. Make this, implement a CompilerPassInterface, and then go to the Code -> Generate menu - or Command + N on a Mac - click "Implement Methods" and select process().

// ... lines 1 - 7
class WordProviderCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
}
}

A compiler pass also receives a ContainerBuilder argument. But, instead of being empty, this is full of all of the services from all of the bundles. That means that we can say foreach ($container->findTaggedServiceIds(), pass this the tag we're using: knpu_ipsum_word_provider, and say as $id => $tags.

// ... lines 1 - 9
public function process(ContainerBuilder $container)
{
foreach ($container->findTaggedServiceIds('knpu_ipsum_word_provider') as $id => $tags) {
// ... line 13
}
// ... line 15
}

This is a little confusing: the $id key is the service ID that was tagged. Then, $tags is an array with extra information about the tag. Sometimes, a tag can have other attributes, like priority. You can also tag the same service with the same tag, multiple times.

Anyways, we don't need that info: let's just var_dump($id) to see if it works, then die.

// ... lines 1 - 11
foreach ($container->findTaggedServiceIds('knpu_ipsum_word_provider') as $id => $tags) {
var_dump($id);
}
die;
// ... lines 16 - 17

Registering the Compiler Pass

To tell Symfony about the compiler pass, open your bundle class. Here, go back to the Code -> Generate menu - or Command + N on a Mac - choose "Override Methods" and select build(). You don't need to call the parent build() method: it's empty. All we need here is $container->addCompilerPass(new WordProviderCompilerPass()).

// ... lines 1 - 9
class KnpULoremIpsumBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new WordProviderCompilerPass());
}
// ... lines 16 - 27
}

There are different types of compiler passes, which determine when they are executed relative to other passes. And, there's also a priority. But unless you're doing something really fancy, the standard type and priority work fine.

Thanks to this line, whenever the container is built, it should hit our die statement. Let's move over to the browser and, refresh!

Yes! There is the one service that has the tag.

And now... it's easy! The code in a compiler pass looks a lot like the code in an extension class. At the top, add $definition = $container->getDefinition('knpu_lorem_ipsum.knpu_ipsum').

Ultimately, we need to modify this services's first argument. Create an empty $references array. And, in the foreach, just add stuff to it: $references[] = new Reference() and pass in the $id.

Finish this with $definition->setArgument(), pass it 0 for the first argument, and the array of reference objects.

// ... lines 1 - 10
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('knpu_lorem_ipsum.knpu_ipsum');
$references = [];
foreach ($container->findTaggedServiceIds('knpu_ipsum_word_provider') as $id => $tags) {
$references[] = new Reference($id);
}
$definition->setArgument(0, $references);
}

We're done! Go back to our browser and try it! Woohoo! We're now passing an array of all of the word provider services into the KnpUIpsum class.... which... yea, is just one right now.

Cleanup the Old Configuration

With this in place, we can remove our old config option. In the Configuration class, delete the word_provider option. And in the extension class, remove the code that reads this.

// ... lines 1 - 10
class KnpULoremIpsumExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$definition = $container->getDefinition('knpu_lorem_ipsum.knpu_ipsum');
$definition->setArgument(1, $config['unicorns_are_real']);
$definition->setArgument(2, $config['min_sunshine']);
}
// ... lines 25 - 29
}

Tagging the CustomWordProvider

Next, move over to the application code, and in config/packages/knpu_lorem_ipsum.yaml, yep, take out the word_provider key.

knpu_lorem_ipsum:
min_sunshine: 5

If you refresh now... it's going to work. But, not surprisingly, the word "beach" will not appear in the text. Remember: "beach" is the word that we're adding with our CustomWordProvider. This class is not being used. And... that make sense! We haven't tagged this service with anything, so our bundle doesn't know to use it.

Before we do that, now that there are multiple providers, I don't need to extend the core provider anymore. Implement the WordProviderInterface directly. Then, just return an array with the one word: beach.

14 lines | src/Service/CustomWordProvider.php
// ... lines 1 - 6
class CustomWordProvider implements WordProviderInterface
{
public function getWordList(): array
{
return ['beach'];
}
}

To tag the service, open config/services.yaml. This class is automatically registered as a service. But to give it a tag, we need to override that: App\Service\CustomWordProvider, and, below, tags: [knpu_ipsum_word_provider].

40 lines | config/services.yaml
// ... lines 1 - 5
services:
// ... lines 7 - 37
App\Service\CustomWordProvider:
tags: ['knpu_ipsum_word_provider']

Let's try it! Refresh! Yes! It's alive!

Setting up Autoconfiguration

But... there's something that's bothering me. Most of the time in Symfony, you don't need to manually configure the tag. For example, earlier, when we created an event subscriber, we did not need to give it the kernel.event_subscriber tag. Instead, Symfony was smart enough to see that our class implemented EventSubscriberInterface, and so it added that tag for us automatically.

So... what's the difference? Why can't the tag be automatically added in this situation? Well... it can! But we need to set this up in our bundle. Open the extension class, go anywhere in the load() method, and add $container->registerForAutoconfiguration(WordProviderInterface::class). The feature that automatically adds tags is called autoconfiguration, and this method returns a "template" Definition object that we can modify. Use ->addTag('knpu_ipsum_word_provider').

// ... lines 1 - 11
class KnpULoremIpsumExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// ... lines 16 - 25
$container->registerForAutoconfiguration(WordProviderInterface::class)
->addTag('knpu_ipsum_word_provider');
}
// ... lines 29 - 33
}

Cool, right? Back in our app code, remove the service entirely. And now, try it! Hmm, no beach the first time but on the second refresh... we got it!

We now have a true word provider plugin system. And creating a custom word provider is as easy as creating a class that implements WordProviderInterface.

Next, let's finally put our library up on Packagist!