The DependencyInjection component keeps evolving in Symfony 8.1 with several quality-of-life improvements for autowiring, service decoration, tagged services, and env vars.
Autowiring Env Vars as Closures or Stringables
When you inject an env var as a plain string, its value is baked into the
container at compile time and never changes. That's fine for traditional
request/response applications, but it's a problem in long-running workers
(Messenger, FrankenPHP, Roadrunner), where you may want the env var to refresh
between requests via Container::resetEnvCache().
Symfony 8.1 lets you autowire env vars (or any %env(...)% expression) as
Closure or Stringable values. Each call returns the current value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Worker
{
public function __construct(
#[Autowire(env: 'DB_URL')]
private \Closure $dbUrl,
#[Autowire(env: 'APP_NAME')]
private string|\Stringable $appName = 'default',
// Embedded env vars in any string also work:
#[Autowire('redis://%env(HOST)%:%env(PORT)%')]
private \Stringable $redisDsn,
) {
}
}
When a default value is declared, it's returned when the env var is missing
(it must be a string for Stringable and can be any type for Closure).
The same behavior is available in YAML through the new !env_closure tag:
1 2 3 4 5
services:
App\Worker:
arguments:
- !env_closure '%env(DB_URL)%'
- !env_closure ['%env(APP_NAME)%', 'default']
Service Stacks as Decorators
Service stacks let you compose multiple services into a chain where each
layer wraps the next via @.inner. Until now, stacks couldn't decorate an
existing service, requiring manual decoration instead.
Symfony 8.1 closes that gap by adding decorates and decorates_tag
support to stack definitions. The innermost service in the stack becomes the
decorator of the target service:
1 2 3 4 5 6 7 8
services:
my_stack:
decorates: api_platform.serializer.context_builder
stack:
- class: App\Decorator\AddGroupsContextBuilder
arguments: ['@.inner']
- class: App\Decorator\AddFiltersContextBuilder
arguments: ['@.inner']
When you use decorates_tag, the stack is cloned once per tagged service,
and each clone automatically decorates a different target.
Decorating Tagged Services
Decorating every service that carries a given tag previously required a custom
compiler pass. Symfony 8.1 makes this declarative through a new
decorates_tag option, available both as a service configuration key and as
the #[AsTagDecorator] PHP attribute:
1 2 3 4 5 6 7 8 9
use Symfony\Component\DependencyInjection\Attribute\AsTagDecorator;
#[AsTagDecorator('app.handler')]
class LoggingHandler
{
public function __construct(private object $inner)
{
}
}
The same configuration in YAML:
1 2 3 4
services:
app.logging_handler:
class: App\Decorator\LoggingHandler
decorates_tag: app.handler
Every service tagged with app.handler is now wrapped by LoggingHandler.
This is convenient when you need to add global concerns like logging, tracing,
or caching to a family of services.
Inline Definitions as Factories and Configurators
setFactory() and setConfigurator() already accepted callables
expressed as arrays ([$ref, 'method']) or strings. In Symfony 8.1, they
also accept a Definition instance directly, which Symfony wraps as
[$definition, '__invoke']. This mirrors how Reference objects are
already handled:
1 2 3 4 5
use Symfony\Component\DependencyInjection\Definition;
$container->register('app.handler', HandlerClass::class)
->setFactory(new Definition(InvokableFactory::class))
->setConfigurator(new Definition(InvokableConfigurator::class));
This is useful when the factory or configurator is a one-off invokable that doesn't deserve its own named service definition.
Excluding Files When Importing Services
When you import service definitions from a glob pattern, you can now exclude
specific matches with the new exclude argument of
ContainerConfigurator::import():
1 2 3 4 5 6
return function (ContainerConfigurator $configurator): void {
$configurator->import('services/*.php', exclude: [
'services/legacy/*',
'services/dev_only.php',
]);
};
Previously, the only way to skip a file inside an imported directory was to restructure the service tree.
Stronger #[Target] Support
#[Target] is the recommended way to disambiguate multiple implementations
of the same interface. Symfony 8.1 makes it more ergonomic on both sides.
On the declaration side, #[AsAlias] now accepts a target argument that
creates a named autowiring alias automatically:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(StorageInterface::class, target: 'image')]
class ImageStorage implements StorageInterface
{
}
#[AsAlias(StorageInterface::class, target: 'document')]
class DocumentStorage implements StorageInterface
{
}
On the injection side, Symfony 8.1 deprecates the legacy behavior where the
parameter name alone could match a named alias. That fallback was fragile
because renaming a parameter could silently break the injection, and the
dependency target was invisible from the constructor signature. Starting in
Symfony 8.1, you should always make the target explicit with #[Target]:
1 2 3 4 5
public function __construct(
- private StorageInterface $imageStorage,
+ #[Target('image')] private StorageInterface $storage,
) {
}
Implicit name-based matching still works but emits a deprecation notice. It will be removed in Symfony 9.0.
Setting Voter Priority with #[AsTaggedItem]
Security voters are autoconfigured with the security.voter tag. To set a
voter's priority, you previously had to add #[AutoconfigureTag] or YAML
configuration, but both approaches created a duplicate security.voter tag,
which often caused subtle issues.
Symfony 8.1 lets you use #[AsTaggedItem] on voters instead. It writes the
priority on the existing tag without duplicating it, and it doesn't even
require naming the tag explicitly:
1 2 3 4 5 6 7 8
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
#[AsTaggedItem(priority: 10)]
final class PostVoter extends Voter
{
// ...
}
Environment Variables with Dots
Symfony used to reject env vars whose names contained dots, even though they are valid at the OS level. This was a problem for tooling that generates env vars from hierarchical configuration.
Symfony 8.1 allows dots in env var names, so you can consume those variables directly:
1 2
# .env or container environment
DATABASE.PRIMARY.URL="postgresql://..."
1 2 3 4
services:
App\Repository\UserRepository:
arguments:
$dsn: '%env(DATABASE.PRIMARY.URL)%'
Deprecating Default Index/Priority Methods
When defining tagged iterators or locators, Symfony has long supported two
magic static methods on tagged classes to provide a default index or priority:
getDefault<Name>Name() and getDefaultPriority(). These methods, along
with their defaultIndexMethod and defaultPriorityMethod configuration
counterparts, are deprecated in Symfony 8.1.
The replacement, #[AsTaggedItem], has been available since Symfony 5.3 and
makes the intent visible directly on the class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
+#[AsTaggedItem(index: 'json', priority: 10)]
class JsonHandler
{
- public static function getDefaultName(): string
- {
- return 'json';
- }
-
- public static function getDefaultPriority(): int
- {
- return 10;
- }
}
The deprecated method conventions will be removed in Symfony 9.0.
I'm so happy to see named autowiring aliases w/o
#[Target]is being deprecated/removed! I recently did a short video on the dangers of it. https://www.youtube.com/watch?v=t2CdE7DgmmM