Symfony: Automatically inject a stack of plugins into a service

KC Müller
May 2, 2019 · 2 min read

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store