New in Symfony 6.3: Dependency Injection Improvements
May 25, 2023 • Published by Javier Eguiluz
Symfony 6.3 is backed by:
Warning: This post is about an unsupported Symfony version. Some of this information may be out of date. Read the most recent Symfony Docs.
The Service Container is the key feature that makes Symfony applications so fast and flexible. In Symfony 6.3 we've improved it with a lot of new features.
New Options for Autowire
Attribute
The Autowire attibute was introduced in Symfony 6.1 and allows to autowire
services, parameters and expressions. In Symfony 6.3, it can also autowire environment
variables (via the env
option). Also, parameters are now autowired using the
new param
option:
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 Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class MessageGenerator
{
public function __construct(
// ...
// when using 'param', you don't have to wrap the parameter name with '%'
#[Autowire(param: 'kernel.debug')]
bool $debugMode,
#[Autowire(env: 'SOME_ENV_VAR')]
string $senderName,
) {
}
// ...
}
Configure Aliases with Attributes
Service aliases allow you to use services using your own custom service ID
instead of the original ID given to the service. In Symfony 6.3 we're adding a
new #[AsAlias]
attribute so you can define aliases directly in your code:
1 2 3 4 5 6 7 8 9 10 11
// src/Mail/PhpMailer.php
namespace App\Mail;
// ...
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(id: 'app.mailer', public: true)]
class PhpMailer
{
// ...
}
When using #[AsAlias]
attribute, you may omit passing id
argument if the
service class implements exactly one interface. In those cases, the FQCN of the
interface will be used as the alias.
New Options for Autoconfigure
Attribute
When using some class as its own service factory, you can use the new
constructor
option of the #[Autoconfigure]
to define the name of the
class method that acts as its constructor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Email/NewsletterManager.php
namespace App\Email;
// ...
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(constructor: 'create')]
class NewsletterManager
{
private string $sender;
public static function create(
#[Autowire(param: 'app.newsletter_sender')] string $sender
): self
{
$newsletterManager = new self();
$newsletterManager->sender = $sender;
// ...
return $newsletterManager;
}
// ...
}
Nesting Related Attributes in Autowire
In Symfony 6.3 we've improved the #[Autowire]
attribute so you can nest
other autowiring-related attributes into it. The following example shows this
feature in action using all the available options:
1 2 3 4 5 6 7 8 9 10 11 12 13
#[AsDecorator(decorates: AsDecoratorFoo::class)]
class AutowireNestedAttributes implements AsDecoratorInterface
{
public function __construct(
#[Autowire([
'decorated' => new MapDecorated(),
'iterator' => new TaggedIterator('foo'),
'locator' => new TaggedLocator('foo'),
'service' => new Autowire(service: 'bar')
])] array $options)
{
}
}
Using Autowiring to generate Closures
In the Dependency Injection component, "service closures" are closures that
return a service. They come in handy when dealing with laziness on the consumer side.
In Symfony 6.3, we've added support for autowiring services as closures using
the #[AutowireServiceClosure]
attribute:
1 2
#[AutowireServiceClosure('my_service')]
Closure $serviceResolver
This will generate a closure that returns the service my_service
when called.
It's also quite common to have a service accept a closure with a specific
signature as argument. In Symfony 6.3, you can generate such closures using
the #[AutowireCallable]
attribute:
1 2
#[AutowireCallable(service: 'my_service', method: 'myMethod')]
Closure $callable
This will generate a closure that will have the same signature as the method, so
that calling it will forward to the myMethod()
on the service my_service
.
This type of closure-injection is not lazy by default: my_service
will be
instantiated when creating the $callable
argument. If you want to make it
lazy, you can use the lazy
option:
1 2
#[AutowireCallable(service: 'my_service', method: 'myMethod', lazy: true)]
Closure $callable
This will generate a closure that will instantiate the my_service
service
only when the closure is called.
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, and they can be used as type-hints.
The #[AutowireCallable]
attribute can be used to generate an adapter for a
functional interface. For example, if you have the following functional
interface:
1 2 3 4
interface UriExpanderInterface
{
public function expand(string $uri, array $parameters): string;
}
You can use the #[AutowireCallable]
attribute to generate an adapter for it:
1 2
#[AutowireCallable(service: 'my_service', method: 'myMethod')]
UriExpanderInterface $expander
Even if my_service
does not implement UriExpanderInterface
, the
$expander
argument will be an instance of UriExpanderInterface
generated
by Symfony. Calling its expand()
method will forward to the myMethod()
method of the my_service
service.
Support for generating such adapters in YAML, XML or PHP is also provided.
Autowiring Lazy Services
The #[Autowire]
attribute can be used to tell how services should be autowired.
In Symfony 6.3, we've added support for autowiring lazy services using the lazy
option:
1 2
#[Autowire(lazy: true)]
MyServiceInterface $service
This will generate a lazy service that will be instantiated only when the $service
argument is actually used. This works by generating a proxy class that implements
MyServiceInterface
and forwards all method calls to the actual service.
When targeting an argument with many possible types, you can use the lazy
option with a class-string value to specify which type should be generated:
1 2
#[Autowire(lazy: FirstServiceInterface::class)]
FirstServiceInterface|SecondServiceInterface $service
This will generate a proxy class that implements only FirstServiceInterface
.
This feature allows services that do not always consume their dependencies to initialize them only when actually needed. This can provide a significant performance boost, particularly with services that are expensive to initialize.
Deprecating Container Parameters
Another new feature related to the Service Container is that you can now deprecate parameters. This is useful e.g. for extensions who want to rename or remove parameters, so they can warn users about this future change before doing it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public function load(array $configs, ContainerBuilder $containerBuilder)
{
// ...
$containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']);
// the parameter to deprecate must be set before marking it as deprecated
$containerBuilder->deprecateParameter(
'acme_demo.database_user',
'acme/database-package',
'1.3',
// optionally you can set a custom deprecation message
'"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.'
);
}
Consider using this option if you want to transform your temporary parameters into build parameters, another new feature introduced in Symfony 6.3.
Exclude Classes with Attributes
When configuring services in the container, you can use the exclude
option
to tell Symfony to not create services to one or more classes. This option comes
in handy when excluding entire directories (e.g. src/Entity/
). However, if
you just want to exclude some specific classes, in Symfony 6.3 you can also do
that with the new #[Exclude]
attribute:
1 2 3 4 5 6 7 8 9 10
// src/Kernel.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
#[Exclude]
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}
Allow Extending the Autowire
Attribute
In Symfony 6.3, the #[Autowire]
attribute can be extended to create your own
custom autowiring helpers. For example, consider this example that creates a
custom attribute to autowire repositories:
1 2 3 4 5 6 7 8 9
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Repository extends Autowire
{
public function __construct(string $class)
{
parent::__construct(expression: \sprintf("service('some.repository.factory').create('%s')", $class));
}
}
And then, use it like this in your project:
1 2 3 4 5 6 7
/**
* @param ObjectRepository<User> $repository
*/
public function __construct(
#[Repository(User::class)] private ObjectRepository $repository
) {
}
Thanks to all contributors who improved Dependency Injection in Symfony 6.3 and special thanks to Nicolas Grekas who also contributed half of the contents of this blog post to explain the most advanced features.
Help the Symfony project!
As with any Open-Source project, contributing code or documentation is the most common way to help, but we also have a wide range of sponsoring opportunities.
Comments are closed.
To ensure that comments stay relevant, they are closed for old posts.
Just amazing ...
Is it possible or does it even make sense to use these attributes for configuration in a bundle?
It will be very useful for us! Keep it up