autoconfigure in Symfony: DI Tags Without Manual Configuration
AI generated
SF
{ }
Symfony · Dependency Injection · autoconfigure · DI Tags
autoconfigure in Symfony:
DI Tags Without Manual Configuration

In Symfony, registering event listeners, voters, commands and Twig extensions long meant: implement an interface and separately write a tag entry in services.yaml. The autoconfigure feature recognizes well-known interfaces automatically and assigns the necessary DI tags without a single tag ever having to be written by hand.

16 min read autoconfigure · registerForAutoconfiguration · PHP Attributes · DI Tags Symfony 5.x / 6.x / 7.x · PHP 8.1+

1. What is autoconfigure and why does it exist?

Symfony's Dependency Injection container works with tags: a service gets a tag such as kernel.event_listener, twig.extension or security.voter, and this tells Symfony which internal infrastructure it needs to be hooked into. Historically, these tags were entered manually in services.yaml. That meant implementing an interface was not enough by itself; you also had to follow up in YAML, and every forgotten line resulted in a service that had no effect, without any error message.

autoconfigure solves this problem through automatic tag detection. Symfony knows the associated tags for many of its own interfaces and assigns them automatically whenever a service implements that interface. EventSubscriberInterface automatically gets the tag kernel.event_subscriber. VoterInterface gets security.voter. Command subclasses get console.command. In other words: implementing the interface is enough, Symfony takes care of the rest. The services.yaml entries for tags become unnecessary.

Since Symfony 5.3, autoconfigure also works with PHP attributes: classes carrying certain attributes, such as #[AsEventListener], #[AsCommand] or #[AsTwigComponent], also receive their tags automatically. This makes autoconfigure the central mechanism for zero-YAML DI configuration in modern Symfony projects. The container compiler processes all recognized interfaces and attributes and generates the tags internally; the developer only ever sees clean PHP code.

2. Enabling and verifying autoconfigure

In a standard Symfony project, autoconfigure is enabled globally for all services that fall under the App\ namespace. The relevant configuration lives in config/services.yaml: the entry _defaults: autoconfigure: true applies to all services in the same block. Anyone who wants to disable autoconfigure for a single service sets autoconfigure: false directly on the service entry; that is rarely needed, but possible if a service should deliberately not receive an automatic tag.

To verify which tags a service has received, use bin/console debug:container --show-tags App\EventListener\MyListener. The command shows all tags of the service, both manually entered ones and those assigned automatically by autoconfigure. Anyone who wants to see which services are registered for a particular tag uses bin/console debug:container --tag=kernel.event_listener. Both commands are indispensable when debugging DI configuration in projects that make heavy use of autoconfigure.

One important aspect of activation: autoconfigure only affects services that are registered in the container. Classes that are not explicitly registered as services and are not picked up by the App\ glob import do not benefit from autoconfigure. That is relevant for classes in directories excluded from the standard import, for example classes in src/DataFixtures, or classes that are only used as an argument rather than as a service. The glob import in services.yaml determines which classes are registered as services at all.


# config/services.yaml - standard Symfony project configuration
services:
  _defaults:
    autowire: true       # Constructor parameters resolved automatically
    autoconfigure: true  # Interface-based tags applied automatically
    public: false        # All services private by default

  App\:
    resource: '../src/'
    exclude:
      - '../src/DependencyInjection/'
      - '../src/Entity/'
      - '../src/Kernel.php'

# With autoconfigure: true, NO manual tags needed for:
#   - EventSubscriberInterface  → kernel.event_subscriber
#   - VoterInterface            → security.voter
#   - AbstractCommand           → console.command
#   - MessageHandlerInterface   → messenger.message_handler
#   - TwigExtension             → twig.extension

# Disabling autoconfigure for a single service (rare):
# App\Service\SpecialService:
#   autoconfigure: false

3. Well-known interfaces and their automatic tags

Symfony knows an extensive list of interfaces for which autoconfigure assigns tags automatically. The most important ones: EventSubscriberInterface gets kernel.event_subscriber. VoterInterface gets security.voter. AbstractCommand subclasses get console.command. MessageHandlerInterface gets messenger.message_handler. TwigExtension gets twig.extension. EncoderInterface implementations, constraint validator classes and form type extensions follow the same pattern.

A concrete example: anyone implementing a new Messenger handler in Symfony writes the class, implements MessageHandlerInterface or uses the #[AsMessageHandler] attribute, and the handler is immediately registered with the messenger. No tag in services.yaml, no configuration in messenger.yaml for the handler itself. The autoconfigure feature directly connects the interface implementation with the Symfony infrastructure that expects this interface. That is the actual added value: fewer places where the same fact has to be expressed.

Not all third-party bundles use autoconfigure with equal consistency. Older bundles may still require manual tags for their extension points. Anyone building or using a modern bundle should check whether it offers autoconfigure support; by now that has become a quality hallmark of good bundle architecture. Symfony itself applies it consistently: all internal extension points use autoconfigure so that developers do not have to research framework internals just to register a service correctly.

4. PHP attributes as autoconfigure triggers

Since Symfony 5.3, PHP attributes can be registered as autoconfigure triggers. An attribute on a class or method can have the same effect as an interface implementation: it causes a DI tag to be assigned automatically. That is the mechanism behind #[AsEventListener], #[AsCommand], #[AsMessageHandler] and every other Symfony attribute that moves DI configuration into the PHP code. Internally, Symfony registers an autoconfigure rule for each of these attributes, which is evaluated during container compilation.

Attribute-based autoconfigure is more flexible than interface-based autoconfigure in one crucial respect: attributes can carry parameters. #[AsEventListener(event: KernelEvents::REQUEST, priority: 50)] conveys not just the tag name but also the event name and priority, information that with interface-based registration could only be conveyed through services.yaml tag parameters. With PHP attributes, these parameters belong directly to the PHP code and are therefore versionable, greppable and refactoring-friendly.

Third-party bundles can define their own attributes and register them as autoconfigure triggers. This lets bundle authors offer an ergonomic developer experience: users of the bundle write an attribute on their class, and the bundle's DI pass processes the attribute and configures the service completely. A practical example: a rate-limiting bundle could define a #[RateLimited(maxRequests: 10, period: 60)] attribute that sets the necessary tags and parameters for the rate-limiting middleware stack on a controller.


<?php

declare(strict_types=1);

// Custom attribute that triggers autoconfigure in a bundle or project
// src/DependencyInjection/Attribute/AsDataTransformer.php

namespace App\DependencyInjection\Attribute;

use Attribute;

/**
 * Marks a class as a data transformer, triggers autoconfigure DI tag.
 * Apply on classes implementing DataTransformerInterface.
 */
#[Attribute(Attribute::TARGET_CLASS)]
final class AsDataTransformer
{
    public function __construct(
        public readonly string $supports,  // FQCN of the class this transformer handles
        public readonly int $priority = 0,
    ) {}
}

// --- Registering the attribute as autoconfigure trigger ---
// src/DependencyInjection/AppExtension.php (or a Compiler Pass)

use App\DependencyInjection\Attribute\AsDataTransformer;
use Symfony\Component\DependencyInjection\ContainerBuilder;

// In your bundle's build() method or a CompilerPass:
public function build(ContainerBuilder $container): void
{
    $container->registerAttributeForAutoconfiguration(
        AsDataTransformer::class,
        static function (
            ChildDefinition $definition,
            AsDataTransformer $attribute,
            \Reflector $reflector,
        ): void {
            $definition->addTag('app.data_transformer', [
                'supports'  => $attribute->supports,
                'priority'  => $attribute->priority,
            ]);
        }
    );
}

// --- Using the attribute on a transformer class ---
#[AsDataTransformer(supports: ProductDto::class, priority: 10)]
final class ProductDataTransformer implements DataTransformerInterface
{
    public function transform(mixed $value): mixed
    {
        // Transformation logic here
        return $value;
    }
}

5. Defining your own autoconfigure rules

Symfony provides the method ContainerBuilder::registerForAutoconfiguration() to define your own interface-to-tag rules. This method takes an interface name and a closure that gets called for every service implementing that interface. Inside the closure, the service definition can be modified: adding tags, configuring method calls, or setting arguments. This is the entry point for your own bundle or project extensions that want to introduce autoconfigure conventions for their interfaces.

A practical use case for projects without a bundle: a custom PriorityHandlerInterface implemented by several classes. With registerForAutoconfiguration() in a custom compiler pass, all implementations automatically get a tag carrying the priority from an interface constant. That replaces a long list of manual tag entries in services.yaml with a single configuration rule. Anyone adding a new implementation writes the class, implements the interface, and the tag appears automatically.

For integrating autoconfigure rules into projects without their own bundle, a compiler pass is the clean approach. The class implements CompilerPassInterface and is registered in the Kernel::build() callback: $container->addCompilerPass(new MyAutoconfigurePass()). Inside the pass's process() callback, you call $container->registerForAutoconfiguration(). That keeps the autoconfigure rules separate from the service logic and makes them easy to find.

6. Using autoconfigure in your own bundles

Bundle authors define their autoconfigure rules in the build() method of the bundle class or in a compiler pass registered inside the bundle. The method $container->registerAttributeForAutoconfiguration() links a PHP attribute to a configuration closure; that is the modern way to do attribute-based autoconfigure. Alternatively, you use $container->registerForAutoconfiguration() for interface-based rules. Both approaches can be combined: an interface sets the base tag, an attribute adds optional parameters such as priority or alias.

A well-designed bundle documents its autoconfigure interfaces and attributes explicitly in its README: which interfaces must be implemented, which attributes are optionally available, and which tags result from them. That keeps the bundle transparent for users without any YAML configuration. The best way to test autoconfigure rules is through a functional container test that checks whether a test class implementing the interface receives the expected tag, before the bundle is deployed in a real project.


<?php

declare(strict_types=1);

namespace App\Bundle\TransformerBundle;

use App\Bundle\TransformerBundle\DependencyInjection\Attribute\AsDataTransformer;
use App\Bundle\TransformerBundle\Contract\DataTransformerInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

/**
 * TransformerBundle, registers autoconfigure rules for transformers.
 * Implementors of DataTransformerInterface get tagged automatically.
 */
final class TransformerBundle extends AbstractBundle
{
    /**
     * Register autoconfigure rules for this bundle's extension points.
     */
    public function build(ContainerBuilder $container): void
    {
        parent::build($container);

        // Interface-based autoconfigure: all DataTransformerInterface implementations are tagged
        $container->registerForAutoconfiguration(DataTransformerInterface::class)
            ->addTag('transformer.data_transformer');

        // Attribute-based autoconfigure: #[AsDataTransformer] adds tag with metadata
        $container->registerAttributeForAutoconfiguration(
            AsDataTransformer::class,
            static function (
                ChildDefinition $definition,
                AsDataTransformer $attribute,
            ): void {
                $definition->addTag('transformer.data_transformer', [
                    'supports'  => $attribute->supports,
                    'priority'  => $attribute->priority,
                ]);
            }
        );
    }
}

// Any class implementing DataTransformerInterface automatically gets the tag.
// No services.yaml entry needed in the consuming project.

7. Debugging: checking tags and autoconfigure

The most important debugging tool for autoconfigure is bin/console debug:container. Without an argument it lists all services in the container. With the service name as an argument it shows all details: class, arguments, tags, and whether autoconfigure is active. With --tag=my_tag it lists all services carrying that tag, which immediately answers the question of whether autoconfigure is having an effect on a service. A service missing an expected tag points either to missing autoconfigure, an incorrect namespace configuration, or an interface that was not actually implemented.

When autoconfigure unexpectedly does not work, the following workflow helps: check whether the service is in the container (debug:container App\MyClass), check whether autoconfigure is active (the output shows autoconfigure: true), check whether the interface is correctly implemented (implements MyInterface with the full namespace). The most common mistake: the interface's use statement is missing or points to the wrong namespace. That causes PHP to treat the class as not implementing the interface even though the method name matches, a mistake a static analyzer would catch immediately.

Interface / Attribute Automatic Tag Symfony Component Manual Alternative
EventSubscriberInterface kernel.event_subscriber EventDispatcher YAML tag entry
VoterInterface security.voter Security YAML tag entry
#[AsCommand] console.command Console YAML or configure()
MessageHandlerInterface messenger.message_handler Messenger YAML tag with handles attribute
AbstractExtension (Twig) twig.extension TwigBundle YAML tag entry

8. Limits of autoconfigure

autoconfigure is not a cure-all; there are scenarios where manual DI configuration remains necessary. Tags with complex attribute values that cannot be derived from the PHP code at compile time cannot be assigned through autoconfigure. Example: a tag whose attribute value comes from an environment variable that is not yet known at compile time. In such cases, services.yaml remains the only option.

Another limit concerns scope: autoconfigure operates on the entire service definition, not on individual methods. Attribute-based autoconfigure can read attributes applied to methods (as with #[AsEventListener] on methods), but that requires a dedicated registerAttributeForAutoconfiguration() registration that uses reflection. Complex registration logic, for example services that need to know about each other, still requires compiler passes. autoconfigure is a convenience feature for common patterns, not a replacement for full DI flexibility.

9. Comparison: manual tags vs. autoconfigure

The difference between manual tag configuration and autoconfigure shows most clearly in larger projects: anyone with twenty event subscribers needs zero tag entries in services.yaml when using autoconfigure. Anyone modeling the same thing without autoconfigure has twenty tag entries, and every class rename requires a change in YAML. That is the actual cost reduction: not just less code when creating something, but less maintenance effort with every refactoring.

On the other hand, the explicitness of YAML tags is sometimes an advantage: you can see at a glance which services are registered for which tags without having to read the code. With autoconfigure, this information is distributed across PHP classes. bin/console debug:container --tag=my_tag provides the same overview, but it requires actively querying for it. For teams that prefer transparency about registrations, this adjustment is an investment that pays off in the long run through reduced YAML maintenance.

Mironsoft

Symfony Dependency Injection, bundle development and DI architecture

Ready to modernize your Symfony DI configuration and shed YAML overhead?

We analyze existing services.yaml configurations, identify manual tags that can be replaced by autoconfigure, and migrate step by step to modern attribute-based DI, with full verification and tests.

DI Audit

Analysis of all manual tags in services.yaml and identification of autoconfigure migration candidates

Migration

Step-by-step transition to autoconfigure and PHP attributes with verification via debug:container

Bundle Development

Designing your own autoconfigure rules and PHP attributes for reusable bundle components

10. Summary

The autoconfigure feature in Symfony eliminates manual DI tag entries for all well-known extension points of the framework. Interface-based rules assign tags automatically during container compilation: EventSubscriberInterface, VoterInterface, MessageHandlerInterface and many others never need to be tagged in YAML again. PHP attributes such as #[AsEventListener] or #[AsCommand] extend this mechanism with parameters and turn the entire DI configuration into type-safe PHP code.

Custom autoconfigure rules can be defined via registerForAutoconfiguration() and registerAttributeForAutoconfiguration(), in compiler passes for projects and in the build() method for bundles. That makes autoconfigure a universal mechanism for zero-YAML DI in modern Symfony projects. bin/console debug:container remains the central tool for verification and debugging.

Symfony autoconfigure: the essentials at a glance

Activation

autoconfigure: true in the _defaults block of services.yaml, already active in standard Symfony projects. Verification via debug:container.

Interface Rules

Well-known interfaces such as EventSubscriberInterface and VoterInterface receive their tags automatically, no YAML required.

Custom Rules

registerForAutoconfiguration() and registerAttributeForAutoconfiguration() in compiler passes or bundle build() define custom rules.

Debugging

bin/console debug:container --tag=tag_name shows all services with that tag. debug:container App\MyClass shows all tags of a service.

11. FAQ: autoconfigure in Symfony

1What does autoconfigure do?
Assigns DI tags automatically based on interfaces and PHP attributes during container compilation. Eliminates manual tag entries in services.yaml for all well-known Symfony extension points.
2How do I enable it?
autoconfigure: true in _defaults of services.yaml, standard in Symfony since 4.4. Individual services can be excluded with autoconfigure: false.
3Which interfaces get tags?
EventSubscriberInterface → kernel.event_subscriber. VoterInterface → security.voter. MessageHandlerInterface → messenger.message_handler. AbstractExtension → twig.extension. And many more.
4Define custom rules?
registerForAutoconfiguration() in a compiler pass or build(). For attributes: registerAttributeForAutoconfiguration() with a closure. The closure receives ChildDefinition, the attribute instance and a Reflector.
5Check tag assignment?
bin/console debug:container App\MyService shows all tags. debug:container --tag=my_tag lists all services with that tag. The central debugging tool for autoconfigure questions.
6PHP attributes supported?
Yes, since Symfony 5.3. #[AsEventListener], #[AsCommand], #[AsMessageHandler] are autoconfigure triggers. Parameters in the attribute (priority, event name) are passed along to the tag.
7Limits of autoconfigure?
No tag assignment with values from environment variables (compile time). Complex service relationships still need compiler passes. No complete replacement for services.yaml.
8Problems in tests?
No, autoconfigure is only a compile feature. Services in tests have the same tags as in production. Test services can be given autoconfigure: false when YAML overrides are needed.
9Replace services.yaml entirely?
No, only tag entries for well-known interfaces become unnecessary. Arguments, parameters, aliases and factory definitions stay in services.yaml. The goal is eliminating tag duplication.
10Test custom rules?
Container integration test: create a ContainerBuilder, register the bundle, run the compiler passes, then check for the tag's presence. Alternatively: KernelTestCase plus debug:container for manual verification.