Компонент Symfony 2 Config предназначен для работы с конфигурационными файлами и предоставляет следующие возможности:

  • Поддержка древовидной структуры конфигурации
  • Абстракция составных частей конфигурации, из которых производится ее загрузка (ресурсы, загрузчики ресурсов и т.д.)
  • Поддержка произвольного количества составных частей конфигурации и некоторых правил по сборке и объединению
  • Кеширование прочитанной конфигурации и автоматическая ее пересборка при изменении одного из исходных файлов
  • Валидация конфигурации по различным правилам и подробная информация об ошибках парсинга

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

Определение структуры конфигурации


Типы ключей конфигурации


Вот так выглядит диаграмма классов, которые описывают структуру конфигурации.

Назначение практически всех классов понятно из их названия. Отмечу только, что для построения дерева конфигурации используется нода ArrayNode. Если требуется, чтобы внутри ArrayNode размещались не просто предпоределенные ноды, а несколько других ArrayNode, но с четко одинаковой предопределенной внутренней структурой, можно использовать PrototypedArrayNode.

Для построения описания конфигурации используется класс Symfony\Component\Config\Definition\Builder\TreeBuilder примерно вот таким способом:

<?php

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_demo');

        $rootNode
            ->children()
            ->arrayNode('entities')
            ->addDefaultsIfNotSet()
            ->prototype('scalar')->end()
            ->defaultValue(
                array(
                    'Acme\BaseBundle\Entity\DefaultEntity1',
                    'Acme\BaseBundle\Entity\DefaultEntity2',
                )
            )
            ->end();

        return $rootNode;
    }
}

Структуру конфигурации не обязательно объявлять всю целиком в одном месте. Можно сделать это частями, а затем объединить части при помощи метода append у NodeBuilder.

Нормализация


Нормализацией называется приведение имен ключей нод и их значений, если потребуется, к каноническому виду. Фактически, сейчас нормализация используется только для того, чтобы привести ноды, описанные в xml в виде

<children>
    <child>Значение потомка</child>
</children>

к виду
    "children" => Array(
        [0] => "Значение потомка"
    )

Для нормализации нод вызывается метод normalize() из Symfony\Component\Config\Definition\NodeInterface. А кроме того, у Symfony\Component\Config\Definition\BaseNode есть еще метод preNormalize. Последний используется для приведения к общему виду ключей типа foo_bar и foo-bar.

Финализация


Процесс финализации ноды выполняет действия, по подготовке ноды к чтению внутри конфигурации и проверки на соответствие заявленому типу и его правилам. Финализация выполянется методом finalizeValue потомков BaseNode

Валидация данных выполняется как с помощью предопределенных методов NodeDefinition и его потомков вроде isRequired, так и с помощью расширенной валидации, делегированной классу Symfony\Component\Config\Definition\Builder\ValidationBuilder.

Правила объединения данных из нескольких частей содержатся в классе Symfony\Component\Config\Definition\Builder\MergeBuilder. Делегирование ему проверок выполняется методом merge() класса NodeDefinition. Например, можно запретить переопределять значение выбранного ключа конфигурации другими конфигурационными файлами после того, как он был прочитан в первый раз.

Сам процесс валидации / нормализации / финализации конфигурации выглядит так:

$configs = array($config1, $config2); //Загруженные любым способом части конфигурации

$processor = new Processor(); // Процессор конфигурации
$configuration = new Configuration(); // Класс Configuration c правилами проверки (см. выше).
$processedConfiguration = $processor->processConfiguration(
    $configuration,
    $configs
);

Билдер


Как нетрудно заметить, для самого процесса построения описания конфигурации TreeBuilder использует экземпляр класса Symfony\Component\Config\Definition\Builder\NodeBuilder. Поэтому вы вполне можете определять свои типы нод для конфигурации. Для этого необходимо создать свой вариант реализации NodeInterface и своего потомка \Symfony\Component\Config\Definition\Builder\NodeDefinition. После чего просто вызвать метод setNodeClass у NodeBuilder.

Во всех подробностях процесс определения структуры конфигурации описан тут.

Дампер


После того, как структура конфигурации построена, ее можно сдампить с помощью различных дамперов из пространства имен Symfony\Component\Config\Definition\Dumper. Сейчас там есть два варианта: YamlReferenceDumper и XmlReferenceDumper. Эти дамперы используются, например, когда вы вызываете с консоли ./bin/symfony config:dump-reference (см. Symfony\Bundle\FrameworkBundle\Command\ConfigDumpReferenceCommand)

Загрузка конфигурации


Ресурсы и загрузчики



Нужно сказать, что сам компонент Config не содержит конкретных реализаций загрузчиков. Он лишь предоставляет необходимые интерфейсы для их реализации. Причем способ загрузки и целевой контейнер для загруженных данных тоже не регламентирован. Если посмотреть на реализацию Symfony\Component\DependencyInjection\Loader\YamlFileLoader, то видно, что конфигурация загружается прямо в контейнер.

Кеширование конфигурации


Symfony Config позволяет кешировать загруженную конфигурацию с помощью класса Symfony\Component\Config\ConfigCache:

<?php

use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;

$cachePath = __DIR__.'/cache/appSomeCacheFile.php';

// Режим отладки определяет, будут ли проверяться на изменения ресурсы, из которых строился кеш
$cacheFile = new ConfigCache($cachePath, true);

if (!$cacheFile->isFresh()) {
    $configFiles = []; // Здесь имена файлов, из которых состоит конфигурация

    $resources = array();
    foreach ($configFiles as $cfgFile) {
        // Здесь загружаем конфигурацию
        // .....
        // И добавляем ресурс в массив
        $resources[] = new FileResource($cfgFile);
    }

    $code = '...'; //Здесь строим кэш из загруженных данных

    //Пишем кеш. Рядом с файлом кеша запишется файл с метаданными со списком исходных ресурсов
    $cacheFile->write($code, $resources);
}

// Подключаем файл кеша
require $cachePath;

Можно инкапсулировать алгоритм перестройки кеша, например, в класс, а затем воспользоваться Symfony\Component\Config\ConfigCacheFactory вместо ConfigCache для дальнейшей работы. ConfigCacheFactory принимает в конструкторе callable, который будет перестраивать кеш.

Пример использования компонента


Компонент Symfony Config вполне можно использовать и без фреймворка. В качестве примера приведу небольшой кусочек кода, написанный уважаемым magickatt:

<?php
// Загружаем специфичную для приложения конфигурацию
try {
    $basepath = __DIR__ . '/config';
    $configuration = Yaml::parse($basepath . '/config.yml');
} catch (\InvalidArgumentException $exception) {
    exit("Кажется, конфигурационный файл отсутствует");
}
// Используем ConfigurationInterface для работы с *.yml форматом
$yamlConfiguration = new \Configuration();
// Обрабатываем конфигурационные файлы (объединяем один или больше файлов *.yml)
$processor = new Processor();
$configuration = $processor->processConfiguration(
    $yamlConfiguration,
    array($configuration) // Здесь может быть любое количество *.yml файлов
);

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration
{
    /**
     * @return TreeBuilder
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('arbitary');
        $rootNode->children()
            ->scalarNode('host')
            ->isRequired()
            ->cannotBeEmpty()
            ->end()
            ->scalarNode('username')
            ->isRequired()
            ->cannotBeEmpty()
            ->end()
            ->scalarNode('password')
            ->isRequired()
            ->cannotBeEmpty()
            ->end()
            ->booleanNode('bindRequiresDn')
            ->defaultTrue()
            ->end();
        return $treeBuilder;
    }
}
Поделиться с друзьями
-->

Комментарии (6)


  1. wersoo
    12.12.2016 21:29
    +1

    А в чем преимущества этого подхода перед обычным и быстрым parse_ini_file()?


    1. torf
      12.12.2016 23:47
      +1

      Ну сравнение ИМХО не очень корректно. Symfony отделяет структуру конфигурации от формата источника. Вы можете хранить конфигурацию не только в .ini, но и в нативном php (что для новичков более предпочтительно, а для некоторых олдскульщиков ещё и удобно, т.к. не нужно напрягаться), а также в .yml, который с моей точки зрения является более удобным для конфигураций, т.к. по природе своей лаконичен, интуитивно понятен (пример, кстати, можно посмотреть, например, в конфигах системы деплоя Magallanes) и никак не зависит от php, что даёт преимущества при переезде / скрещивании проекта с другими языками и платформами. Но и опять же, с точки зрения архитектуры приложения отделение структуры от методов загрузки и хранения — правильно, т.к. позволяет менять данные ветви логики в независимости друг от друга, что облегчает масштабируемость приложения. Другое дело, что в Symfony иногда перегибают мальца с абстракциями =)


      1. SerafimArts
        13.12.2016 01:55

        И все эти и yml, и php, и json, и проч. великолепно конвертятся в нативные php массивы. В результате достаточно было обычной конфиго-репы, принимающий array на вход и пара драйверов, конвертящих внешние форматы в этот array.


        1. Gemorroj
          14.12.2016 11:20
          -1

          тоже, считаю, что симфони переусложнили компонент. поэтому он и не пользуется особой популярностью.


    1. FractalizeR
      13.12.2016 10:19
      +1

      У INI файлов по сравнению с другими форматами конфигурации есть один недостаток — отсутствие возможности организовывать естественную иерархию более чем в 2 уровня. Да и нет возможности выполнять валидацию.


      1. wersoo
        16.12.2016 14:45

        Zend_Config_Ini позволяет строить древовидную структуру из INI-файла с неограниченным количеством уровней.
        Валидации нет, правда. Да и переиспользование и интерполяция работать не будут.
        С другой стороны не придется возиться с кэшированием и инвалидированием, создавать эти массивные TreeBuilder'ы.


        В общем, в отрыве от Symfony, мне кажется, этот компонент требует слишком много внимания к себе.