Pull to refresh

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

Reading time 6 min
Views 7.9K


Компонент 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;
    }
}
Tags:
Hubs:
+21
Comments 6
Comments Comments 6

Articles