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

Дата оновлення перекладу 2024-04-30

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

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

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

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

Використання розширення пакета

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

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

Основна ідея полягає в тому, що замість того, щоб користувач перевизначав окремі параметри, ви можете дозволити користувачу сконфігурувати декілька спеціально створених опцій. Як розробник пакета, ви потім парсуєте цю конфігурацію і завантажуєте правильні сервіси та параметри всередині класу "Extension".

Note

Кореневий ключ конфігурації вашого пакета (acme_social у попередньому прикладі) автоматично визначається з імені вашого пакету (це зміїинй регістр імені пакета без суфіксу Bundle).

See also

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

Tip

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

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
15
[
    // значення з config/packages/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ],
    ],
    // значення з config/packages/dev/acme_social.yaml
    [
        'twitter' => [
            'client_id' => 456,
        ],
    ],
]

Порядок двох масивів залежить від того, який встановлено першим.

Але не хвилюйтеся! Компонент Symfony Конфігурація допоможе вам обʼєднати ці значення, надасть значення за замовчуванням та видасть користувачу помилки валідації у поганій конфігурації. Ось, як це працює. Створіть клас 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 нормалізацію та прросунуте обʼєднання. Ви можете прочитати більше про це у документації компонента Конфігурація. Ви також можете побачити це в дії, вивчивши деякі базові класи Конфігурації, як, наприклад, Конфігурацію FrameworkBundle або Конфігурацію TwigBundle.

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

1
2
3
4
5
6
7
8
9
10
// src/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/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> <!-- буде динамічно заповнено за допомогою client_id -->
            <argument></argument> <!-- буде динамічно заповнено за допомогою client_secret -->
        </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/DependencyInjection/HelloExtension.php
namespace Acme\HelloBundle\DependencyInjection;

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

class AcmeHelloExtension extends ConfigurableExtension
{
    // відмітьте, що цей метод називається loadInternal, а не завантажує захищену
    функцію loadInternal(array $mergedConfig, ContainerBuilder $container)
    {
        // ...
    }
}

Цей клас використовує метод getConfiguration(), щоб отримати екземпляр Конфігурації.

Використання компонента Config абсолютно не обовʼязково. Метод load() отримує масив значень конфігурації. Ви можете просто проаналізувати ці масиви самостійно (наприклад, перевизначивши конфігурації та використавши isset, щоб перевірити наявність значення). Майте на увазі, що буде дуже складно підтримувати XML.

public function load(array $configs, ContainerBuilder $container): void { $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
    {
        // На відміну від класу Розширення, змінна "$config" вже злита та оброблена.
        // Ви можете використати її напряму, щоб сконфігурувати сервіс-контейнер.
        $containerConfigurator->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
15
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;

// ...
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 використовують однаковий синтаксис та пітдримуються за замовчуванням при використанні компонента Конфігурація. Підтримка XML вимагає від вас деяких речей. Але при загальному використанні пакета з іншими, рекомендується слідувати цим крокам.

Підготуйте ваше дерево конфігурації до XML

Компонент Конфігурація надає деякі методи за замовчуванням, щоб дозволяти йому коректно обробляти 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-файл використовується інтегрованим середовищем обробки для автозаповнення та використовується компонентом Конфігурація для валідації елементів.

Для того, шоб використати схему, файл 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>