Коротко об архитектуре компонента Symfony Config

a160a994006649bd8ecddca7657becc7.jpeg

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

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

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

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

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


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

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

Для построения описания конфигурации используется класс Symfony\Component\Config\Definition\Builder\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" => 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)Загрузка конфигурации

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


  • Части конфигурации в Symfony описываются ресурсами (Symfony\Component\Config\Resource\ResourceInterface). Понятие ресурса достаточно абстрактно. Им может быть как файл, так и любой другой источник данных. Например, таблица БД или поле в ней.
  • Мониторинг ресурсов на наличие изменений в них ведут инспекторы ресурсов (Symfony\Component\Config\ResourceCheckerInterface).
  • Загрузку конфигурации из ресурсов выполняют загрузчики (Symfony\Component\Config\Loader\LoaderInterface).
  • Поиск подходящего загрузчика для ресурса выполняют ресолверы (Symfony\Component\Config\Loader\LoaderResolverInterface).
  • Symfony\Component\Config\Loader\DelegatingLoader позволяет загрузить ресурс, автоматически найдя необходимый загрузчик, перебирая массив переданных ресолверов.
  • Размещать конфигурационные файлы можно в различных папках. Поиск файлов в них можно вести с помощью Symfony\Component\Config\FileLocator

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

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


Symfony Config позволяет кешировать загруженную конфигурацию с помощью класса Symfony\Component\Config\ConfigCache:
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:
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;
    }
}

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

© Habrahabr.ru