Skip to content

[DependencyInjection] Add support for generating lazy closures #18347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions reference/attributes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ Dependency Injection
* :ref:`Autoconfigure <lazy-services_configuration>`
* :ref:`AutoconfigureTag <di-instanceof>`
* :ref:`Autowire <autowire-attribute>`
* :ref:`AutowireCallable <container_closure-as-argument>`
* :ref:`AutowireCallable <autowiring_closures>`
* :doc:`AutowireDecorated </service_container/service_decoration>`
* :doc:`AutowireServiceClosure </service_container/service_closures>`
* :ref:`AutowireServiceClosure <autowiring_closures>`
* :ref:`Exclude <service-psr4-loader>`
* :ref:`TaggedIterator <tags_reference-tagged-services>`
* :ref:`TaggedLocator <service-subscribers-locators_defining-service-locator>`
Expand Down
50 changes: 5 additions & 45 deletions service_container.rst
Original file line number Diff line number Diff line change
Expand Up @@ -780,54 +780,14 @@ Our configuration looks like this:
;
};

.. versionadded:: 6.1

The ``closure`` argument type was introduced in Symfony 6.1.

It is also possible to convert a callable into an injected closure
thanks to the
:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable`
attribute. Let's say our ``MessageHashGenerator`` class now has a ``generate()``
method::

// src/Hash/MessageHashGenerator.php
namespace App\Hash;

class MessageHashGenerator
{
public function generate(): string
{
// Compute and return a message hash
}
}
.. seealso::

We can inject the ``generate()`` method of the ``MessageHashGenerator``
like this::

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

use App\Hash\MessageHashGenerator;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
Closures can be injected :ref:`by using autowiring <autowiring_closures>`
and its dedicated attributes.

class MessageGenerator
{
public function __construct(
private LoggerInterface $logger,
#[AutowireCallable(service: MessageHashGenerator::class, method: 'generate')]
private \Closure $generateMessageHash
) {
// ...
}

// ...
}

.. versionadded:: 6.3
.. versionadded:: 6.1

The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable`
attribute was introduced in Symfony 6.3.
The ``closure`` argument type was introduced in Symfony 6.1.

.. _services-binding:

Expand Down
98 changes: 98 additions & 0 deletions service_container/autowiring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,104 @@ The ``#[Autowire]`` attribute can also be used for :ref:`parameters <service-par

The ``param`` and ``env`` arguments were introduced in Symfony 6.3.

.. _autowiring_closures:

Generate Closures With Autowiring
---------------------------------

A **service closure** is an anonymous function that returns a service. This type
of instanciation is handy when you are dealing with lazy-loading.

Automatically creating a closure encapsulating the service instanciation can be
done with the
:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireServiceClosure`
attribute::

// 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')]
\Closure $messageFormatterResolver
) {
}

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

// ...
}
}

.. versionadded:: 6.3

The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireServiceClosure`
attribute was introduced in Symfony 6.3.

It is common that a service accepts a closure with a specific signature.
In this case, you can use the
:class:`Symfony\Component\DependencyInjection\Attribute\\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::

// 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')]
\Closure $formatCallable
) {
}

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

// ...
}
}

Finally, you can pass the ``lazy: true`` option to the
:class:`Symfony\Component\DependencyInjection\Attribute\\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.

.. versionadded:: 6.3

The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireCallable`
attribute was introduced in Symfony 6.3.

.. _autowiring-calls:

Autowiring other Methods (e.g. Setters and Public Typed Properties)
Expand Down
42 changes: 5 additions & 37 deletions service_container/service_closures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,45 +92,13 @@ argument of type ``service_closure``:

.. seealso::

Another way to inject services lazily is via a
:doc:`service locator </service_container/service_subscribers_locators>`.

Thanks to the
:class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireServiceClosure`
attribute, defining a service wrapped in a closure can directly be done
in the service class, without further configuration::

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

use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;
use Symfony\Component\Mailer\MailerInterface;

class MyService
{
public function __construct(
#[AutowireServiceClosure('mailer')]
private \Closure $mailer
) {
}

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

$this->getMailer()->send($email);
}
Service closures can be injected :ref:`by using autowiring <autowiring_closures>`
and its dedicated attributes.

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

.. versionadded:: 6.3
.. seealso::

The :class:`Symfony\\Component\\DependencyInjection\\Attribute\\AutowireServiceClosure`
attribute was introduced in Symfony 6.3.
Another way to inject services lazily is via a
:doc:`service locator </service_container/service_subscribers_locators>`.

Using a Service Closure in a Compiler Pass
------------------------------------------
Expand Down