Symfony: Automatically inject a stack of plugins into a service

During the development of an application you often face the requirement of instantiating a bunch of services that share the same interface and execute a method in each of it. In general we are then talking about plugins. Let’s assume you have a “Text” object and you want to apply different “Filter” services that implement a “FilterInterface” and have a “execute()” method which will modify the “Text” object in some way.

What you would normally do is to instantiate all Filter services somewhere and executing them one after another. A very simple implementation can look like this:

<?php

namespace
App;
use App\Text;
use App\Filter\FirstFilter;
use App\Filter\SecondFilter;
use App\Filter\ThirdFilter;

class FilterExecutor
{
/**
*
@var \App\Filter\FilterInterface[]|array
*/
protected $filters = [];

/**
*
@return void
*/
public function applyFilters(Text $text): void
{
$this->loadFilters();

foreach($this->filters as $filter) {
$filter->execute($text);
}
}

/**
*
@return void
*/
protected function loadFilters(): void
{
$this->filters = [
new FirstFilter(),
new SecondFilter(),
new ThirdFilter(),
];
}
}

If you want to add another Filter later, you would also have to modify the loadFilters() method.

Symfony has a very feature rich dependency injection feature called “auto wiring”. Basically Symfony will automatically inject an instance of a service if you are type hinting it in a method parameter (read more about auto wiring here: https://symfony.com/doc/current/service_container/autowiring.html).

But auto wiring can do even more. Since Symfony 3.3 you can configure it so that all services which implement a certain interface are injected as an array automatically. This way, you don’t have to instantiate all Filter services manually. The FilterExecutor service will then look like this:

<?php

namespace
App;
use App\Text;

class FilterExecutor
{
/**
*
@var \App\Filter\FilterInterface[]|array
*/
protected $filters = [];

/**
* Services with FilterInterface will be injected automatically
*
*
@param \App\Filter\FilterInterface[]|array $filters
*/
public function __construct(array $filters)
{
$this->filters = $filters;
}

/**
*
@return void
*/
public function applyFilters(Text $text): void
{
foreach($this->filters as $filter) {
$filter->execute($text);
}
}
}

To accomplish that, you have to configure two things in your services.yaml. (Documentation: https://symfony.com/doc/current/service_container/tags.html#autoconfiguring-tags)

First you have to configure the service container to apply a tag to all services that implement a specific interface:

_instanceof:
# tagging all services that implement FilterInterface
App\Filter\FilterInterface:
tags: ['app.text_filter']

Then you have to configure the FilterExecutor service so that all services with the “text_filter” tag are injected as an array:

App\FilterExecutor:
class: App\FilterExecutor
arguments:
# add services that have "app.text_filter" tag as argument
— !tagged app.text_filter

Now the service container cares about injecting all services that implement the FilterInterface into the FilterExecutor service. If you add more services that implement the FilterInterface, they will be automatically added to the stack.