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

Дата оновлення перекладу 2022-12-12

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

Сервіс-контейнер може бути скомпільований з багатьох причин. Ці причини включають в себе перевірку потенційних проблем на кшталт циклічних посилань, підвищення ефективності контейнера шляхом розвʼязаванні параметрів та видалення сервісів, що не використовуються. Також, певні функції, на кшталт використання батьківських сервісів, вимагають компіляції контейнера.

Він компілюється шляхом виклику:

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\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
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']);

    // ...
}

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

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() у класі розширення викликається під час кроку PassConfig::TYPE_BEFORE_OPTIMIZATION. Ви можете прочитати
наступний розділ , якщо вам потрібно редагувати контейнер під час іншого кроку.

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() з компонента Filesystem Symfony або інші методи, надані 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'))
        );
    }
}

Це можна покращити ще, просто перекомпілювавши контейнер у режимі налагодження, якщо в його конфігурації були зроблені зміни, а не роблячи це за кожним запитом. Це можна зробити шляхом кешування файлів джерел, використаних для конфігурації контейнера, описаним у документації компонента Config "Кешування, засноване на джерелах".

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

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(array('class' => 'MyCachedContainer')),
        $containerBuilder->getResources()
    );
}

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

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

Note

У комплексному фреймворку про кешування та компіляцію контейнера потурбувалися за вас.