Ленивые сервисы

Дата обновления перевода 2024-07-27

Ленивые сервисы

See also

Другими способами ленивого внедрения сервисов являются замыкание сервиса или подписчик сервиса.

Почему ленивые сервисы?

В некоторых случаях, вы можете захотеть внедрить сервис, который немного тяжёлый для инстанциации, но не всегда используется внутри вашего объекта. Например, представьте, что у вас есть NewsletterManager и вы внедряете в него сервис mailer. Только несколько методов в вашем NewsletterManager действительно используют mailer, но даже когда он вам не нужн, сервис mailer всегда инстанциируется, чтобы построить ваш NewsletterManager.

Решением этого являются ленивые сервисы. С ленивым сервисом на самом деле внедряется "прокси" сервиса mailer. Он выглядит и ведёт себя точно так же, как mailer, кроме того, что mailer на самом деле не инстанциируется до того, как вы начнёте какое-либо взаимодействие с прокси.

Caution

Ленивые сервыси не поддерживают классы final, но вы можете использовать Проксификация интерфейса, чтобы обойти это ограничение.

В версиях PHP до 8.0, ленивые сервисы не поддерживают параметры со значениями по умолчанию для встроенных PHP-классов (например, PDO).

Конфигурация

Вы можете отметить сервис как lazy, изменив его определение:

1
2
3
4
# config/services.yaml
services:
    App\Twig\AppExtension:
        lazy: true

Как только вы внедрите сервис в другой сервис, должен быть внедрен ленивый объект ghost с такой же подписью класса, представляющей сервис. Ленивый объект ghost - это объект, который создается пустым и который может инициализировать сам себя, когда к нему получают доступ впервые. То же самое происходит при вызове
Container::get() напрямую.

Чтобы проверить, работает ли ваш прокси, вы можете просто проверить интерфейс полученного объекта:

1
2
dump(class_implements($service));
// вывод должен включать в себя "ProxyManager\Proxy\LazyLoadingInterface"

Вы также можете настроить ленивость вашего сервиса благодаря атрибуту Autoconfigure. Например, чтобы определить свой сервис как ленивый, используйте следующее:

1
2
3
4
5
6
7
8
9
10
namespace App\Twig;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Twig\Extension\ExtensionInterface;

#[Autoconfigure(lazy: true)]
class AppExtension implements ExtensionInterface
{
    // ...
}

Вы также можете конфигурировать ленивость, когда ваш сервис внедряется с помощью атрибута Autowire :

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Service;

use App\Twig\AppExtension;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        #[Autowire(service: 'app.twig.app_extension', lazy: true)] ExtensionInterface $extension
    ) {
        // ...
    }
}

Этот атрибут также позволяет определить интерфейсы для проксификации при использовании ленивости, и поддерживает ленивое автомонтирование типов объединений:

1
2
3
4
5
public function __construct(
    #[Autowire(service: 'foo', lazy: FooInterface::class)]
    FooInterface|BarInterface $foo,
) {
}

Другая возможность - использовать атрибут Lazy :

namespace AppTwig;

use SymfonyComponentDependencyInjectionAttributeLazy; use TwigExtensionExtensionInterface;

#[Lazy] class AppExtension implements ExtensionInterface { // ... }

Этот атрибут может быть применен как к классу, так и к параметрам, которые должны быть лениво загружены. Он определяет необязательный параметр, используемый для определения интерфейсов для типов прокси и пересечений:

1
2
3
4
5
public function __construct(
    #[Lazy(FooInterface::class)]
    FooInterface|BarInterface $foo,
) {
}

7.1

Атрибут #[Lazy] был представлен в Symfony 7.1.

Проксификация интерфейса

За кулисами, прокси, сгенерированные для ленивой загрузки сервисов, наследуют из класса, использованого сервисом. Однако, иногда это вообще невозможно (например, так как класс является final и не может быть расширен) или просто неудобно.

Чтобы обойти это ограничение, вы можете сконфигурировать прокси, чтобы он реализовывал только конкретные интерфейсы.

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    App\Twig\AppExtension:
        lazy: 'Twig\Extension\ExtensionInterface'
        # or a complete definition:
        lazy: true
        tags:
            - { name: 'proxy', interface: 'Twig\Extension\ExtensionInterface' }

Как и в разделе Конфигурация , вы можете использовать атрибут Autoconfigure для настройки интерфейса проксификации, передав его FQCN в качестве значения параметра lazy:

1
2
3
4
5
6
7
8
9
10
namespace App\Twig;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Twig\Extension\ExtensionInterface;

#[Autoconfigure(lazy: ExtensionInterface::class)]
class AppExtension implements ExtensionInterface
{
    // ...
}

Виртуальный прокси, внедрренный в другие сервисы, будет реализовывать только указанные интерфейсы и не будет расширять изначальный класс сервиса, что позволит ленивую загрузку севисов, используя классы final. Вы можете сконфигурировать прокси, чтобы реализовыватьь несколько интерфейсов, добавив новые теги "прокси".

Tip

Эта функция также может вести себя как надежная защита: учитывая, что прокси не расширяет изначальный класс, только методы, определенные интерфейсом, могут быть вызваны, что предотвращает вызов специфических методов реализации. Это также предовращает внедрение зависимости в целом, если вы добавили подсказку конкретной реализации, а не интерфейса.