Skip to content

Defining Service Dependencies Automatically (Autowiring)

Edit this page

Autowiring allows you to manage services in the container with minimal configuration. It reads the type-hints on your constructor (or other methods) and automatically passes the correct services to each method. Symfony's autowiring is designed to be predictable: if it is not absolutely clear which dependency should be passed, you'll see an actionable exception.

Tip

Thanks to Symfony's compiled container, there is no runtime overhead for using autowiring.

An Autowiring Example

Imagine you're building an application that shows random happy messages to users and that these messages are formatted before displaying them.

Start by creating a formatter class:

1
2
3
4
5
6
7
8
9
10
// src/Formatter/TextFormatter.php
namespace App\Formatter;

class TextFormatter
{
    public function format(string $message): string
    {
        return wordwrap($message, 70);
    }
}

And now a message generator using this formatter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Service/MessageGenerator.php
namespace App\Service;

use App\Formatter\TextFormatter;

class MessageGenerator
{
    public function __construct(
        private TextFormatter $formatter,
    ) {
    }

    public function getHappyMessage(): string
    {
        $messages = [
            // ...
        ];

        return $this->formatter->format($messages[array_rand($messages)]);
    }
}

If you're using the default services.yaml configuration, both classes are automatically registered as services and configured to be autowired. This means you can use them immediately without any configuration.

However, to understand autowiring better, the following examples explicitly configure both services:

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
    # ...

    App\Service\MessageGenerator:
        # redundant thanks to _defaults, but value is overridable on each service
        autowire: true

    App\Formatter\TextFormatter:
        autowire: true

Now, you can use the MessageGenerator service immediately in a controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Controller/DefaultController.php
namespace App\Controller;

use App\Service\MessageGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DefaultController extends AbstractController
{
    #[Route('/message')]
    public function message(MessageGenerator $messageGenerator): Response
    {
        $message = $messageGenerator->getHappyMessage();

        // ...
    }
}

This works automatically! The container knows to pass the TextFormatter service as the first argument when creating the MessageGenerator service.

Note

In this example, the MessageGenerator service is injected as an argument of the controller action method. This is a special convenience feature of the framework, available only in controllers: in all other services, dependencies can only be autowired in the constructor and in the methods and properties marked with #[Required]. See Controller for more details.

Autowiring Logic Explained

Autowiring works by reading the TextFormatter type-hint in MessageGenerator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Service/MessageGenerator.php
namespace App\Service;

use App\Formatter\TextFormatter;

class MessageGenerator
{
    // ...

    public function __construct(
        private TextFormatter $formatter,
    ) {
    }
}

The autowiring system looks for a service whose id exactly matches the type-hint: so App\Formatter\TextFormatter. In this case, that exists! When you configured the TextFormatter service, you used its fully-qualified class name as its id. Autowiring isn't magic: it looks for a service whose id matches the type-hint. If you load services automatically, each service's id is its class name.

If there is not a service whose id exactly matches the type, a clear exception will be thrown.

Autowiring is a great way to automate configuration, and Symfony tries to be as predictable and as clear as possible.

Using Aliases to Enable Autowiring

The main way to configure autowiring is to create a service whose id exactly matches its class. In the previous example, the service's id is App\Formatter\TextFormatter, which allows to autowire this type automatically.

This can also be accomplished using an alias: a definition that gives an additional name to an existing service. Suppose that for some reason, the id of the service was instead app.text_formatter. In this case, any arguments type-hinted with the class name (App\Formatter\TextFormatter) can no longer be autowired.

No problem! To fix this, you can create a service whose id matches the class by adding a service alias:

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    # ...

    # the id is not a class, so it won't be used for autowiring
    app.text_formatter:
        class: App\Formatter\TextFormatter
        # ...

    # but this fixes it!
    # the "app.text_formatter" service will be injected when
    # an App\Formatter\TextFormatter type-hint is detected
    App\Formatter\TextFormatter: '@app.text_formatter'

This creates a service "alias", whose id is App\Formatter\TextFormatter. Thanks to this, autowiring sees this and uses it whenever the TextFormatter class is type-hinted.

Tip

Aliases are used by the core bundles to allow services to be autowired. For example, MonologBundle creates a service whose id is logger. But it also adds an alias: Psr\Log\LoggerInterface that points to the logger service. This is why arguments type-hinted with Psr\Log\LoggerInterface can be autowired.

Tip

In YAML, you can also use a shortcut to alias a service:

1
2
3
4
# config/services.yaml
services:
    # ...
    app.text_formatter: '@App\Formatter\TextFormatter'

The #[AsAlias] Attribute

Instead of defining the alias in a configuration file, you can use the #[AsAlias] attribute on the class of the service:

1
2
3
4
5
6
7
8
9
10
// src/Formatter/TextFormatter.php
namespace App\Formatter;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('app.text_formatter')]
class TextFormatter
{
    // ...
}

The attribute also allows you to make the alias public by setting its public option to true.

Tip

When using the #[AsAlias] attribute, you may omit passing the id argument if the class implements exactly one interface. In that case, the interface name becomes the alias of the service:

1
2
3
4
5
6
7
8
9
10
11
// src/Formatter/TextFormatter.php
namespace App\Formatter;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

// this defines FormatterInterface as an alias of this service
#[AsAlias]
class TextFormatter implements FormatterInterface
{
    // ...
}

The #[AsAlias] attribute can also be limited to one or more specific config environments using the when argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Formatter/TextFormatter.php
namespace App\Formatter;

// ...
use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(id: 'app.text_formatter', when: 'dev')]
class TextFormatter
{
    // ...
}

// pass an array to apply it in multiple config environments
#[AsAlias(id: 'app.text_formatter', when: ['dev', 'test'])]
class TextFormatter
{
    // ...
}

You can also use the target parameter to wire a specific implementation via named autowiring. This is useful when you have multiple implementations of the same interface and need to inject a specific one based on context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Formatter/TextFormatter.php
namespace App\Formatter;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(FormatterInterface::class, target: 'textFormatter')]
class TextFormatter implements FormatterInterface
{
    // ...
}

// src/Formatter/HtmlFormatter.php
namespace App\Formatter;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias(FormatterInterface::class, target: 'htmlFormatter')]
class HtmlFormatter implements FormatterInterface
{
    // ...
}

Then use the #[Target] attribute to inject the desired implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace App\Service;

use App\Formatter\FormatterInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class MessageGenerator
{
    public function __construct(
        #[Target('textFormatter')]
        private FormatterInterface $formatter,
    ) {
    }
}

class NewsletterGenerator
{
    public function __construct(
        #[Target('htmlFormatter')]
        private FormatterInterface $formatter,
    ) {
    }
}

8.1

The target parameter for the #[AsAlias] attribute was introduced in Symfony 8.1.

Tip

Aliases can be deprecated to warn about their removal in future versions. See how to deprecate services and aliases.

Working with Interfaces

You might also find yourself type-hinting abstractions (e.g. interfaces) instead of concrete classes as it replaces your dependencies with other objects. To follow this best practice, suppose you decide to create a FormatterInterface:

1
2
3
4
5
6
7
// src/Formatter/FormatterInterface.php
namespace App\Formatter;

interface FormatterInterface
{
    public function format(string $message): string;
}

Then, you update TextFormatter to implement it:

1
2
3
4
5
// ...
class TextFormatter implements FormatterInterface
{
    // ...
}

Now that you have an interface, you should use this as your type-hint:

1
2
3
4
5
6
7
8
9
10
class MessageGenerator
{
    public function __construct(
        private FormatterInterface $formatter,
    ) {
        // ...
    }

    // ...
}

But now, the type-hint (App\Formatter\FormatterInterface) no longer matches the id of the service (App\Formatter\TextFormatter). This means that the argument can no longer be autowired.

To fix that, add an alias:

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ...

    App\Formatter\TextFormatter: ~

    # the App\Formatter\TextFormatter service will be injected when
    # an App\Formatter\FormatterInterface type-hint is detected
    App\Formatter\FormatterInterface: '@App\Formatter\TextFormatter'

Thanks to the App\Formatter\FormatterInterface alias, the autowiring subsystem knows that the App\Formatter\TextFormatter service should be injected when dealing with the FormatterInterface.

Tip

When loading services automatically with resource, if only one service is discovered that implements an interface, configuring the alias is not mandatory and Symfony will automatically create one.

Tip

Autowiring is powerful enough to guess which service to inject even when using union and intersection types. This means you're able to type-hint argument with complex types like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;

class DataFormatter
{
    public function __construct(
        private (NormalizerInterface&DenormalizerInterface)|SerializerInterface $transformer,
    ) {
        // ...
    }

    // ...
}

Dealing with Multiple Implementations of the Same Type

Suppose you create a second class called HtmlFormatter that implements FormatterInterface:

1
2
3
4
5
6
7
8
9
10
// src/Formatter/HtmlFormatter.php
namespace App\Formatter;

class HtmlFormatter implements FormatterInterface
{
    public function format(string $message): string
    {
        return sprintf('<p>%s</p>', $message);
    }
}

If you register this as a service, you now have two services that implement the App\Formatter\FormatterInterface type. The autowiring subsystem can not decide which one to use. Remember, autowiring isn't magic; it looks for a service whose id matches the type-hint. So you need to choose one by creating an alias from the type to the correct service id. That alias defines the default implementation: the one injected whenever the interface is type-hinted.

For the rest of the implementations, define a named autowiring alias: an alias whose id is a special string containing the interface followed by a name of your choice, formatted like a PHP variable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# config/services.yaml
services:
    # ...

    App\Formatter\TextFormatter: ~
    App\Formatter\HtmlFormatter: ~

    # this creates a named autowiring alias called 'htmlFormatter'
    # for the HtmlFormatter implementation
    App\Formatter\FormatterInterface $htmlFormatter: '@App\Formatter\HtmlFormatter'

    # this defines TextFormatter as the default implementation,
    # injected whenever the FormatterInterface type-hint is detected
    App\Formatter\FormatterInterface: '@App\Formatter\TextFormatter'

Then, add the #[Target] attribute with the name of the named alias to the arguments where you want to inject a non-default implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Service/NewsletterGenerator.php
namespace App\Service;

use App\Formatter\FormatterInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class NewsletterGenerator
{
    public function __construct(
        #[Target('htmlFormatter')]
        private FormatterInterface $formatter,
    ) {
    }

    public function generate(string $contents): string
    {
        return $this->formatter->format($contents);
    }
}

The #[Target] attribute keeps the argument name separate from any implementation name and, in addition, you'll get an exception in case you make any typo in the target name.

Warning

The #[Target] attribute only accepts the name of a named autowiring alias; it does not accept service ids or service aliases.

Tip

Since the #[Target] attribute normalizes the string passed to it to its camelCased form, name variations (e.g. html.formatter) also work.

Note

Some IDEs will show an error when using #[Target] as in the previous example: "Attribute cannot be applied to a property because it does not contain the 'Attribute::TARGET_PROPERTY' flag". The reason is that thanks to PHP constructor promotion this constructor argument is both a parameter and a class property. You can safely ignore this error message.

You can get a list of named autowiring aliases by running the debug:autowiring command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ php bin/console debug:autowiring LoggerInterface

Autowirable Types
=================

 The following classes & interfaces can be used as type-hints when autowiring:
 (only showing classes/interfaces matching LoggerInterface)

 Describes a logger instance.
 Psr\Log\LoggerInterface - alias:monolog.logger
 Psr\Log\LoggerInterface $assetMapperLogger - target:asset_mapperLogger - alias:monolog.logger.asset_mapper
 Psr\Log\LoggerInterface $cacheLogger - alias:monolog.logger.cache
 Psr\Log\LoggerInterface $httpClientLogger - target:http_clientLogger - alias:monolog.logger.http_client
 Psr\Log\LoggerInterface $mailerLogger - alias:monolog.logger.mailer

 [...]

Injecting the Service Based on the Argument Name

Named autowiring aliases also work without the #[Target] attribute: when an argument is type-hinted with the interface and its name matches the name of a named alias, the aliased service is injected automatically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Service/NewsletterGenerator.php
namespace App\Service;

use App\Formatter\FormatterInterface;

class NewsletterGenerator
{
    public function __construct(
        // the argument name matches the 'htmlFormatter' named alias,
        // so the container injects the HtmlFormatter service
        private FormatterInterface $htmlFormatter,
    ) {
    }

    // ...
}

8.1

Relying solely on the parameter name to match a named autowiring alias (i.e. without using the #[Target] attribute) is deprecated since Symfony 8.1. Always use #[Target] to select a named autowiring alias.

This wiring is fragile because it depends on the name of a PHP variable. If you later rename the argument, the container won't fail; it will silently inject the default implementation instead, which can create bugs that are hard to find. That's why the #[Target] attribute is the recommended way of injecting non-default implementations.

Registering Named Autowiring Aliases from PHP Code

If you need to register named autowiring aliases from PHP code (e.g. in a compiler pass or a bundle extension), use the registerAliasForArgument() method:

1
2
3
4
5
6
7
use App\Formatter\FormatterInterface;

$container->registerAliasForArgument(
    'app.html_formatter',       // the service id
    FormatterInterface::class,  // the type-hint
    'htmlFormatter'             // the argument name
);

This registers the alias FormatterInterface $htmlFormatter, so any argument type-hinted with FormatterInterface and named $htmlFormatter will receive the app.html_formatter service.

You can also pass a fourth argument to define a distinct name for the #[Target] attribute, separate from the argument name:

1
2
3
4
5
6
$container->registerAliasForArgument(
    'app.html_formatter',
    FormatterInterface::class,
    'htmlFormatter',  // the argument name
    'html'            // the #[Target] name
);

This allows the service to be injected using #[Target('html')] on any argument, regardless of its name.

Fixing Non-Autowireable Arguments

Autowiring only works when your argument is an object. But if you have a scalar argument (e.g. a string), this cannot be autowired: Symfony will throw a clear exception.

To fix this, you can manually wire the problematic argument in the service configuration. You wire up only the difficult arguments, Symfony takes care of the rest.

You can also use the #[Autowire] parameter attribute to instruct the autowiring logic about those arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        #[Autowire(service: 'monolog.logger.request')]
        private LoggerInterface $logger,
    ) {
        // ...
    }
}

The #[Autowire] attribute can also be used for container parameters, complex expressions and even environment variables, including env variable processors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        // use the %...% syntax for container parameters
        #[Autowire('%kernel.project_dir%/data')]
        private string $dataDir,

        // or use argument "param"
        #[Autowire(param: 'kernel.debug')]
        private bool $debugMode,

        // expressions
        #[Autowire(expression: 'service("App\\\\Mail\\\\MailerConfiguration").getMailerMethod()')]
        private string $mailerMethod,

        // environment variables
        #[Autowire(env: 'SOME_ENV_VAR')]
        private string $senderName,

        // environment variables with processors
        #[Autowire(env: 'bool:SOME_BOOL_ENV_VAR')]
        private bool $allowAttachments,
    ) {
    }
}

Autowiring Refreshable Environment Variables

8.1

Autowiring environment variables as Closure or Stringable was introduced in Symfony 8.1.

When you inject an environment variable as a plain string, its value is resolved once and baked into the compiled container, so it never changes at runtime. That is fine for traditional request/response applications, but it is a limitation in long-running workers (Messenger, FrankenPHP, RoadRunner), where you may want the value to refresh between requests after a call to resetEnvCache().

To support this, #[Autowire(env: ...)] (or any #[Autowire('...%env(...)%...')] string value) can target a parameter typed Closure or Stringable. Instead of a fixed string, you receive a wrapper that resolves the environment reference every time the closure is invoked or the value is cast to a string, returning the current value on each call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        // invoke the closure to read the current value: ($this->dbUrl)()
        #[Autowire(env: 'DB_URL')]
        private \Closure $dbUrl,

        // the union type is required so the string default type-checks;
        // 'default' is returned when APP_NAME is not defined
        #[Autowire(env: 'APP_NAME')]
        private string|\Stringable $appName = 'default',

        // embedded %env(...)% references inside a string also work;
        // cast to string to read the current value: (string) $this->redisDsn
        #[Autowire('redis://%env(HOST)%:%env(PORT)%')]
        private \Stringable $redisDsn,

        // %param% references are resolved through the parameter bag
        #[Autowire('%dsn_template%')]
        private \Stringable $dsn,
    ) {
    }
}

When a default value is declared, it is returned while the environment variable is not defined. The default can be of any type for a Closure, but it must be a string (or null) for a Stringable.

The same wiring is available in configuration files through the !env_closure YAML tag and the EnvClosureArgument PHP class. Passing true as the third argument switches the wrapper from a Closure to a Stringable:

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    App\Service\MessageGenerator:
        arguments:
            - !env_closure '%env(DB_URL)%'
            # with a default value (returns a Closure)
            - !env_closure ['%env(APP_NAME)%', 'default']
            # Stringable wrapper instead of a Closure (third argument: true)
            - !env_closure ['%env(APP_NAME)%', 'default', true]
1
2
3
4
5
6
7
8
9
10
11
12
// config/services.php
use App\Service\MessageGenerator;
use Symfony\Component\DependencyInjection\Argument\EnvClosureArgument;

return function (ContainerConfigurator $container): void {
    $container->services()
        ->set(MessageGenerator::class)
            ->args([
                new EnvClosureArgument('%env(DB_URL)%'),
                new EnvClosureArgument('%env(APP_NAME)%', 'default', true),
            ]);
};

Note

Wrapping a plain %parameter% value works too, but only the %env(...)% portions of the resolved value stay refreshable; literal parameter values are fixed at compile time.

Binding Arguments by Name or Type

Instead of configuring the same argument service by service, you can use the bind keyword to bind arguments by name or type for all the services defined in a configuration file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# config/services.yaml
services:
    _defaults:
        bind:
            # pass this value to any $adminEmail argument for any service
            # that's defined in this file (including controller arguments)
            $adminEmail: 'manager@example.com'

            # pass this service to any $requestLogger argument for any
            # service that's defined in this file
            $requestLogger: '@monolog.logger.request'

            # pass this service for any LoggerInterface type-hint for any
            # service that's defined in this file
            Psr\Log\LoggerInterface: '@monolog.logger.request'

            # optionally you can define both the name and type of the argument to match
            string $adminEmail: 'manager@example.com'
            Psr\Log\LoggerInterface $requestLogger: '@monolog.logger.request'
            iterable $rules: !tagged_iterator app.foo.rule

    # ...

Tip

The bind config can also be applied to specific services or when loading many services at once.

Generating Closures with Autowiring

A service closure is an anonymous function that returns a service. This type of instantiation is handy when you are dealing with lazy-loading. It is also useful for non-shared service dependencies. If you want to delay the instantiation of a service without changing the code that uses it, inject it as a lazy service instead; if you need a whole set of services that may not be known in advance, use a service locator.

Automatically creating a closure encapsulating the service instantiation can be done with the AutowireServiceClosure attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/Service/Remote/MessageFormatter.php
namespace App\Service\Remote;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('third_party.remote_message_formatter')]
class MessageFormatter
{
    public function __construct()
    {
        // ...
    }

    public function format(string $message): string
    {
        // ...
    }
}

// src/Service/MessageGenerator.php
namespace App\Service;

use App\Service\Remote\MessageFormatter;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;

class MessageGenerator
{
    public function __construct(
        #[AutowireServiceClosure('third_party.remote_message_formatter')]
        private \Closure $messageFormatterResolver,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->messageFormatterResolver)()->format($message);

        // ...
    }
}

It is common that a service accepts a closure with a specific signature. In this case, you can use the AutowireCallable attribute to generate a closure with the same signature as a specific method of a service. When this closure is called, it will pass all its arguments to the underlying service function. If the closure needs to be called more than once, the service instance is reused for repeated calls. Unlike a service closure, this will not create extra instances of a non-shared service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class MessageGenerator
{
    public function __construct(
        #[AutowireCallable(service: 'third_party.remote_message_formatter', method: 'format')]
        private \Closure $formatCallable,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->formatCallable)($message);

        // ...
    }
}

Finally, you can pass the lazy: true option to the AutowireCallable attribute. By doing so, the callable will automatically be lazy, which means that the encapsulated service will be instantiated only at the closure's first call.

AutowireMethodOf

The AutowireMethodOf attribute provides a simpler way of specifying the name of the service method by using the property name as method name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;

class MessageGenerator
{
    public function __construct(
        #[AutowireMethodOf('third_party.remote_message_formatter')]
        private \Closure $format,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->format)($message);

        // ...
    }
}

The #[AutowireMethodOf] attribute is particularly useful in the following scenarios:

Decoupling from heavy dependencies: Instead of injecting an entire repository or service, you can inject only the specific method you need. This makes your code more modular and explicit about its dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Controller/ConferenceController.php
namespace App\Controller;

use App\Repository\CommentRepository;
use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;

class ConferenceController
{
    public function __construct(
        // instead of injecting the entire repository...
        // private CommentRepository $commentRepository,

        // ...inject only the method you need
        #[AutowireMethodOf(CommentRepository::class)]
        private \Closure $getCommentPaginator,
    ) {
    }

    public function show(Conference $conference, int $page): Response
    {
        $paginator = ($this->getCommentPaginator)($conference, $page);

        // ...
    }
}

Simplified testing: When you inject closures instead of full services, testing becomes simpler because you can replace the closure with a test double without complex mocking frameworks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// tests/Controller/ConferenceControllerTest.php
namespace App\Tests\Controller;

use App\Controller\ConferenceController;

class ConferenceControllerTest extends TestCase
{
    public function testShow(): void
    {
        // create a mock closure instead of mocking the entire repository
        $getCommentPaginator = function (Conference $conference, int $page) {
            return new MockPaginator([/* test comments */]);
        };

        $controller = new ConferenceController($getCommentPaginator);

        // Test your controller...
    }
}

Using interfaces for type safety: For better IDE support and static analysis, you can define a functional interface and use it as the parameter type instead of \Closure:

1
2
3
4
5
6
7
8
9
10
// src/Repository/Function/GetCommentPaginatorInterface.php
namespace App\Repository\Function;

use App\Entity\Conference;
use Doctrine\ORM\Tools\Pagination\Paginator;

interface GetCommentPaginatorInterface
{
    public function __invoke(Conference $conference, int $page): Paginator;
}

Then use this interface in your service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Controller/ConferenceController.php
namespace App\Controller;

use App\Repository\CommentRepository;
use App\Repository\Function\GetCommentPaginatorInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;

class ConferenceController
{
    public function __construct(
        #[AutowireMethodOf(CommentRepository::class)]
        private GetCommentPaginatorInterface $getCommentPaginator,
    ) {
    }

    public function show(Conference $conference, int $page): Response
    {
        // Call directly without parentheses around the property
        $paginator = ($this->getCommentPaginator)($conference, $page);

        // ...
    }
}

Injecting Service Closures in Configuration Files

Service closures can also be defined in configuration files, without using any attribute in the service class. The service is instantiated the first time the closure is called, while all subsequent calls return the same instance, unless the service is not shared:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Service/MyService.php
namespace App\Service;

use Symfony\Component\Mailer\MailerInterface;

class MyService
{
    /**
     * @param callable(): MailerInterface
     */
    public function __construct(
        private \Closure $mailer,
    ) {
    }

    public function doSomething(): void
    {
        // ...

        $this->getMailer()->send($email);
    }

    private function getMailer(): MailerInterface
    {
        return ($this->mailer)();
    }
}

To define a service closure and inject it to another service, create an argument of type service_closure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# config/services.yaml
services:
    App\Service\MyService:
        arguments: [!service_closure '@mailer']

        # In case the dependency is optional
        # arguments: [!service_closure '@?mailer']

    # you can also use the special '@>' syntax as a shortcut of '!service_closure'
    App\Service\AnotherService:
        arguments: ['@>mailer']

        # the shortcut also works for optional dependencies
        # arguments: ['@>?mailer']

If the injected closure is not meant to return a service but to be called by your class, use the closure argument type instead. It wraps any callable service into a closure:

1
2
3
4
5
6
7
8
9
10
// src/Hash/MessageHashGenerator.php
namespace App\Hash;

class MessageHashGenerator
{
    public function __invoke(): string
    {
        // compute and return a message hash
    }
}
1
2
3
4
5
# config/services.yaml
services:
    App\Service\MessageGenerator:
        arguments:
            $generateMessageHash: !closure '@App\Hash\MessageHashGenerator'

See also

Another way to inject services lazily is via a service locator.

Using a Service Closure in a Compiler Pass

In compiler passes you can create a service closure by wrapping the service reference into an instance of ServiceClosureArgument:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

public function process(ContainerBuilder $container): void
{
    // ...

    $myService->addArgument(new ServiceClosureArgument(new Reference('mailer')));
}

Generating Adapters for Functional Interfaces

Functional interfaces are interfaces with a single method. They are conceptually very similar to a closure except that their only method has a name. Moreover, they can be used as type-hints across your code.

The AutowireCallable attribute can be used to generate an adapter for a functional interface. Let's say you have the following functional interface:

1
2
3
4
5
6
7
// src/Service/MessageFormatterInterface.php
namespace App\Service;

interface MessageFormatterInterface
{
    public function format(string $message, array $parameters): string;
}

You also have a service that defines many methods and one of them is the same format() method of the previous interface:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Service/MessageUtils.php
namespace App\Service;

class MessageUtils
{
    // other methods...

    public function format(string $message, array $parameters): string
    {
        // ...
    }
}

Thanks to the #[AutowireCallable] attribute, you can now inject this MessageUtils service as a functional interface implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Service\Mail;

use App\Service\MessageFormatterInterface;
use App\Service\MessageUtils;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class Mailer
{
    public function __construct(
        #[AutowireCallable(service: MessageUtils::class, method: 'format')]
        private MessageFormatterInterface $formatter
    ) {
    }

    public function sendMail(string $message, array $parameters): string
    {
        $formattedMessage = $this->formatter->format($message, $parameters);

        // ...
    }
}

Instead of using the #[AutowireCallable] attribute, you can also generate an adapter for a functional interface through configuration:

1
2
3
4
5
6
7
8
# config/services.yaml
services:

    # ...

    app.message_formatter:
        class: App\Service\MessageFormatterInterface
        from_callable: [!service {class: 'App\Service\MessageUtils'}, 'format']

By doing so, Symfony will generate a class (also called an adapter) implementing MessageFormatterInterface that will forward calls of MessageFormatterInterface::format() to your underlying service's method MessageUtils::format(), with all its arguments.

The #[AutowireInline] Attribute

Sometimes a dependency is only used by one service and doesn't need to be registered as a full service. In those cases, you can define an anonymous service and inject it with the AutowireInline attribute, directly next to the corresponding argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __construct(
    #[AutowireInline(
        class: [ScopingHttpClient::class, 'forBaseUri'],
        arguments: [
            '$baseUri' => 'https://api.example.com',
            '$defaultOptions' => [
                'auth_bearer' => '%env(EXAMPLE_TOKEN)%',
            ],
        ]
    )]
    private HttpClientInterface $client,
) {
}

This example tells Symfony to inject an object created by calling the ScopingHttpClient::forBaseUri() factory with the specified base URI and default options. This is just one example: you can use the #[AutowireInline] attribute to define any kind of anonymous service.

While this approach is convenient for simple service definitions, consider moving complex or heavily configured services to a configuration file to ease maintenance.

Autowiring Other Methods (e.g. Setters and Public Typed Properties)

When autowiring is enabled for a service, you can also configure the container to call methods on your class when it's instantiated. For example, suppose you want to inject the logger service, and decide to use setter-injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Formatter/TextFormatter.php
namespace App\Formatter;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\Attribute\Required;

class TextFormatter
{
    private LoggerInterface $logger;

    #[Required]
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function format(string $message): string
    {
        $this->logger->info('Formatting '.$message);
        // ...
    }
}

Autowiring will automatically call any method with the #[Required] attribute, autowiring each argument. If you need to manually wire some of the arguments to a method, you can always explicitly configure the method call.

If a class declares several #[Required] methods, the container calls them in the order in which they are declared. Use the priority argument to change that order: methods with a higher priority are called first and methods with the same priority keep their declaration order:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Formatter/TextFormatter.php
namespace App\Formatter;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\Attribute\Required;
use Symfony\Contracts\Translation\TranslatorInterface;

class TextFormatter
{
    private LoggerInterface $logger;
    private TranslatorInterface $translator;

    // the default priority is 0, so this method is called after setLogger()
    #[Required]
    public function setTranslator(TranslatorInterface $translator): void
    {
        $this->translator = $translator;
    }

    // this method is called first because it has a higher priority
    #[Required(priority: 10)]
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
}

8.2

The priority argument of #[Required] was introduced in Symfony 8.2. It requires symfony/service-contracts 3.7 or later.

Note

Immutable setters (#[Required] methods that return static) are always called before the other required methods, regardless of their priority. The priority argument only sorts them relative to each other.

Despite property injection having some drawbacks, autowiring with #[Required] can also be applied to public typed properties. The service to inject is resolved in the same way as a constructor argument: the property type defines the service to look for (and you can add the #[Target] attribute to inject a named autowiring alias). Then, right after creating your service, the container assigns the resolved service directly to the property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace App\Formatter;

use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\Attribute\Required;

class TextFormatter
{
    // the container resolves the service associated with the LoggerInterface
    // type and runs the equivalent of '$textFormatter->logger = $logger;'
    // right after instantiating the TextFormatter object
    #[Required]
    public LoggerInterface $logger;

    public function format(string $message): string
    {
        $this->logger->info('Formatting '.$message);
        // ...
    }
}

The priority argument has no effect on properties: the container always sets them in the order in which they are declared.

Performance Consequences

Thanks to Symfony's compiled container, there is no performance penalty for using autowiring. However, there is a small performance penalty in the dev environment, as the container may be rebuilt more often as you modify classes. If rebuilding your container is slow (possible on very large projects), you may not be able to use autowiring.

Note

Public bundles should explicitly configure their services and not rely on autowiring, because they have no control over the service container of the applications they are included in. See Best Practices for Reusable Bundles for more details.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version