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

Nicolas Grekas
Contributed by Nicolas Grekas in #63988

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

Nicolas Grekas
Contributed by Nicolas Grekas in #63590

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

Mathias Arlaud
Contributed by Mathias Arlaud in #62638

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

Jérôme Tamarelle
Contributed by Jérôme Tamarelle in #63910

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

tilaven
Contributed by tilaven in #63111

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

Ayyoub AFW-ALLAH Nicolas Grekas
Contributed by Ayyoub AFW-ALLAH and Nicolas Grekas in #63181 and #63426

#[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]

Ayyoub AFW-ALLAH
Contributed by Ayyoub AFW-ALLAH in #62824

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

Massimiliano Baldanza
Contributed by Massimiliano Baldanza in #62835

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

Nicolas Grekas
Contributed by Nicolas Grekas in #62339

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.

Published in #Living on the edge