Компиляция контейнера

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

Компиляция контейнера

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

Он компилируется путём вызова:

1
$container->compile();

Метод компиляции использует Передачи компилятора для компиляции. Компонент DependencyInjection поставляется с несколькими передачами, которые автоматически регистрируются для компиляции. Например, CheckDefinitionValidityPass проверяет на наличие различных проблем определений, установленных в контейнере. После этой и нескольких других передач, проверяющих валидность контейнера, последующие передачи используются для оптимизации конфигурации перед её кешированием. Например, приватные и абстрактные сервисы удаляются, а псевдонимы разрешаются.

Управление конфигурацией с расширениями

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

Расширения должны реализовать ExtensionInterface и могут быть зарегистрированы в контейнере с помощью:

1
$container->registerExtension($extension);

Главная работа расширения проходит в методе load(). В методе load() вы можете загружать конфигурацию из одного или более файлов конфигурации, а также манипулировать определениями контейнера, используя методы, как показано в Как работать с объектами определений сервиса.

Методу load() передаётся свежий контейнер для установки, который потом слияется с контейнером, в котором он зарегистрирован. Это позволяет вам иметь несколько расширений, управляющих определениями контейнера независимо. Расширения не добавляют в конфигурацию контейнеров при добавлении, но обрабатываются при вызове метода контейнера compile().

Очень простое расширение может просто загружать файлы конфигурации в контейнер:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

class AcmeDemoExtension implements ExtensionInterface
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.xml');
    }

    // ...
}

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

Расширение должно указывать метод getAlias() для реализации интерфейса:

1
2
3
4
5
6
7
8
9
10
11
// ...

class AcmeDemoExtension implements ExtensionInterface
{
    // ...

    public function getAlias()
    {
        return 'acme_demo';
    }
}

Для файлов конфигурации YAML, указание псевдонима расширения в качестве ключа будет означать, что эти значения передаются методу расширения load():

1
2
3
4
# ...
acme_demo:
    foo: fooValue
    bar: barValue

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

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$containerBuilder = new ContainerBuilder();
$containerBuilder->registerExtension(new AcmeDemoExtension);

$loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__));
$loader->load('config.yaml');

// ...
$containerBuilder->compile();

Note

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

Значения из этих разделов файлов конфигурации передаются в первый аргумент метода расширения load():

1
2
3
4
5
public function load(array $configs, ContainerBuilder $container)
{
    $foo = $configs[0]['foo']; //fooValue
    $bar = $configs[0]['bar']; //barValue
}

Аргумент $configs - это массив, содержащий каждый отличный файл конфигурации, который был загружен в контейнер. Вы загружаете только один файл конфигурации в примере выше, но он всё равно будет в массиве. Массив будет выглядеть так:

1
2
3
4
5
6
[
    [
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ],
]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Config\Definition\Processor;
// ...

public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $foo = $config['foo']; //fooValue
    $bar = $config['bar']; //barValue

    // ...
}

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

1
2
3
4
5
6
7
8
9
public function getXsdValidationBasePath()
{
    return __DIR__.'/../Resources/config/';
}

public function getNamespace()
{
    return 'http://www.example.com/symfony/schema/';
}

Note

XSD валидация необязательна, возвращение false из метода getXsdValidationBasePath() отключит её.

XML версия конфигурации будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme-demo="http://www.example.com/schema/dic/acme_demo"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://www.example.com/schema/dic/acme_demo
        https://www.example.com/schema/dic/acme_demo/acme_demo-1.0.xsd"
>
    <acme-demo:config>
        <acme_demo:foo>fooValue</acme_demo:foo>
        <acme_demo:bar>barValue</acme_demo:bar>
    </acme-demo:config>
</container>

Note

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

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

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $container->setParameter('acme_demo.FOO', $config['foo']);

    // ...
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $loader = new XmlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.xml');

    if ($config['advanced']) {
        $loader->load('advanced.xml');
    }
}

Note

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

1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\ContainerBuilder;

$containerBuilder = new ContainerBuilder();
$extension = new AcmeDemoExtension();
$containerBuilder->registerExtension($extension);
$containerBuilder->loadFromExtension($extension->getAlias());
$containerBuilder->compile();

Note

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

Добавление к конфигурации, переданной расширению

Расширение может добавлять к конфигурации любого пакета перед вызовом метода load(), реализуя PrependExtensionInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
// ...

class AcmeDemoExtension implements ExtensionInterface, PrependExtensionInterface
{
    // ...

    public function prepend(ContainerBuilder $container)
    {
        // ...

        $container->prependExtensionConfig($name, $config);

        // ...
    }
}

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

Выполнение кода во время компиляции

Вы также можете выполнить пользовательский код во время компиляции, написав вашу собственную передачу компилятора. Реализовав CompilerPassInterface в вашем расширении, добавленный метод process() будет вызван во время компиляции:

1
2
3
4
5
6
7
8
9
10
11
12
// ...
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
       // ... сделать что-то во время компиляции
    }

    // ...
}

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

Параметрами и определениями контейнера можно манипулровать, используя методы, описанные в Как работать с объектами определений сервиса.

Note

Пожалуйста, отметьте, что метод process() в классе расширения вызывается во время шага оптимизации. Вы можете прочитать следующий раздел , если вам нужно редактировать контейнер во время другого шага.

Note

Возьмите себе за правило только работать с определениями сервисов в передаче компилятора, а не создавать экземпляры сервиса. На практике, это означает использование методов has(), findDefinition(), getDefinition(), setDefinition(), и др., вместо get(), set(), и др.

Tip

Убедитесь в том, что ваша передача компилятора не требует существования сервисов. Прервите вызов метода, если какой-то из требуемых сервисов недоступен.

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

Создание отдельных передач компилятора

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

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class CustomPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
       // ... сделать что-то во время компиляции
    }
}

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

1
2
3
4
use Symfony\Component\DependencyInjection\ContainerBuilder;

$containerBuilder = new ContainerBuilder();
$containerBuilder->addCompilerPass(new CustomPass());

Note

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

Контроль порядка передач

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

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

  • PassConfig::TYPE_BEFORE_OPTIMIZATION
  • PassConfig::TYPE_OPTIMIZE
  • PassConfig::TYPE_BEFORE_REMOVING
  • PassConfig::TYPE_REMOVE
  • PassConfig::TYPE_AFTER_REMOVING

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

1
2
3
4
5
// ...
$containerBuilder->addCompilerPass(
    new CustomPass(),
    PassConfig::TYPE_AFTER_REMOVING
);

Вы также можете контролировать порядок, в котором выполняются передачи компилятора для каждой фазы компиляци. Используйте необязательный третий аргумент addCompilerPass(), чтобы установить приоритетность в виде числа. Приоритет по умолчанию 0, и чем выше его значение, тем раньше он будет выполнен:

1
2
3
4
5
6
7
8
// ...
// FirstPass выполняется после SecondPass, так как его приоритет ниже
$container->addCompilerPass(
    new FirstPass(), PassConfig::TYPE_AFTER_REMOVING, 10
);
$container->addCompilerPass(
    new SecondPass(), PassConfig::TYPE_AFTER_REMOVING, 30
);

Сброс конфигурации для производительности

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;

$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new ProjectServiceContainer();
} else {
    $containerBuilder = new ContainerBuilder();
    // ...
    $containerBuilder->compile();

    $dumper = new PhpDumper($containerBuilder);
    file_put_contents($file, $dumper->dump());
}

Tip

Функция file_put_contents() - атомарная. Это может привести к проблемам в окружении производства со множеством одновременных запросов. Вместо этого, используйте метод dumpFile() из компонента Symfony Filesystem или другие методы, предоставленные Symfony (например, $containerConfigCache->write()), которые являются атомарными.

ProjectServiceContainer это данное классу сброшенного контейнера по умолчанию. Однако вы можете изменить его с опцией class, когда вы будете его сбрасывать:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $containerBuilder = new ContainerBuilder();
    // ...
    $containerBuilder->compile();

    $dumper = new PhpDumper($containerBuilder);
    file_put_contents(
        $file,
        $dumper->dump(['class' => 'MyCachedContainer'])
    );
}

Теперь вы получите скорость сконфигурированного в PHP контейнера с лёгкостью исползования файлов конфигурации. Кроме того, сброс контейнера таким образом ещё больше оптимизирует то, как создаются сервисы контейнером.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...

// основывается на чем-то в вашем проекте
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';

if (!$isDebug && file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $containerBuilder = new ContainerBuilder();
    // ...
    $containerBuilder->compile();

    if (!$isDebug) {
        $dumper = new PhpDumper($containerBuilder);
        file_put_contents(
            $file,
            $dumper->dump(array('class' => 'MyCachedContainer'))
        );
    }
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...

// основываясь на чём-то в вашем проекте
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);

if (!$containerConfigCache->isFresh()) {
    $containerBuilder = new ContainerBuilder();
    // ...
    $containerBuilder->compile();

    $dumper = new PhpDumper($containerBuilder);
    $containerConfigCache->write(
        $dumper->dump(['class' => 'MyCachedContainer']),
        $containerBuilder->getResources()
    );
}

require_once $file;
$container = new MyCachedContainer();

Теперь кешированный сброшенный контейнер используется невзирая на то, включен ли режим отладки. Разница заключается в том, что ConfigCache установлен в режим отладки вторым аргументом конструктора. Когда кеш не находится в режиме отладки, всегда будет исползован кешированный контейнер, если он существует. В режиме отладки, пишется дополнительный файл метаданных с помощью временных отметок всех файлов источников. Потом они проверяются, чтобы увидеть, не изменялись ли файлы, и если они изменялись, то кеш будет считаться просроченным.

Note

В комплексном фреймворке о кешировании и компиляции контейнера позаботились за вас.