Как создать дружественную конфигурацию для пакета
Дата обновления перевода 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.
Использование класса 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>