Сервис-контейнер

Ваше приложение полно полезных объектов: объект "Mailer" может помочь вам отправлять email'ы, в то время, как другой объект, может помочь вам сохранять данные базу данных. Почти все, что "делает" ваше приложение, на самом деле выполняется одним из этих объектов. И каждый раз, когда вы устанавливаете новый пакет, вы получаете доступ к новым объектам!

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

1
2
$logger = $container->get('logger');
$entityManager = $container->get('doctrine.orm.entity_manager');

Контейнер позволяет вам централизовать то, как создаются объектв. Он делает вашу жизнь легче, пропагандрует сильную архитектуру, а также он очень быстрый!

Вызов и использование сервисов

В тот момент, когда вы запускаете приложение Symfony, ваш контейнер уже содержит много сервисов. Они очень похожи на инструменты: ждут, пока вы восползуетесь ими. В вашем контроллере, вы можете "запросить" сервис из контейнера, путем типизирования аргумента с классом сервиса или именем интерфейса. Хотите сохранить что-то в журнал? Не проблема:

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

use Psr\Log\LoggerInterface;

/**
 * @Route("/products")
 */
public function listAction(LoggerInterface $logger)
{
    $logger->info('Look! I just used a service');

    // ...
}

New in version 3.3: Возможность типизировать сервис для его получения была добавлена в Symfony 3.3. См. главу о контроллере для большей информации.

Какие еще сервисы существуют? Узнайте, запустив:

1
$ php bin/console debug:container

Это только маленький образец того, что вы увидите:

ID сервиса Имя класса
doctrine Doctrine\Bundle\DoctrineBundle\Registry
filesystem Symfony\Component\Filesystem\Filesystem
form.factory Symfony\Component\Form\FormFactory
logger Symfony\Bridge\Monolog\Logger
request_stack Symfony\Component\HttpFoundation\RequestStack
router Symfony\Bundle\FrameworkBundle\Routing\Router
security.authorization_checker Symfony\Component\Security\Core\Authorization\AuthorizationChecker
security.password_encoder Symfony\Component\Security\Core\Encoder\UserPasswordEncoder
session Symfony\Component\HttpFoundation\Session\Session
translator Symfony\Component\Translation\DataCollectorTranslator
twig Twig_Environment
validator Symfony\Component\Validator\Validator\ValidatorInterface

Вы также можете использовать уникальный ID сервиса, чтобы получить доступ к сервису напрямую:

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

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class ProductController extends Controller
{
    /**
     * @Route("/products")
     */
    public function listAction()
    {
        $logger = $this->container->get('logger');
        $logger->info('Look! I just used a service');

        // ...
    }
}

Запрос сервиса напрямую из контейнера таким образом работает только при расширении класса Controller.

На протяжении всех документов, вы увидите, как использовать множество разных сервисов, которые живут в контейнере.

Погодите! Неужели все сервисы (объекты) запускаются по каждому запросу? Нет! Контейнер ленив: он не запускает сервис, пока (и в случае если) вы не попросите об этом. Например, если вы никогда не используете сервис validator во время запроса, контейнер никогда его и не запустит.

Создание/конфигурация сервисов в контейнере

Tip

Рекомендуемый способ конфигурации сервисов изменился в Symfony 3.3. Для более глубокого разъяснения, см. The Symfony 3.3 DI Container Changes Explained (autowiring, _defaults, etc).

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

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

class MessageGenerator
{
    public function getHappyMessage()
    {
        $messages = [
            'You did it! You updated the system! Amazing!',
            'That was one of the coolest updates I\'ve seen all day!',
            'Great work! Keep going!',
        ];

        $index = array_rand($messages);

        return $messages[$index];
    }
}

Поздравляем! Вы только что создали ваш первый класс сервисов! Вы можете использовать его сразу же в вашем контроллере:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use AppBundle\Service\MessageGenerator;

public function newAction(MessageGenerator $messageGenerator)
{
    // благодаря типизированию, контейнер вызовет
    // новый и передаст его вам!
    // ...

    $message = $messageGenerator->getHappyMessage();
    $this->addFlash('success', $message);
    // ...
}

Когда вы запросите сервис MessageGenerator, контейнер создаст новый объект MessageGenerator и вернет его (смотрите объяснение ниже). Но если вы никогда не запросите этот сервис, он никогда не будет создан: экономия памяти и скорости. В качестве бонуса, сервис MessageGenerator создается только единожды: каждый раз, когда вы запрашиваете его, вам возващается один и тот же экземпляр.

Документация предполагает, что вы используете конфигурацию
Symfony Standard Edition (version 3.3) services.yml. Наиболее важной частью является эта:
  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    # app/config/services.yml
    services:
        # конфигурация по умолчанию в *этом* файле
        _defaults:
            autowire: true
            autoconfigure: true
            public: false
    
        # делает классы в src/AppBundle доступными для использования
        #в качестве сервисов
        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}'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- Default configuration for services in *this* file -->
            <defaults autowire="true" autoconfigure="true" public="false" />
    
            <!-- Load services from whatever directories you want (you can update this!) -->
            <prototype namespace="AppBundle\" resource="../../src/AppBundle/*" exclude="../../src/AppBundle/{Entity,Repository}" />
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // app/config/services.php
    // _defaults and loading entire directories is not possible with PHP configuration
    // you need to define your services one-by-one
    use AppBundle\Service\MessageGenerator;
    
    $container->autowire(MessageGenerator::class)
        ->setAutoconfigured(true)
        ->setPublic(false);
    

Благодаря этой конфигурации, вы можете автоматически использовать любые классы из каталога src/AppBundle в качестве сервиса, и вам не нужно будет вручную конфигурировать их. Позже, вы узнаете больше об этом в Одномоментный импорт множества сервисов с помощью источника.

Если вы предпочитаете создавать сервис вручную, это также возможно: см. Ясная конфигурация сервисов и аргументов.

New in version 3.3: Ключ _defaults и возможность загружать сервисы из каталога были добавлены в Symfony 3.3.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use AppBundle\Service\MessageGenerator;

// получение доступа к сервисам таким образом работает только,
// если вы расширите контроллер
class ProductController extends Controller
{
    public function newAction()
    {
        // only works if your service is public
        $messageGenerator = $this->get(MessageGenerator::class);

        $message = $messageGenerator->getHappyMessage();
        $this->addFlash('success', $message);
        // ...
    }
}

Однако, это работает только если вы сделате ваш сервис общедоступным.

Caution

ID сервисов не чувствительны к регистру (т.е. AppBundle\Service\MessageGenerator и appbundle\service\messagegenerator ссылаются на один и тот же сервис). Но это было осуждено в Symfony 3.3. Начиная с версии 4.0, id сервисов будут чувствительны к регистру.

Внедрение сервисов/конфигурации в сервис

Что, если вам нужно получить доступ к сервису logger из MessageGenerator? Ваш сервис не имеет прямого доступа к контейнеру, так что вы не можете вызвать его через $this->container->get().

Не проблема! Вместо этого, создайте метод __construct() с аргументом $logger, который имеет типизирование LoggerInterface. Установите это на новом свойстве $logger и используйте его позже:

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

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function getHappyMessage()
    {
        $this->logger->info('About to find a happy message!');
        // ...
    }
}

Вот и все! Контейнер автоматически будет знать, что ему надо передать сервис logger при загруке MessageGenerator. Откуда он это знает? Autowiring. Ключом является титпизирование LoggerInterface в вашем методе __construct() и конфигурация autowire: true в services.yml. Когда вы типизируете аргумент, контейнер будет автоматически искать соответствующий сервис. Если это не удастся, вы увидите явное исключение с предложением помощи.

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

1
$ php bin/console debug:container --types

Это только малая часть того, что вы увидите:

Service ID Class name
Psr\Cache\CacheItemPoolInterface alias for "cache.app.recorder"
Psr\Log\LoggerInterface alias for "monolog.logger"
Symfony\Component\EventDispatcher\EventDispatcherInterface alias for "debug.event_dispatcher"
Symfony\Component\HttpFoundation\RequestStack alias for "request_stack"
Symfony\Component\HttpFoundation\Session\SessionInterface alias for "session"
Symfony\Component\Routing\RouterInterface alias for "router.default"

Управление множеством сервисов

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

 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
// src/AppBundle/Updates/SiteUpdateManager.php
namespace AppBundle\Updates;

use AppBundle\Service\MessageGenerator;

class SiteUpdateManager
{
    private $messageGenerator;
    private $mailer;

    public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer)
    {
        $this->messageGenerator = $messageGenerator;
        $this->mailer = $mailer;
    }

    public function notifyOfSiteUpdate()
    {
        $happyMessage = $this->messageGenerator->getHappyMessage();

        $message = \Swift_Message::newInstance()
            ->setSubject('Site update just happened!')
            ->setFrom('[email protected]')
            ->setTo('[email protected]')
            ->addPart(
                'Someone just updated the site. We told them: '.$happyMessage
            );
        $this->mailer->send($message);

        return $message;
    }
}

Это использует сервисы MessageGenerator и Swift_Mailer. Если вы загружаете сервисы из src/AppBundle, вы можете использовать сервис сразу же:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use AppBundle\Updates\SiteUpdateManager;

public function newAction(SiteUpdateManager $siteUpdateManager)
{
    // ...

    $message = $siteUpdateManager->notifyOfSiteUpdate();
    $this->addFlash('success', $message);
    // ...
}

Благодаря автоподключению и типизированию в __construct(), контейнер создает объект SiteUpdateManager и передает ему правильный аргумент. В большинстве случаев это работает идеально.

Подключение аргументов вручную

Но существуют некоторые случаи, когда аргумент не может быть автоматически подключен к сервису. Например, представьте, что вы хотите сделать email админа конфигурируемым:

 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/AppBundle/Updates/SiteUpdateManager.php
// ...

class SiteUpdateManager
{
    // ...
+    private $adminEmail;

-    public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer)
+    public function __construct(MessageGenerator $messageGenerator, \Swift_Mailer $mailer, $adminEmail)
    {
        // ...
+        $this->adminEmail = $adminEmail;
    }

    public function notifyOfSiteUpdate()
    {
        // ...

        $message = \Swift_Message::newInstance()
            // ...
-            ->setTo('[email protected]')
+            ->setTo($this->adminEmail)
            // ...
        ;
        // ...
    }
}

Если вы внесете это изменение и обновите его, вы увидите ошибку:

Cannot autowire service "AppBundleUpdatesSiteUpdateManager": argument "$adminEmail" of method "__construct()" must have a type-hint or be given a value explicitly. (Невозможно автоматически подключить сервис "AppBundleUpdatesSiteUpdateManager": аргумент "$adminEmail" метода method "__construct()" должен иметь типизирование или точно заданное значение).

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

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    # app/config/services.yml
    services:
        # ...
    
        # same as before
        AppBundle\:
            resource: '../../src/AppBundle/*'
            exclude: '../../src/AppBundle/{Entity,Repository}'
    
        # explicitly configure the service
        AppBundle\Updates\SiteUpdateManager:
            arguments:
                $adminEmail: '[email protected]'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <!-- Same as before -->
            <prototype namespace="AppBundle\" resource="../../src/AppBundle/*" exclude="../../src/AppBundle/{Entity,Repository}" />
    
            <!-- Explicitly configure the service -->
            <service id="AppBundle\Updates\SiteUpdateManager">
                <argument key="$adminEmail">[email protected]</argument>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // app/config/services.php
    use AppBundle\Updates\SiteUpdateManager;
    
    // _defaults и импортирование каталогов не работают в PHP
    // но точная регистрация сервиса - работает
    $container->autowire(SiteUpdateManager::class)
        ->setAutoconfigured(true)
        ->setPublic(false)
        ->setArgument('$adminEmail', '[email protected]');
    

New in version 3.3: Возможность конфигурировать аргумент по имени ($adminEmail) была добавлена в Symfony 3.3. Ранее, вы могли конфигурировать его только по индексу (2 в этом случае) или используя пустые кавычки для других аргументов.

Благодаря этому, контейнер будет передавать manager@example.com в качестве третьего аргумента __construct при создании сервиса SiteUpdateManager. Другие аргументы всееще будут автоматизированы.

Но разве это не зыбко? К счастью - нет! Если вы переименуете аргумент $adminEmail на что-то другое, например, $mainEmail, вы получите явное исключение при перезагрузке следующей страницы (даже если страницы не использует этот сервис).

Параметры сервиса

В дополнение к объектам сервиса, контейнер также содержит конфигурацию, под названием parameters. Чтобы создать параметр, добавьте его под ключом parameters и сошлитесь на него с помощью синтаксиса %parameter_name%:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # app/config/services.yml
    parameters:
        admin_email: [email protected]
    
    services:
        # ...
    
        AppBundle\Updates\SiteUpdateManager:
            arguments:
                $adminEmail: '%admin_email%'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="admin_email">[email protected]</parameter>
        </parameters>
    
        <services>
            <!-- ... -->
    
            <service id="AppBundle\Updates\SiteUpdateManager">
                <argument key="$adminEmail">%admin_email%</argument>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    // app/config/services.php
    use AppBundle\Updates\SiteUpdateManager;
    $container->setParameter('admin_email', '[email protected]');
    
    $container->autowire(SiteUpdateManager::class)
        // ...
        ->setArgument('$adminEmail', '%admin_email%');
    

На самом деле, как только вы определите параметр, на него можно сослаться с помощью синтаксиса %parameter_name% в любом другом файле конфигурации сервиса - типа config.yml. Многие параметры определены в parameters.yml file.

Вы также можете вызвать параметры напрямую из контейнера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function newAction()
{
    // ...

    // это работает ТОЛЬКО если вы расширите контроллер
    $adminEmail = $this->container->getParameter('admin_email');

    // или более короткий путь!
    // $adminEmail = $this->getParameter('admin_email');
}

Чтобы узнать больше о параметрах, см Introduction to Parameters.

Выбор особенного сервиса

Сервис MessageGenerator, созданные ранее, требует аргумент LoggerInterface:

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

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    // ...
}

Однако, существует множество сервисов в контейнере, которые имплементируют LoggerInterface, например, logger, monolog.logger.request, monolog.logger.php, и др. Как контейнер знает, какие использовать?

В таких ситуациях, контейнер обычно сконфигурирован так, чтобы автоматически выбирать один из сервисов - в этом случае logger (узнайте больше, почему в service-autowiring-alias-ru). Но,вы можете контролировать это и передать другой logger:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/services.yml
    services:
        # ... такой же код как раньше
    
        # точно сконфигурируйте сервис
        AppBundle\Service\MessageGenerator:
            arguments:
                $logger: '@monolog.logger.request'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... same code as before -->
    
            <!-- Explicitly configure the service -->
            <service id="AppBundle\Service\MessageGenerator">
                <argument key="$logger" type="service" id="monolog.logger.request" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // app/config/services.php
    use AppBundle\Service\MessageGenerator;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->autowire(MessageGenerator::class)
        ->setAutoconfigured(true)
        ->setPublic(false)
        ->setArgument('$logger', new Reference('monolog.logger.request'));
    

Это сообщает контейнеру, что аргумент $logger для __construct должен использовать сервис, id которого monolog.logger.request.

Tip

Символ @ оченьважен: это то, что сообщает контейнеру, что вы хотите передать сервис, id которого monolog.logger.request, а не просто строку monolog.logger.request.

Опция автоподключения

Выше, файл services.yml имеет autowire: true в разделе _defaults,так что это относится ко всем сервисам, определенным в этом файле. С этой настройкой, вы можете типизировать аргументы в методе __construct() ваших сервисов и контейнер автоматически будет передавать вам правильные аргументы. Эта вся запись была написана без автоматизации.

Чтобы узнать больше, см Defining Services Dependencies Automatically (Autowiring).

Опция автоконфигурации

New in version 3.3: Опция autoconfigure была добавлена в Symfony 3.3.

Выше, файл services.yml имеет autoconfigure: true в секции _defaults, так что это относится ко всем сервисам, определенным в этом файле. С этой настройкой, контейнер будет автоматически применять определенную конфигурацию к вашим сервисам, основываясь на классе вашего сервиса. Это чаще всего используется для авто-тегирования ваших сервисов.

Например, чтобы создать Twig Extension (Расширение Twig), вам нужно создать класс, зарегистрировать его в качестве сервиса, и тегировать его twig.extension:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/services.yml
    services:
        # ...
    
        AppBundle\Twig\MyTwigExtension:
            tags: [twig.extension]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <service id="AppBundle\Twig\MyTwigExtension">
                <tag name="twig.extension" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    // app/config/services.php
    use AppBundle\Twig\MyTwigExtension;
    
    $container->autowire(MyTwigExtension::class)
        ->addTag('twig.extension');
    

Однако с autoconfigure: true, вам не нужен тег. На самом деле, если вы используете Symfony Standard Edition services.yml config, вам не нужно ничего делать: сервис будет загружен автоматически. Потом, autoconfigure добавит тег twig.extension за вас, так как вас класс имплементирует Twig_ExtensionInterface. И благодаря``autowire``, вы даже можете добавить аргументы-конструкторы без какой-либо конфигурации.

Конечное же, вы все еще можете сконфигурировать сервис вручную, если вам это нужно.

Публичные сервисы против приватных

Благодаря разделу _defaults в services.yml, каждый сервис, определенный в этом файле, public: false по умолчанию:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/services.yml
    services:
        # конфигурация по умолчанию для сервисов в *этом* файле
        _defaults:
            # ...
            public: false
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- Default configuration for services in *this* file -->
            <defaults autowire="true" autoconfigure="true" public="false" />
        </services>
    </container>
    

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

1
2
3
4
5
6
7
8
9
use AppBundle\Service\MessageGenerator;

public function newAction(MessageGenerator $messageGenerator)
{
    // типизирование его в качестве аргумента РАБОТАЕТ

    // но доступ к нему напрямую из контейнера НЕ работает
    $this->container->get(MessageGenerator::class);
}

Обычно это нормально: существуют лучшие способы получить доступ к сервису. Но, если вам надо сделать ваш сервис публичным, просто перепишите эту установку:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/services.yml
    services:
        # ... same code as before
    
        # точно сконфигурируйте сервис
        AppBundle\Service\MessageGenerator:
            public: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... same code as before -->
    
            <!-- Explicitly configure the service -->
            <service id="AppBundle\Service\MessageGenerator" public="true"></service>
        </services>
    </container>
    

Одномоментный импорт множества сервисов с помощью источника

Вы уже видели, что вы можете импортировать множество сервисов одновременно, используя ключ resource. Например, конфигурация Symfony по умолчанию, содержит следующее:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    # app/config/services.yml
    services:
        # ...
    
        # префикс пространства имен для классов (должен заканчиваться \)
        AppBundle\:
            # принимает глобальный паттерн
            resource: '../../src/AppBundle/*'
            # исключить некоторые пути
            exclude: '../../src/AppBundle/{Entity,Repository}'
    
        # были импортированы выше, но мы хотим дополнительную конфигурацию
        AppBundle\Controller\:
            resource: '../../src/AppBundle/Controller'
            # применить какую-то конфигурацию к этим сервисам
            public: true
            tags: ['controller.service_arguments']
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <prototype namespace="AppBundle\" resource="../../src/AppBundle/*" exclude="../../src/AppBundle/{Entity,Repository}" />
    
            <prototype namespace="AppBundle\Controller\" resource="../../src/AppBundle/Controller" public="true">
                <tag name="controller.service_arguments" />
            </prototype>
        </services>
    </container>
    

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

Вы также можете exclude (исключить) определенные пути. Это не обязательно, но слегка увеличит производительность окружения dev: исключенные пути не отслеживаются, так что их изменение не приведет к перестройке контейнера.

Note

Погодите, это что, значит, что каждый класс в src/AppBundle зарегистрирован как сервис? Даже модель или классы сущностей? На самом деле, нет. Если у вас есть public: false в вашем ключе _defaults (или вы можете добавить его в конкретном импорте), все импортированные сервисы являются приватными. Благодаря этому, все классы в src/AppBundle, которые не явно используются как сервисы, автоматически удаляются из финального контейнера. В действительности, импорт просто означает, что все классы "доступны для использования в качестве сервисов, без необходимости ручной конфигурации.

Ясная конфигурация сервисов и аргументов

До появления Symfony 3.3, все сервисы и (обычно) аргументы, были ясно сконфигурированы: было невозможно загрузить сервисы автоматически и автоподключение было намного менее распространено.

Обе эти функции не обязательны. И даже если вы используете их, могут быть некоторые случае, когда вы захотите вручную подключить сервис. Например, представьте, что вы хотите зарегистрировать 2 сервиса для класса SiteUpdateManager - каждый с разным email админа. В этом случае, каждый должен иметь уникальный id сервиса:

  • YAML
     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
    # app/config/services.yml
    services:
        # ...
    
        # это id сервиса
        site_update_manager.superadmin:
            class: AppBundle\Updates\SiteUpdateManager
            # you CAN still use autowiring: we just want to show what it looks like without
            autowire: false
            # manually wire all arguments
            arguments:
                - '@AppBundle\Service\MessageGenerator'
                - '@mailer'
                - '[email protected]'
    
        site_update_manager.normal_users:
            class: AppBundle\Updates\SiteUpdateManager
            autowire: false
            arguments:
                - '@AppBundle\Service\MessageGenerator'
                - '@mailer'
                - '[email protected]'
    
        # создайте союзника, чтобы по умолчанию, если вы типизируете SiteUpdateManager,
        # использовался the site_update_manager.superadmin
        AppBundle\Updates\SiteUpdateManager: '@site_update_manager.superadmin'
    
  • XML
     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
    <!-- app/config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <!-- ... -->
    
            <service id="site_update_manager.superadmin" class="AppBundle\Updates\SiteUpdateManager" autowire="false">
                <argument type="service" id="AppBundle\Service\MessageGenerator" />
                <argument type="service" id="mailer" />
                <argument>[email protected]</argument>
            </service>
    
            <service id="site_update_manager.normal_users" class="AppBundle\Updates\SiteUpdateManager" autowire="false">
                <argument type="service" id="AppBundle\Service\MessageGenerator" />
                <argument type="service" id="mailer" />
                <argument>[email protected]</argument>
            </service>
    
            <alias id="AppBundle\Updates\SiteUpdateManager" service="site_update_manager.superadmin">
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // app/config/services.php
    use AppBundle\Updates\SiteUpdateManager;
    use AppBundle\Service\MessageGenerator;
    use Symfony\Component\DependencyInjection\Reference;
    
    $container->register('site_update_manager.superadmin', SiteUpdateManager::class)
        ->setAutowire(false)
        ->setArguments(array(
            new Reference(MessageGenerator::class),
            new Reference('mailer'),
            '[email protected]'
        ));
    
    $container->register('site_update_manager.normal_users', SiteUpdateManager::class)
        ->setAutowire(false)
        ->setArguments(array(
            new Reference(MessageGenerator::class),
            new Reference('mailer'),
            '[email protected]'
        ));
    
    $container->setAlias(SiteUpdateManager::class, 'site_update_manager.superadmin')
    

В этом случае, два сервиса зарегистрированы: site_update_manager.superadmin и site_update_manager.normal_users. Благодаря союзнику, если вы типизируете SiteUpdateManager будет передано первое (site_update_manager.superadmin). Если вы хотите передать второе, то вам нужно вручную подключить сервис.

Caution

Если вы не создадите союзника и загружаете все сервисы из src/AppBundle, тогда три сервиса будут созданы (автоматический сервис + два ваших), и автоматически загруженный сервис будет передан - по умолчанию - когда вы типизируете SiteUpdateManager. Поэтому создание союзника - хорошая идея.

Узнайте больше

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.