Обзор Symfony компонента: Config +13


В этой статье попробуем разобраться с компонентом под название Config, который помогает загружать и обрабатывать различные данные независимо от источника.


Ниже представлен перевод статьи Symfony2 components overview: Config. Оригинал был опубликован в 2014 году и нем указывается вторая версия Symfony, но информация актуальна и для последней, на данный момент, четвертой версии.


Давайте представим, что мы хотим создать генератор блогов, который будет принимать несколько параметров таких как заголовок(title), описание(description), количество постов на главной странице(posts_main_page), иконки соцсетей(social) и наличие или отсутствие RSS ленты(rss). Для этих целей, мы опишем конфигурационный файл в формате YAML:


blog:
    title: My blog
    description: This is just a test blog
    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, таким образом, не придется тратить время на написание большей части кода. Компонент прост, хорошо тестируется и достаточно гибок для использования в разных проектах.


Архитектура


Архитектура делится на две основные части:


  1. Определение иерархической структуры параметров.
    Компонент позволяет определить формат источника конфигурации, который может быть чем угодно, от простого INI-файла до чего-то более эзотерического, такого как специальный протокол конфигурационных сообщений. Используя класс TreeBuilder, мы можем определить типы значений, сделать их обязательными / необязательными и установить значение по умолчанию.
  2. Обнаружение, загрузка и обработка.
    После того, как формат источника указан, он должен быть найден, загружен и обработан. В завершении, компонент вернет нам простой массив с проверенными значениями или выбросит исключение при ошибке.

Пример


Давайте вернемся к нашему примеру. И так, мы хотим создать гибкую систему генерации блогов. Для начала определим иерархическую структуру(дерево), а поможет нам в этом экземпляр класса 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
        );
    }
}

Как видите все очень просто. Метод supports используется классом LoaderResolver для проверки источника конфигурации. Метод 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 окружений.
  • Встроенная проверка параметров и их значений.
  • Генерация справочной информации.




К сожалению, не доступен сервер mySQL