В этой статье попробуем разобраться с компонентом под название Config, который помогает загружать и обрабатывать различные данные независимо от источника.
Ниже представлен перевод статьи Symfony2 components overview: Config. Оригинал был опубликован в 2014 году и в нем указывается вторая версия Symfony, но информация актуальна и для последней, на данный момент, четвертой версии.
Давайте представим, что мы хотим создать генератор блогов, который будет принимать несколько параметров таких как заголовок(title), описание(description), количество постов на главной странице(posts_main_page), иконки соцсетей(social) и наличие или отсутствие RSS
ленты(rss). Для этих целей, мы опишем конфигурационный файл в формате YAML
:
blog:
title: Мой чудный новый блог
description: И это только начало...
rss: true
posts_main_page: 2
social:
twitter:
url: http://twitter.com/raulfraile
icon: twitter.png
sensiolabs_connect:
url: https://connect.sensiolabs.com/profile/raulfraile
icon: sensiolabs_connect.png
Теперь попробуем распарсить наш файл, проверим наличие обязательных полей, установим при необходимости значения по умолчанию. Все полученные данные проверим на соответствие установленным правилам, например rss может содержать только булевое значение, а в posts_main_page должно находиться целочисленное значение в интервале от 1 до 10. Эти процедуры нам придется повторять каждый раз при обращении к файлу, если же конечно не используется система кеширования. Кроме того подобный механизм усложняет использование файлов других форматов как INI
, XML
или JSON
.
Для упрощения вышеперечисленных действий, воспользуемся компонентом Config. Компонент прост, хорошо тестируется и достаточно гибок для использования в разных проектах.
Архитектура
Разделим проект на две основные части:
- Определение иерархической структуры параметров.
Компонент позволяет определять формат источника конфигурации, который может быть чем угодно, от простогоINI
-файла до чего-то более экзотического. Класс TreeBuilder, поможет определить типы параметров, сделать их обязательными / необязательными и установить значение по умолчанию. - Обнаружение, загрузка и обработка.
После того, как формат источника указан, он должен быть найден, загружен и обработан. В завершении, компонент вернет нам простой массив с проверенными значениями или выбросит исключение при ошибке.
Пример
Давайте вернемся к нашему примеру. И так, мы хотим создать гибкую систему генерации блогов. Для начала определим иерархическую структуру(дерево), а поможет нам в этом экземпляр класса TreeBuilder, который предоставляет DSL
-синтаксис подобный интерфейс.
<?php
namespace RaulFraile\Config;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('blog');
$rootNode
->children()
->scalarNode('title')
->isRequired()
->end()
->scalarNode('description')
->defaultValue('')
->end()
->booleanNode('rss')
->defaultValue(false)
->end()
->integerNode('posts_main_page')
->min(1)
->max(10)
->defaultValue(5)
->end()
->arrayNode('social')
->arrayPrototype()
->children()
->scalarNode('url')->end()
->scalarNode('icon')->end()
->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}
Не переживайте, если вы видите подобную структуру PHP кода в первый раз, DSL
-синтаксис в нем, всегда выглядит немного странно. В примере выше, мы определили корневой узел blog и от него выстроили структуру дерева конфигурации, ветвями которого являются необходимые нам параметры и правила к их значениям. К примеру title обозначен как обязательный параметр скалярного типа, description как опциональный параметр, который по умолчанию пуст, в rss ожидаем булевое значение, которое по умолчанию равно false
, а posts_main_page должен содержать целочисленное значение в диапазоне от 1 до 10, при этом 5 по умолчанию.
Отлично, структуру мы определили, теперь давайте займемся загрузкой и обработкой. По условию, источник может быть любым, для начала нам нужно преобразовать его в обычный массив, чтобы в дальнейшем проверить и обработать все значения используя нашу конфигурационную структуру. Для каждого формата источника нам потребуется отдельный класс, так если мы собираемся использовать форматы файлов YAML
и XML
нам нужно создать два класса. В примере ниже представлен для простоты только класс формата YAML
:
<?php
namespace RaulFraile\Config;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Yaml\Yaml;
class YamlConfigLoader extends FileLoader
{
public function load($resource, $type = null)
{
$configValues = Yaml::parse(file_get_contents($resource));
return $configValues;
}
public function supports($resource, $type = null)
{
return is_string($resource) && 'yml' === pathinfo(
$resource,
PATHINFO_EXTENSION
);
}
}
Как видите все очень просто. Метод YamlConfigLoader::supports
используется классом LoaderResolver
для проверки источника конфигурации. Метод YamlConfigLoader::load
преобразовывает YAML
файл в массив данных используя другой Symfony компонент YAML.
В завершении, мы объединяем конфигурационную структуру и загрузчик при обработке источника для получения необходимых значений:
<?php
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Definition\Processor;
use RaulFraile\Config\YamlConfigLoader;
use RaulFraile\Config\Configuration;
include_once __DIR__. '/vendor/autoload.php';
// расположение файлов конфигурации
$directories = array(__DIR__.'/config');
$locator = new FileLocator($directories);
// преобразование данных из файла в массив
$loader = new YamlConfigLoader($locator);
$configValues = $loader->load($locator->locate('config.yml'));
// обработка данных
$processor = new Processor();
$configuration = new Configuration();
try {
$processedConfiguration = $processor->processConfiguration(
$configuration,
$configValues
);
// проверка данных
var_dump($processedConfiguration);
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
Давайте разберем данный код. Сначала мы определяем массив директорий где могут находиться конфигурационные файлы, и помещаем его в качестве параметра в объект FileLocator
который ищет файл config.yml
в указанной директории. Затем, создаем объект YamlConfigLoader
, который возвращает массив со значениями, и уже он обрабатывается нашей конфигурационной структурой.
В результате получим следующий массив:
array(5) {
'title' =>
string(7) "My blog"
'description' =>
string(24) "This is just a test blog"
'rss' =>
bool(true)
'posts_main_page' =>
int(2)
'social' =>
array(2) {
'twitter' =>
array(2) {
'url' =>
string(29) "http://twitter.com/raulfraile"
'icon' =>
string(11) "twitter.png"
}
'sensiolabs_connect' =>
array(2) {
'url' =>
string(49) "https://connect.sensiolabs.com/profile/raulfraile"
'icon' =>
string(22) "sensiolabs_connect.png"
}
}
}
Если попробуем изменить config.yml
удалив rss и post_main_page поля, то получим значения по умолчанию:
array(5) {
...
'rss' =>
bool(false)
'posts_main_page' =>
int(5)
Кеширование
Обработка больших конфигурационных файлов может стать ресурсоемкой задачей. Описываемый компонент обладает простым механизмом кэширования основанной на проверке даты изменения файла конфигурации.
Для включения кэша достаточно нескольких строк кода:
<?php
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Definition\Processor;
use RaulFraile\Config\YamlConfigLoader;
use RaulFraile\Config\Configuration;
include_once __DIR__. '/vendor/autoload.php';
$cachePath = __DIR__.'/cache/config.php';
$configFile = 'config.yml';
// второй аргумент включает / отключает режим отладки
$cache = new ConfigCache($cachePath, true);
if (!$cache->isFresh()) {
$directories = array(__DIR__.'/config');
$locator = new FileLocator($directories);
$loader = new YamlConfigLoader($locator);
$configFilePath = $locator->locate($configFile);
$configValues = $loader->load($configFilePath);
$resource = new FileResource($configFilePath);
$processor = new Processor();
$configuration = new Configuration();
try {
$processedConfiguration = $processor->processConfiguration(
$configuration,
$configValues
);
// сериализация массива и сохранение
$cache->write(serialize($processedConfiguration), array($resource));
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
}
Экземпляр класса ConfigCache
проверяет существование кэша файла и при наличии сравнивает дату изменения конфигурационного файла. Когда мы создаем кэш файла мы также сохраняем список используемых объектов для дальнейшего сопоставления.
Множественная загрузка
Для добавления другого формата конфигурации, достаточно определить класс который будет отвечать за конкретный формат. В примере ниже, мы добавили поддержку XML
конфигурации и соответствующий обработчик. Класс LoaderResolver
поможет нам объединить разные форматы в общий пул, а класс DelegatingLoader
загрузит по запросу необходимый файл.
<?php
namespace RaulFraile\Config;
use Symfony\Component\Config\Loader\FileLoader;
class XmlConfigLoader extends FileLoader
{
public function load($resource, $type = null)
{
// обработка xml
return $configValues;
}
public function supports($resource, $type = null)
{
return is_string($resource) && 'xml' === pathinfo(
$resource,
PATHINFO_EXTENSION
);
}
}
$loaderResolver = new LoaderResolver(array(
new YamlConfigLoader($locator),
new XmlConfigLoader($locator)
));
$delegatingLoader = new DelegatingLoader($loaderResolver);
$configValues = $delegatingLoader->load($locator->locate('config.xml'));
Генерация справочной информации
Помимо прочего, компонент обладает функционалом для генерации справочной информации к вашей документации.
<?php
...
use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper;
$dumper = new YamlReferenceDumper();
echo $dumper->dump($configuration);
Будет выведено:
blog:
title: ~ # Required
description: ''
rss: false
posts_main_page: 5
social:
url: ~
icon: ~
Итог
Возможно вы посчитаете, что все это слишком сложно и запутанно, и можно обойтись парочкой функций. Возможно, но такова «цена» за хорошую ООП структуру. С другой стороны, данный компонент предлагает ряд преимуществ:
- Покрытие компонента тестами на уровне ~80% и активная поддержка.
- Добавлять новые форматы конфигурации действительно просто. Достаточно определить обработчик который преобразует исходные данные в обычный массив. Похожая структура будет использоваться и для других форматов. Расширение любой части компонента реализуется добавлением необходимого интерфейса.
- Кэширование работает «из коробки» с гибкими настройками как для
dev
так и дляprod
окружений. - Встроенная проверка параметров и их значений.
- Генерация справочной информации.