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

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

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

Если вы откроете ваш главный каталог приложения (обычно config/packages/), то вы увидите некоторое количество разных файлов, вроде framework.yaml, twig.yaml и doctrine.yaml. Каждый из них конфигурирует особый пакет, позволяя вам определять опции на высоком уровне, а потом позволяя пакету сделать все сложные изменения низшего уровня, основываясь на ваших настройках.

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

1
2
3
# config/packages/framework.yaml
framework:
    form: true

Использование расширения пакета

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

1
2
3
4
5
# config/packages/acme_social.yaml
acme_social:
    twitter:
        client_id: 123
        client_secret: your_secret

Основная идея заключается в том, что вместо того, чтобы переопределять отдельные параметры, вы позволяете пользователю сконфигурировать всего несколько специально созданных опций. Как разработчик пакетов, вы потом проанализируете эту конфигурацию и загрузите правильные сервисы и парааметры вутри класса "Extension".

Note

Ключ корня вашей конфигурации пакета (acme_social в предыдущем примере) автоматически определяется из имени вашего пакета (это snake case имени пакета без суффикса Bundle).

See also

Прочтите больше о расширении в Как загружать конфигурацию сервиса внутри пакета.

Tip

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

See also

Для работы с параметрами в рамках контейнера внедрения зависимости, см. Использование параметров в классе внедрения зависимостей.

Обработка массива $configs

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

Каждый раз, когда пользователь включает ключ acme_social (который является дополнительным именем ВЗ) в файле конфигурации, конфигурация под ним добавляется в массив конфигурация и передаётся методу load() вашего расширения (Symfony автоматически конвертирует XML и YAML в массив).

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

1
2
3
4
5
6
7
8
[
    [
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
]

Отметьте, что это массив массивов, а не просто плоский массив значений конфигурации. Это сделано специально, так как позволяет Symfony анализировать несколько источников конфигурации. Например, если acme_social появляется в другом файле конфигурации, скажем, config/packages/dev/acme_social.yaml, с другими значениями под ним, входящий массив может выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
    // значения из config/packages/acme_social.yaml
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
    // values from config/packages/dev/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 456,
        ],
    ],
]

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

Но не волнуйтесь! Компонент Symfony Config поможет вам объедиить эти значения, предоставит значения по умолчанию и даст пользователю ошибки валидации в плохой конфигурации. Вот, как это работает. Создайте класс Configuration в каталоге DependencyInjection и постройте дерево, определяющее структуру конфигурации вашего пакета.

Класс Configuration для обработки пробной конфигурации выглядит так:

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
// src/DependencyInjection/Configuration.php
namespace Acme\SocialBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('acme_social');

        $treeBuilder->getRootNode()
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;

        return $treeBuilder;
    }
}

See also

Класс Configuration может быть намного сложнее, чем показано здесь, поддерживать узлы "прототипов", продвинутую валидацию, XML нормализацию и продвинутое объёдинение. Вы можете прочитать больше об этом в документации компонента Config. Вы также можете увидеть это в действии, изучив некоторые базовые классы Конфигурации, как, например, Конфигурацию FrameworkBundle или Конфигурацию TwigBundle.

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

1
2
3
4
5
6
7
8
9
10
// src/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php
public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();

    $config = $this->processConfiguration($configuration, $configs);

    // теперь у вас есть эти 2 ключа конфигурации
    // $config['twitter']['client_id'] and $config['twitter']['client_secret']
}

Метод processConfiguration() использует дерево конфигурации, которое вы определили в классе Configuration для валидации, нормализаци и слияния всех массивов конфигурации.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- src/Acme/SocialBundle/Resources/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
        https://symfony.com/schema/dic/services/services-1.0.xsd"
>
    <services>
        <service id="acme.social.twitter_client" class="Acme\SocialBundle\TwitterClient">
            <argument></argument> <!-- will be filled in with client_id dynamically -->
            <argument></argument> <!-- will be filled in with client_secret dynamically -->
        </service>
    </services>
</container>

В своем расширении вы можете загрузить это и динамически устанавливать аргументы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/DependencyInjection/AcmeSocialExtension.php
namespace Acme\SocialBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

public function load(array $configs, ContainerBuilder $container): void
{
    $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
    $loader->load('services.xml');

    $configuration = new Configuration();
    $config = $this->processConfiguration($configuration, $configs);

    $definition = $container->getDefinition('acme.social.twitter_client');
    $definition->replaceArgument(0, $config['twitter']['client_id']);
    $definition->replaceArgument(1, $config['twitter']['client_secret']);
}

Tip

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;

class AcmeHelloExtension extends ConfigurableExtension
{
    // отметьте, что этот метод называетсят loadInternal, а не загрузка
    защищённой функции loadInternal(массив $mergedConfig, ContainerBuilder $container)
    {
        // ...
    }
}

Этот класс использует метод getConfiguration(), чтобы получить экземпляр Configuration.

Использование компонента Config абсолютно не обязательно. Метод load() получает массив значений конфигурации. Вы можете просто проанализировать эти массивы самостоятельно (например, переопределив конфигурации и использовав isset, чтобы проверить наличие значения). Имейте в виду, что будет очень сложно поддерживать XML:

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container)
{
    $config = [];
    // позвольте источникам переопределить предыдущее установленное значение
    foreach ($configs as $subConfig) {
        $config = array_merge($config, $subConfig);
    }

    // ... теперь используйте чистый массив $config
}

Использование класса AbstractBundle

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

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
35
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->arrayNode('twitter')
                    ->children()
                        ->integerNode('client_id')->end()
                        ->scalarNode('client_secret')->end()
                    ->end()
                ->end() // twitter
            ->end()
        ;
    }

    public function loadExtension(array $config, ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void
    {
        // В отличие от класса Extension, переменная "$config" уже прошла слияние и
        // обработку. Вы можете использовать её напрямую, что сконфигурировать сервис-контейнер.
        $container->services()
            ->get('acme.social.twitter_client')
            ->arg(0, $config['twitter']['client_id'])
            ->arg(1, $config['twitter']['client_secret'])
        ;
    }
}

Note

Методы configure() и loadExtension() вызываются только во время компиляции.

Tip

Метод AbstractBundle::configure() также позволяет импортировать определение конфигурации из одного или более файлов:

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

// ...
class AcmeSocialBundle extends AbstractBundle
{
    public function configure(DefinitionConfigurator $definition): void
    {
        $definition->import('../config/definition.php');
        // вы также можете использовать глобальные паттерны
        //$definition->import('../config/definition/*.php');
    }

    // ...
}
1
2
3
4
5
6
7
8
9
10
// config/definition.php
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;

return static function (DefinitionConfigurator $definition): void {
    $definition->rootNode()
        ->children()
            ->scalarNode('foo')->defaultValue('bar')->end()
        ->end()
    ;
};

Изменение конфигурации другого пакета

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

Сброс конфигурации

Команда config:dump-reference сбрасывает конфигурацию пакета по умолчанию в консоли, используя формат Yaml.

Если конфигурация вашего пакета находится в стандартной локации (YourBundle\DependencyInjection\Configuration) и не имеет консруктора, то она будет работать автоматически. Если же у вас что-то по-другому, ваш класс Extension должен переопределять метод Extension::getConfiguration() и возвращать экземпляр вашей Configuration.

Поддержка XML

Symfony позволяет людям предоставить конфигурацию в трёх разных форматах: Yaml, XML и PHP. Как Yaml, так и PHP используют одинаковый синтаксис и поддерживаются по умолчанию при использовании компонента Config. Поддержка XML требует от вас некоторых вещей. Но при общем использовании пакета с другими, рекомендуется следовать этим шагам.

Подготовьте ваше дерево конфигурации к XML

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

Выбор пространста имён XML

В XML, пространство имён XML используется для определения того, какие элементы принадлежат конфигурации конкретного пакета. Пространство имён возвращается из метода Extension::getNamespace(). По соглашению, пространство имён - это URL (он не должен быть валидным или в принципе сушествовать). По умолчанию, просранство имён для пакета - http://example.org/schema/dic/DI_ALIAS, где DI_ALIAS - дополнительное имя прямого внедрения расширения. Вы можете захотеть изменить это на более профессиональный URL:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

// ...
class AcmeHelloExtension extends Extension
{
    // ...

    public function getNamespace(): string
    {
        return 'http://acme_company.com/schema/dic/hello';
    }
}

Предоставление XML-схемы

XML имеет очень полезную функцию, под названием XML-схема. Она позволяет вам описать все возможные элементы и атрибуты, а также их значения, в Определении XML-схемы (xsd-файл). Этот XSD-файл используется интегрированной средой обработки для автозаполнения и использутется компонентом Configuration для валидации элементов.

Для того, чтобы использовать схему, файл XML-конфигурации должен предоставлять атрибут xsi:schemaLocation, указывающий на XSD-файл для определённого пространства имён XML. Это местоположение всегда начинается с пространства имён XML. Это пространство имён XML потом заменяется базовым путём XSD-валидации, возвращёнными из метода Extension::getXsdValidationBasePath(). Потом за этим пространством имён следует остальной путь из базового пути к самому файлу.

По соглашению, XSD-файл живёт в Resources/config/schema/, но вы можете разместить его где угодно. Вам стоит вернуть этот путь в качестве базового:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/DependencyInjection/AcmeHelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

// ...
class AcmeHelloExtension extends Extension
{
    // ...

    public function getXsdValidationBasePath(): string
    {
        return __DIR__.'/../config/schema';
    }
}

Если предположить, что XSD-файл называется hello-1.0.xsd, то месторасположение схемы будет http://acme_company.com/schema/dic/hello/hello-1.0.xsd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- config/packages/acme_hello.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"
    xmlns:acme-hello="http://acme_company.com/schema/dic/hello"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://acme_company.com/schema/dic/hello
        https://acme_company.com/schema/dic/hello/hello-1.0.xsd"
>
    <acme-hello:config>
        <!-- ... -->
    </acme-hello:config>

    <!-- ... -->
</container>