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

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

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

  • YAML
    1
    2
    framework:
        form: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd
            http://symfony.com/schema/dic/symfony
            http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:form />
        </framework:config>
    </container>
    
  • PHP
    1
    2
    3
    $container->loadFromExtension('framework', array(
        'form' => true,
    ));
    

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

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

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

  • YAML
    1
    2
    3
    4
    5
    # config/packages/acme_social.yaml
    acme_social:
        twitter:
            client_id: 123
            client_secret: your_secret
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/packages/acme_social.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:acme-social="http://example.org/schema/dic/acme_social"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
       <acme-social:config>
           <acme-social:twitter client-id="123" client-secret="your_secret" />
       </acme-social:config>
    
       <!-- ... -->
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    // config/packages/acme_social.php
    $container->loadFromExtension('acme_social', array(
        'client_id'     => 123,
        'client_secret' => 'your_secret',
    ));
    
Прочтите больше о расширении в How to Load Service Configuration inside a Bundle.

Tip

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

Для работы с параметрами в рамках контейнера внедрения зависимости, см. Using Parameters within a Dependency Injection Class.

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

Первым делом вам нужно создать класс расширения так, как объясняется в How to Load Service Configuration inside a Bundle.

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

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

1
2
3
4
5
6
7
8
array(
    array(
        'twitter' => array(
            '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
array(
    // значения из config/packages/acme_social.yaml
    array(
        'twitter' => array(
            'client_id' => 123,
            'client_secret' => 'your_secret',
        ),
    ),
    // значения из config/packages/dev/acme_social.yaml
    array(
        'twitter' => array(
            '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
27
// src/Acme/SocialBundle/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 = new TreeBuilder();
        $rootNode = $treeBuilder->root('acme_social');

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

        return $treeBuilder;
    }
}
Класс Configuration может быть намного сложнее, чем показано здесь, поддерживать узлы "прототипов", продвинутую валидацию, XML нормализацию и продвинутое объёдинение. Вы можете прочитать больше об этом в документации компонента Конфигурации. Вы также можете увидеть это в действии, изучив некоторые базовые классы Конфигурации, как, например, Конфигурацию 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)
{
    $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
        http://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/Acme/SocialBundle/DependencyInjection/AcmeSocialExtension.php
// ...

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

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

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

    $def = $container->getDefinition('acme.social.twitter_client');
    $def->replaceArgument(0, $config['twitter']['client_id']);
    $def->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(), чтобы получить экземпляр Конфигурации.

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

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

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

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

Если у вас есть несколько пакетов, зависящих друг от друга, может быть полезным позволить одному классу Extension Этого можно достигнуть, используя расширение дополнения. Чтобы узнать больше, см. How to Simplify Configuration of Multiple Bundles.

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

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

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

Поддержка XML

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

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

Компонент Конфигурации предоставляет некоторые методы по умолчанию, чтобы позволить ему корректно обработать XML-конфигурацию. Смотрите "Normalization" в документации компонента. Однако, вы можете сделать некоторые дополнительные вещи, которые улучшат опыт использования 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
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php

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

    public function getNamespace()
    {
        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
// src/Acme/HelloBundle/DependencyInjection/AcmeHelloExtension.php

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

    public function getXsdValidationBasePath()
    {
        return __DIR__.'/../Resources/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
<!-- config/packages/acme_hello.xml -->
<?xml version="1.0" ?>
<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://acme_company.com/schema/dic/hello
        http://acme_company.com/schema/dic/hello/hello-1.0.xsd">

    <acme-hello:config>
        <!-- ... -->
    </acme-hello:config>

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

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.