Объяснение изменений контейнера DI в Symfony 3.3 (autowiring, _defaults, и др.)

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

Объяснение изменений контейнера DI в Symfony 3.3 (autowiring, _defaults, и др.)

Если вы посмотрите на файл services.yml в новом проекте Symfony 3.3, то вы заметите некоторые большие изменения: _defaults, autowiring, autoconfigure и другие. Эти функции созданы для автоматизации конфигурации и для того, чтобы разработка стала быстрее, не жертвуя предсказуемостью, которая очень важна! Ещё одна цель - заставить контроллеры и сервисы вести себя более последовательно. В Symfony 3.3, контроллеры являются сервисами по умолчанию.

Документация уже была обновлена так, чтобы предположить, что у вас включены эти новые функции. Если вы уже существующий пользователь Symfony и хотите понять "что" и "почему", стоящие за этими изменениями, то эта статья для вас!

Все измеения необязательны

Самое важное - сегодня вы можете обновиться до Symfony 3.3, не внося никаких изменений в ваше приложение. Symfony имеет строгое обещание обратной совместимости, что означает, что обновляться до упрощённых версий всегда безопасно.

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

Новый файл services.yml по умолчанию

Чтобы понять изменения, посмотрите на новый файл services.yml по умолчанию, в стандартной версии Symfony:

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
# app/config/services.yml
services:
    # конфигурация по умолчанию для сервисов в *этом* файле
    _defaults:
        # автоматически внедряет зависимости в ваших сервисах
        autowire: true
        # автоматически регистрирует ваши сервисы, как команды, подписчики событий и т.д.
        autoconfigure: true
        # это означает, что вы не можете вызывать сервисы напрямую из контейнера через $container->get()
        # если вам нужно сделать это, вы можете переопределить эту настройку в отдельных сервисах
        public: false

    # делает так, чтобы классы в src/AppBundle available были использованы, как сервисы
    # это создаёт по сервису на класс, id которых является полностью сертифицированным именем класса
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository}'

    # контроллеры импортируются отдельно, чтобы убедиться в том, что они публичны
    # и имеют тег, позволяющий действия по типизированию сервисов
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    # добавьте больше сервисов, или переопределите сервисы, которые требуют ручного монтажа
    # AppBundle\Service\ExampleService:
    #     аргументы:
    #         $someArgument: 'some_value'

Эта маленькая часть конфигурации содержит смену понятий того, как конфигурируются сервисы в Symfony.

1) Сервисы загружаются автоматически

See also

Прочтите документацию по автоматической загрузке сервисов .

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

1
2
3
4
5
6
7
8
9
10
11
# app/config/services.yml
services:
    # ...

    # делает классы в src/AppBundle доступными для использования в качестве сервисов
    # это создаёт по сервису на класс, id которых является полностью сертифицированным именем класса
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/{Entity,Repository}'

Это означает, что каждый класс в src/AppBundle/ доступен для использования в качестве сервиса. И благодаря части _defaults сверху файла, все эти сервисы являются автомонтируемым и приватными (т.е. public: false).

Id сервисов равняются имени класса (например, AppBundle\Service\InvoiceGenerator). И это ещё одно изменение, которое вы заметите в Symfony 3.3: мы рекомендуем вам исползовать имя класса в качестве id вашего сервиса, разве что у вас не множество сервисов для одного класса .

Но как контейнер может знать аргументы моего сервиса?

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

Но погодите, если у меня есть какие-то классы модели (не сервисы) в моём каталоге src/AppBundle/, разве это не означает, что они тоже будут зарегистрированы, как сервисы? Разве это не проблема?

На самом деле, это не проблема. Так как все новые сервисы приватные (благодаря _defaults), то есть какие-то из сервисов не используются в вашем коде, они будут автоматически удалены из скомпилированного контейнера. Это означает, что число сервисов в вашем контейнере должно быть одинаковым независимо от того, ясно ли вы сконфигурируете каждый сервис, или загрузите их все одномоментно с помощью этого метода.

Ладно, но могу ли я исключить некоторые пути, которые я знаю, не содержат сервисов?

Да! Ключ exclude - это глобальный шаблон, который можно использовать для внесения в черный список тех путей, которые вы не хотите включать, как сервисы. Но, так как неиспользуемые сервисы автоматически удаляются из контейнера, exclude не так уж и важен. Наибольшим преимуществом является то, что эти пути не отслеживаются контейнером, и поэтому могут привести к тому, что контейнеру нужно будет реже перестраиваться в окружении dev.

2) Автомонтаж по умолчанию: использование типизироване вместо id сервиса

Второе большое изменение - автомонтирование включено (через _defaults) для всех зарегистрированных вами сервисов. Это также означает, что id сервисов теперь менее важны, а "типы" (т.е. имена класса или интерфеса) теперь более важны.

Например, до Symfony 3.3 (и это все еще разрешается), вы могли передать один сервис в качестве аргумента к другому со следующей конфигурацией:

1
2
3
4
5
6
7
8
9
# app/config/services.yml
services:
    app.invoice_generator:
        class: AppBundle\Service\InvoiceGenerator

    app.invoice_mailer:
        class: AppBundle\Service\InvoiceMailer
        arguments:
            - '@app.invoice_generator'

Чтобы передать InvoiceGenerator в качестве аргумента к InvoiceMailer, вам нужно было указать id сервиса, как аргумент: app.invoice_generator. Id сервисов были главным способом конфигурации вещей.

Но в Symfony 3.3, благодаря автомонтированию, всё, что вам надо сделать - это типизировать аргумент с помощью InvoiceGenerator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Service/InvoiceMailer.php
// ...

class InvoiceMailer
{
    private $generator;

    public function __construct(InvoiceGenerator $generator)
    {
        $this->generator = $generator
    }

    // ...
}

Вот и всё! Оба сервиса автоматически зарегистрированы и установлены для автомонтированя. Без какой-либо конфигурации, контейнер знает, что надо передать автоматически зарегистрированный AppBundle\Service\InvoiceGenerator в качестве первого аргумента. Как вы видите, тип класса - AppBundle\Service\InvoiceGenerator - это самое важное, не id. Вы запрашиваете экземпляр конкретного типа и контейнер автоматически передаёт вам правильный сервис.

Ну разве не магия? Как он знает, какой именно сервис передать мне? Что, если у меня множество сервисов одного экземпляра?

Система автомонтирования была создана так, чтобы быть очень предсказуемов. Вначале она работает путём поиска сервиса, iв которого точно совпадает с типизацией. Это означает, что вы полностью контролируете, какое типизирование ведёт к какому сервису. Вы даже можете использовать союзников сервисов, чтобы получить больше контроля. Если у вас множесво сервисов конкретного типа, вы выбираете, какие из них должны быть использованы для автомонтирования. Чтобы узнать все детали о логике автомонтирования, см. .

Но что, если у меня скалярный (например, строка) аргумент? Как он автомонтируется?

Если у вас есть аргумент, который не является объетом, то он не может быть автоматически смонтирован. Но это не страшно! Symfony предоставит вам чёткое исключение (при последующем обновлении любой страницы), сообщающее вам, какой аргумент какого сервиса не мог быть автосмонтирован. Чтобы исправить это, вы можете вручную сконфигурировать *только* этот один аргумент . В этом заключается философия автомонтирования: конфигурировать только те части, которые вам необходимы. Большинство конфигураций автоматизированы.

Ладно, но автомонтирование делае ваше приложение менее стабильным. Если вы измените одну вещь, или сделаете ошибку, могут случиться непредвиденные вещи. Разве это не проблема?

Symfony всегда ценила стабильность, безопасность и предсказуемость в первую очередь. Автомонтирование было создано с мыслью об этом. В особенности:

  • Если естьпроблема с монтированием любого аргумента к любому сервису, выдаётся чёткое исключение при последующем обновлении любой страницы, даже если вы не используете этот сревис на этой странице. Это мощно: невозможно сделать ошибку автомонтирования и не заметить этого.
  • Контейнер определяет, какой сервис передать в подробной манере: он ищет сервис, id которого точно совпадает с типизированием. Он не сканирует все сервисы, в поисках объектов, которые имеют этот класс или интерфейс (на самом деле, он делает это в Symfony 3.3, но это было осуждено. Если вы полагаетесь на это, то вы увидите ясное предупреждение о возражении).

Автомонтирование стремится автоматизировать конфигурацию без магии.

3) Контроллеры регистрируются, как сервисы

Третье большое изменение состоит в том, что в новом проекте Symfony 3.3, ваши контроллеры - это сервисы:

1
2
3
4
5
6
7
8
9
10
# app/config/services.yml
services:
    # ...

    # контроллеры импортируются отдельно, чтобы убедиться в том, что они публичные
    # и имеют тег, который позволяет действия по типизированию сервисов
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

Однако, вы этого можете дажене заметить. Во-первых, ваши контроллеры всё ещё могут расширять тот же базовый класс Controller или новый. Это означает, что у вас есть доступ ко всем тем же шорткатам, что и раньше. Кроме того, аннотация @Route и синтаксис _controller (например,AppBundle:Default:homepage), используемые в маршрутизации, автоматически будут использовать ваш контроллер в качестве сервиса (до тех пор, пока id сервиса будет совпадать с именем класса, что верно в этом случае). Смотрите Как определять контроллеры как сервисы, чтобы узнать больше деталей. Вы даже можете создать вызываемые контроллеры

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

Чтобы ещё больше облегчить вам жизнь, теперь возможно автомонтировать аргументы в ваших методах действий контроллера, так же, как вы можете это сделать с конструктором сервисов. Например:

1
2
3
4
5
6
7
8
9
use Psr\Log\LoggerInterface;

class InvoiceController extends Controller
{
    public function listAction(LoggerInterface $logger)
    {
        $logger->info('A new way to access services!');
    }
}

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

В общем, новой наилучшей практикой является использование нормального конструктора внедрения зависимости (или внедрения "действия" в контроллерах), вместо вызова публичных сервисов через $this->get() (хотя это всё ещё работает).

4) Автотегирование с автоконфигурацией

Последнее большое изменение - это ключ autoconfigure, который установлен как true в _defaults. Благодаря этому, контейнер будет автоматически тегировать сервисы, зарегистрированные в этом файле. Например, представьте, что вы хотите создать подписчика событий. Для начала, вы создаёте класс:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/AppBundle/EventSubscriber/SetHeaderSusbcriber.php
// ...

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SetHeaderSusbcriber implements EventSubscriberInterface
{
    public function onKernelResponse(FilterResponseEvent $event)
    {
        $event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config');
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::RESPONSE => 'onKernelResponse'
        ];
    }
}

Отлично! В Symfony 3.2 или более ранних версиях, вам бы сейчас понадобилось регистрировать это как сервис в services.yml и тегировать с помощью kernel.event_subscriber. В Symfony 3.3, вы уже закончили! Сервис зарегистрирован автоматически. И благодаря autoconfigure, Symfony автоматически тегирует сервис, так как он реализует EventSubscriberInterface.

Это звучит как магия - он автоматически тегирует мои сервисы?

В этом случае, вы создали класс, который реализует EventSubscriberInterface и зарегистрировали его в качестве сервиса. Это более, чем достаточно для того, чтобы контейнер знал, что вы хотите использовать это в качестве подписчика событий: дальнейшая конфигурация не требуется. А система тегирования - это собвственный механизм Symfony. И конечно же, вы всегда можете установить autowire по умолчанию в значенни "false" в services.yml, или отключить его для конкретного сервиса.

Это значит, что теги умерли? Это работает для всех тегов?

Это не работает для всех тегов. Многие теги имеют обязательные атрибуты, как слушатели событий, когда вам также нужно указать имя события и метод в вашем теге. Автоконфигурация работает только для тегов, которые не имеют обязательных атрибутов тега, и когда вы прочтёте документы по функции, из них вы узнаете, требуется ли тег. Вы также можете посмотреть классы расширений (например, FrameworkExtension для 3.3.0), чтобы увидеть, что они конфигурируют автоматически.

Что если мне надо добавить в моей тег приоритетность?

Множество автоматически сокнфигурированных тегов имеют необязательную приоритетность. Если вам нужно указать приоритет (или любой другой необязательный атрибут тега), то это не проблема! Просто сконфигурируйте ваш сервис вручную и добавьте тег. Вам тег будет превосходить по значимости тот, что был добавлен автоконфигурацией.

Что насчёт производительности

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

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

В очень больших проектах это может стать проблемой. Если так случится, вы всегда можете выбрать не использвать автомонтирование. Если вы думаете, что система перестройки кеша могла бы быть умнее в какой-то ситуации, пожалуйста, создайте вопрос!

Обновление до новой конфигурации Symfony 3.3

Готовы обновить ваш существующий проект? Отлично! Представьте, что у вас есть следующая конфигурация:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/config/services.yml
services:
    app.github_notifier:
        class: AppBundle\Service\GitHubNotifier
        arguments:
            - '@app.api_client_github'

    markdown_transformer:
        class: AppBundle\Service\MarkdownTransformer

    app.api_client_github:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://api.github.com'

    app.api_client_sl_connect:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://connect.sensiolabs.com/api'

Это необязательно, но давайте обновим это до конфигурации Symfony 3.3 шаг за шагом, не сломав наше приложение.

Шаг 1): Добавление _defaults

Начните с добавления раздела _defaults с autowire и autoconfigure.

1
2
3
4
5
6
7
# app/config/services.yml
services:
+     _defaults:
+         autowire: true
+         autoconfigure: true

    # ...

Этот шаг очень простой: вы уже ясно конфигурируете все ваши сервисы. Так что autowire не делает ничего. Вы также уже тегируете все ваши сервисы, так что autoconfigure тоже не изменяет никакие существующие сервисы.

Вы пока ещё не добавили public: false. Это случится через минуту.

Шаг 2) Использование id сервисов класса

На данный момент, id сервисов - это машинные имена, например, app.github_notifier. Чтобы хорошо работать с новой системой конфигурации, ваши id сервисов должны быть именами класса, кроме тех случаев, когда у вас множество экземпляров одного и того же сервиса.

Начните с обновления id сервисов до имён класса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app/config/services.yml
services:
    # ...

-     app.github_notifier:
-         class: AppBundle\Service\GitHubNotifier
+     AppBundle\Service\GitHubNotifier:
        arguments:
            - '@app.api_client_github'

-     markdown_transformer:
-         class: AppBundle\Service\MarkdownTransformer
+     AppBundle\Service\MarkdownTransformer: ~

    # оставьте id, так как существует множество экземпляров в классе
    app.api_client_github:
        # ...
    app.api_client_sl_connect:
        # ...

Но это изменение сломает наше приложение! Старые id сервисов (например, app.github_notifier) больше не существуют. Самый простой способ исправить это - найти все ваши старые id сервисов, и обновить их до новых id класса: app.github_notifier до AppBundle\Service\GitHubNotifier.

В больших проектах есть способ получше: создайте союзнические наследования, которые свяжут старый id с новым. Создайте файл legacy_aliases.yml:

1
2
3
4
5
6
7
# app/config/legacy_aliases.yml
services:
    # союзники для того, чтобы к старым id сервисов оставался доступ
    # удалите их если/когда вы не вызываете их напрямую
    # из контейнера через $container->get()
    app.github_notifier: '@AppBundle\Service\GitHubNotifier'
    markdown_transformer: '@AppBundle\Service\MarkdownTransformer'

Потом, импортируйте это сверху services.yml:

1
2
3
4
5
# app/config/services.yml
+ imports:
+     - { resource: legacy_aliases.yml }

# ...

Вот и всё! Старые id сервисов всё ещё работают. Позже, (см. шаг по очистке ниже), вы можете удалить их из своего приложения.

Шаг 3) Сделайте сервисы приватными

Теперь вы готовы сделать так, чтобы все сервисы по умолчанию были приватными:

1
2
3
4
5
6
7
8
# app/config/services.yml
# ...

services:
     _defaults:
         autowire: true
         autoconfigure: true
+          public: false

Благодаря этому, любые сервисы, созданные в этом файле, не могут быть вызваны напрямую из контейнера. Однако, так как старые id сервиса являются союзниками в отдельном файле (legacy_aliases.yml), они всё ещё являются публичными. Таким образом приложение продолжает работать.

Если вы не изменили id некоторых из ваших сервисов (потому что они являются множеством экземпляров одного класса), вам может понадобиться сделать их публичными:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/config/services.yml
# ...

services:
    # ...

    app.api_client_github:
        # ...

+         # удалите это если/когда вы не вызываете это
+         # напрямую из контейнера через $container->get()
+         public: true

    app.api_client_sl_connect:
        # ...
+         public: true

Это для того, чтобы гарантировать, что приложение не сломается. Если вы не вызываете эти сервисы напрямую из контейнера - это не нужно. Через минуту вы это очистите.

Шаг 4) Авторегистрация сервисов

Теперь вы готовы автоматически регистрировать все сервисы в src/AppBundle/ (и/или любом другом каталоге/пакете, который у вас есть):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/config/services.yml

services:
    _defaults:
        # ...

+     AppBundle\:
+         resource: '../../src/AppBundle/*'
+         exclude: '../../src/AppBundle/{Entity,Repository}'
+
+     AppBundle\Controller\:
+         resource: '../../src/AppBundle/Controller'
+         public: true
+         tags: ['controller.service_arguments']

    # ...

Вот и всё! На самом деле, вы уже переопределяете и переконфигурируете все сервисы, которые вы используете (AppBundle\Service\GitHubNotifier и AppBundle\Service\MarkdownTransformer). Но теперь, вам не нужно будет вручную регистрировать будущие сервисы.

Ещё раз, существует дополнительное осложнение, если у вас есть множественные сервисы одного класса:

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

services:
    # ...

+     # союзник ApiClient одного из ваших сервисов ниже
+     # app.api_client_github будет использован для автомонтирования типизирований ApiClient
+     AppBundle\Service\ApiClient: '@app.api_client_github'

    app.api_client_github:
        # ...
    app.api_client_sl_connect:
        # ...

Это гарантирует, что если вы попробуете автоматически смонтировать экземпляр ApiClient, будет использован app.api_client_github. Если у вас этого нет, функция авторегистрации попробует зарегистрировать третий сервис ApiClient и исползовать его для автомонтирования (которое не удастся, так как класс имеет не поддающийся автомонтированию аргумент).

Шаг 5) Очистка!

Чтобы убедиться в том, что ваше приложение не сломалось, вы проделали излишнюю работу. Теперь пора немного убраться! Для начала, обновите ваше приложение так, чтобы оно не использовало старые id сервисов (те, которые в legacy_aliases.yml). Это означает обновление любых аргументов сервиса (например, с @app.github_notifier на @AppBundle\Service\GitHubNotifier) и обновление вашего кода так, чтобы он не вызывал этот сервис напрямую из контейнера. Например:

1
2
3
4
5
6
7
8
9
-     public function indexAction()
+     public function indexAction(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer)
    {
-         // старый способ вызова сервисов
-         $githubNotifier = $this->container->get('app.github_notifier');
-         $markdownTransformer = $this->container->get('markdown_transformer');

        // ...
    }

Как только вы это сделаете, вы можете удалить legacy_aliases.yml и удалить его импорт. Вы должны сделать то же самое с любыми сервисами, которые вы сделали публичными, такими как app.api_client_github и app.api_client_sl_connect. Как только вы перестанете вызывать их напрямую из контейнера, вы можете удалить метку public: true:

1
2
3
4
5
6
7
8
9
10
11
# app/config/services.yml
services:
    # ...

    app.api_client_github:
        # ...
-         public: true

    app.api_client_sl_connect:
        # ...
-         public: true

Наконец, вы можете по желанию удалить любые сервисы из services.yml, аргументы которых могут быть автоматически смонтированы. Финальная конфигурация выглядит так:

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
services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    AppBundle\:
        resource: '../../src/AppBundle/*'
        exclude: '../../src/AppBundle/{Entity,Repository}'

    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    AppBundle\Service\GitHubNotifier:
        # это можно удалить, или я могу быть ясным
        arguments:
            - '@app.api_client_github'

    # союзник ApiClient одного из наших сервисов ниже
    # app.api_client_github будет использован для автомонтирования типизирований ApiClient
    AppBundle\Service\ApiClient: '@app.api_client_github'

    # оставьте эти id, так как существует множество экземпляров одного класса
    app.api_client_github:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://api.github.com'

    app.api_client_sl_connect:
        class: AppBundle\Service\ApiClient
        arguments:
            - 'https://connect.sensiolabs.com/api'

Теперь вы можете пользоваться преимуществами новых функций в будущем.