![](https://habrastorage.org/files/a16/0a9/940/a160a994006649bd8ecddca7657becc7.jpeg)
Компонент Symfony 2 Config предназначен для работы с конфигурационными файлами и предоставляет следующие возможности:
- Поддержка древовидной структуры конфигурации
- Абстракция составных частей конфигурации, из которых производится ее загрузка (ресурсы, загрузчики ресурсов и т.д.)
- Поддержка произвольного количества составных частей конфигурации и некоторых правил по сборке и объединению
- Кеширование прочитанной конфигурации и автоматическая ее пересборка при изменении одного из исходных файлов
- Валидация конфигурации по различным правилам и подробная информация об ошибках парсинга
Официальная документация по этому компоненту содержит подробную информацию по его использованию. А мы давайте посмотрим на то, как устроен этот компонент внутри.
Определение структуры конфигурации
Типы ключей конфигурации
Вот так выглядит диаграмма классов, которые описывают структуру конфигурации.
![](https://habrastorage.org/files/43e/c81/066/43ec81066c36419bbbf4d57dd2ee34d8.png)
Назначение практически всех классов понятно из их названия. Отмечу только, что для построения дерева конфигурации используется нода
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
)Загрузка конфигурации
Ресурсы и загрузчики
- Части конфигурации в 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
:<?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;
}
}
Поделиться с друзьями
wersoo
А в чем преимущества этого подхода перед обычным и быстрым
parse_ini_file()
?torf
Ну сравнение ИМХО не очень корректно. Symfony отделяет структуру конфигурации от формата источника. Вы можете хранить конфигурацию не только в .ini, но и в нативном php (что для новичков более предпочтительно, а для некоторых олдскульщиков ещё и удобно, т.к. не нужно напрягаться), а также в .yml, который с моей точки зрения является более удобным для конфигураций, т.к. по природе своей лаконичен, интуитивно понятен (пример, кстати, можно посмотреть, например, в конфигах системы деплоя Magallanes) и никак не зависит от php, что даёт преимущества при переезде / скрещивании проекта с другими языками и платформами. Но и опять же, с точки зрения архитектуры приложения отделение структуры от методов загрузки и хранения — правильно, т.к. позволяет менять данные ветви логики в независимости друг от друга, что облегчает масштабируемость приложения. Другое дело, что в Symfony иногда перегибают мальца с абстракциями =)
SerafimArts
И все эти и yml, и php, и json, и проч. великолепно конвертятся в нативные php массивы. В результате достаточно было обычной конфиго-репы, принимающий array на вход и пара драйверов, конвертящих внешние форматы в этот array.
Gemorroj
тоже, считаю, что симфони переусложнили компонент. поэтому он и не пользуется особой популярностью.
FractalizeR
У INI файлов по сравнению с другими форматами конфигурации есть один недостаток — отсутствие возможности организовывать естественную иерархию более чем в 2 уровня. Да и нет возможности выполнять валидацию.
wersoo
Zend_Config_Ini
позволяет строить древовидную структуру из INI-файла с неограниченным количеством уровней.Валидации нет, правда. Да и переиспользование и интерполяция работать не будут.
С другой стороны не придется возиться с кэшированием и инвалидированием, создавать эти массивные TreeBuilder'ы.
В общем, в отрыве от Symfony, мне кажется, этот компонент требует слишком много внимания к себе.