Конфигурация сайта 2.2: совместная разработка (наследование)

Продолжаем разговор из прошлых двух частей.
Конфигурация сайта 1: введение
Конфигурация сайта 2.1: совместная разработка (платформы)

В прошлой части у нас массив, соответствующий какой-то платформе (например, vasya), вливался в массив некой базовой конфигурации (config.php). Назовём это: «vasya наследуется от config».

Раньше я делал так:

config — конфигурация системы на рабочем сервере, от неё наследуются конфигурации разработчиков.

Оно не всегда удобно. Лучше наследоваться от какой-то базовой конфигурации, а рабочей сервер поставить в один ряд с разработчиками. Тем более в нашем примере рабочих серверов у нас несколько.

Наследуем всё от базового конфига и введём ещё промежуточные этапы:

Все платформы, что разработческие, что рабочие, наследуются от одной базовой структуры.

Зачем промежуточные dev и prod? В dev'е собраны девелоперы, в prod'е продакшен-серверы. У каждой из этих групп могут быть свои общие для всей группы настройки, отличные от другой группы. Например, в dev включена отладка и отключен кэш для js и css. Что, впрочем, не помешает Пете у себя отладку отключить, когда она ему надоест.

Пример

И вот есть у нас умозрительная система, давайте её хорошенько поконфигурируем.

Будем конфигурировать:

  1. Базу данных
  2. Вывод отладочной информации
  3. Пути к некоторым каталогам
  4. Инверсию зависимостей

С инверсией зависимостей ничего придумывать навороченного не будем. Пускай наша система на верхнем уровне состоит из 4-х подсистем:

Модуль A — центральный, взаимодействует с другими тремя модулями. Модули выполнены в виде классов. Требуется управлять через конфиг реализацией этих модулей, то есть просто указывать класс, который следует использовать.

Пишем базовый конфиг (base.php):

return array(
    'db' => array(
        'host'     => 'localhost',
        'username' => 'project',
        'password' => 'project',
        'dbname'   => 'project',
    ),
    'debug' => false, // Вывод какой-либо отладочной информации в браузер (TRUE/FALSE)
    'paths' => array(
        'subdomain-a' => '/www/a.project', // путь к корню поддомена
        'subdomain-b' => '/www/b.project',
        'cache'       => '/tmp/cache',     // путь к каталогу с кэшем
    ),
    'ioc' => array(
        'B-for-A' => 'ClassB', // какой класс использовать A в качестве модуля B
        'C-for-A' => 'ClassC',        
        'D-for-A' => 'ClassD',        
    ),
);

ioc здесь единственный общий для всех параметр. Все остальные скорее задают значения по-умолчанию и могут меняться для конкретных платформ.

Напишем общий конфиг для продакшена (prod.php):

return array(
    /**
     * База данных располагается на отдельном сервере и общая для всех трёх php-серверов.
     * Переопределим host, а остальные значения используем из базовой конфигурации.
     */
    'db' => array(
        'host' => '192.168.0.97:3307',
    );
    'debug' => false, // чтобы наверняка
    // paths устраивают те, которые определены в базовом
    // ioc не переопределяем
);

server1 и server3 полностью устраивает prod.php. Их настройки просты:

return array();

На server2 по каким-то причинам пришлось перенести один из каталогов:

return array(
    'paths' => array(
        'cache' => '/home/cache/cache',
    ),
);

Теперь dev.php (общая конфигурация разработчиков):

return array(
    'debug' => true, // вывод отладки разработчикам
    'ioc' => array(
        'C-for-A' => 'ClassCDev', // наследуется от ClassC и просто добавляет немного отладочной информации
    ),
);

Ну и конкретный разработчик (vasya.php):

return array(
    'db' => array(
        'username' => 'root', // не стал заморачиваться с созданием отдельного пользователя
        'password' => 'toor',
    ),
    'paths' => array( // и поддомены у него в отдельном месте лежат
        'subdomain-a' => '/home/vasya/www/a.project.loc',
        'subdomain-b' => '/home/vasya/www/b.project.loc',        
    ),
);

В конечном итоге, система на компьютере Василия будет иметь дело со следующей конфигурацией:

return array(
    'db' => array(
        'host'     => 'localhost', // base
        'username' => 'root',      // vasya
        'password' => 'toor',      // vasya
        'dbname'   => 'project',   // base
    ),
    'debug' => true,               // dev
    'paths' => array(
        'subdomain-a' => '/home/vasya/www/a.project.loc', // vasya
        'subdomain-b' => '/home/vasya/www/b.project.loc', // vasya
        'cache'       => '/tmp/cache',                    // base
    ),
    'ioc' => array(
        'B-for-A' => 'ClassB',    // base
        'C-for-A' => 'ClassCDev', // dev 
        'D-for-A' => 'ClassD',    // base
    ),
);

Офигенно.

Указание связей

Нужно как-то указать связь конфигов. То есть, система, подключая на платформе vasya её конфиг, должна знать, что он наследуется от dev, а тот в свою очередь от base. Указать это можно прямо в конфигах отдельным параметром:

base.php:

return array(
 
    '__parent__' => null,
 
    'db' => array( 
        // ...
    ),    
);

dev.php:

return array(
 
    '__parent__' => 'base',
 
    'db' => array( 
        // ...
    ),    
);

vasya.php:

return array(
 
    '__parent__' => 'dev',
 
    'db' => array( 
        // ...
    ),    
);

Ещё немного тюнинга

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

Например, каталог config/vasya:

db.php
debug.php
path.php
ioc.php

А уже в config/vasya/db.php:

return array(
    'host'     => 'localhost',
    'username' => 'root',
    'password' => 'toor',
    'dbname'   => 'project',
);

Сильно разветвлённые параметры — можно ещё и подкаталоги использовать.

Также можно вспомнить про адаптеры. Васе может быть удобно писать php-массивы, а Пете xml-файлы. А Миша для этой цели использует Brainfuck.

P.S.

И вот у нас с конфигами всё стало хорошо. Они наследуемые и гибкие.

Но не всегда. Иногда приходится наворачивать ещё больше сложностей. Но об этом в следующий раз.

34 комментария »

  • Ура! у тебя заработала подсветка кода! Я такой радый!

    Олег Горбунов, 28.01.2011, 9:02

  • Не заработала — я её подключил :)

    vasa_c, 28.01.2011, 9:09

  • Может я не в тему, а о каких конфигах идет речь? О Zend_Config или какой-то гипотетический конфиг?

    alexey_baranov, 28.01.2011, 15:07

  • О гипотетическом абстрактном хранении конфиге, беспременительно к интерфейсу его использования.
    Там вверху две ссылки на предыдущие статейки.

    vasa_c, 28.01.2011, 15:12

  • Главное вовремя остановиться ;)

    artoodetoo, 29.01.2011, 8:00

  • artoodetoo, нет, я абсолютно не собираюсь останавливаться )

    vasa_c, 29.01.2011, 12:27

  • Вообще ты изобретаешь симфонийский IoC. В прошлой статье я писал о том, что тупо мержить конфиги нельзя, так как там могут быть массивы в качестве значений. Так вот https://github.com/fabpot/symfony/pull/554 , кажется там делаются продвижения в этом направлении

    Костег, 30.01.2011, 18:33

  • Вернулся чтобы добавить еще немного очевидностей.

    >> Васе может быть удобно писать php-массивы, а Пете xml-файлы. А Миша для этой цели использует Brainfuck.
    Если исключить эту фигню, то всё красиво. Нет, пожалуй есть еще фигня:
    >> ‘__parent__’ => ‘dev’,
    Это значит скрипт конфигурации должен чего-то думать и вычислять. В этом нет необходимости!

    Как просто реализовать наследование конфигурации. Предположим мы в стиле Zend указываем конфигурацию через переменную среды. Для Apache:

    SetEnv APPLICATION_ENV artoodetoo

    В конфигурационном скрипте:

    $env = getenv('APPLICATION_ENV') or $_env = 'production';
    require $_root . 'protected/config/' . $env . '.php';

    Конфигурация artoodetoo.php, как и все остальные она возвращает массив PHP:

    // Наследуем от development
    $config = include dirname(__FILE__) . '/development.php';
    // Перекрываем что надо
    $config = array_merge($config, array(
    'db' => array(
    'host' => '192.168.0.97:3307',
    ),
    ));
    return $config;

    Если вам кажется, что я Капитан Очевидность, значит всё правильно и логично.

    artoodetoo, 19.05.2011, 22:51

  • Фигли ты не на слёте, КО?

    И как наследование сделать? Не production => все остальные, а с промежуточными этапами.
    И production в корне иметь тоже, имхо, не комильфо. Лучше какой-то базовый, а production отдельной веткой, как и все остальные.

    vasa_c, 21.05.2011, 10:50

  • >> И как наследование сделать? Не production => все остальные, а с промежуточными этапами.
    >>И production в корне иметь тоже, имхо, не комильфо. Лучше какой-то базовый, а production отдельной веткой, как и все остальные.

    Действительно очевидное труднее всего объяснить. КО разъяснияет:
    «наследование» здесь показано — artoodetoo наследует от development, а тот может наследовать от _root_ и т.д.
    Просто заводим такое _соглашение_: предок сначала подключает родителя, затем перекрывает что-то. Сколько угодно этапов. В коневом return array(), а в каждом дочернем:

    return array_merge_recursive(
    include('parent'),
    array('x'=>'overloaded value')
    );

    APPLICATION_ENV указывает на самый крайний этап, дальше цепочка самораскручивается.

    artoodetoo, 28.05.2011, 8:09

  • Единственное но принципиальное отличие от твоего варианта, то что класс «конфигурация приложения» не должен уметь обрабатывать ‘__parent__’ => ‘base’, соответственно мы вообще не вносим в него никакой логики чтения php/ini/xml — это все внешние зависимости. Мы скармливаем ему готовый массив и ниипет. Мы же стремимся к простоте.

    Config::init(include('./config/'.$env.'.php'))

    Если в жопу клюнула бешенная муха, заменяем на

    artoodetoo, 28.05.2011, 8:20

  • … бля, parse_ini_file() или mySuperPuperConfigReading() лишь бы на выходе было массивом.

    artoodetoo, 28.05.2011, 8:23

  • … бля-2, array_merge_recursive надо array_replace_recursive, а он только с версии 5.3. Я ненавижу PHP!

    artoodetoo, 28.05.2011, 9:10

  • artoodetoo, конфиг зачастую бывает весьма ветвист и если держать его в одном файле, разбирательство с ним может быть весьма неприятным. Поэтому я стараюсь разбивать его на разделы, каждый в своём файле.
    Кроме того, так гораздо легче переносить «модули» на новую систему, копированием нужных файлов, а не вычленением разделов из одного файла.

    vasa_c, 28.05.2011, 13:21

  • 1) все это у вас недостаточно гибко
    2) что б было достаточно гибко — вам нужно заново изобрести Symfony\Component\DependencyInjection + Symfony\Component\Config

    Костег, 28.05.2011, 20:37

  • Костег, приведи простой пример

    vasa_c, 28.05.2011, 23:54

  • Писал же http://blgo.ru/blog/2011/01/26/config-joint-platform/

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

    Костег, 29.05.2011, 13:56

  • Все ниасилил. Зачем все усложнять?

    include ROOT_DIR.’base.conf.php’;
    switch ($_SERVER[‘HTTP_HOST’]) {
    case ‘prod.ru’:
    case ‘prod-loc’:
    include ROOT_DIR.$_SERVER[‘HTTP_HOST’];
    break;
    default:
    die(‘Undefined host … bla-bla-bla’);
    }

    Евгений, 7.06.2011, 22:00

  • Евгений, к сожалению, на вопрос «зачем усложнять» нельзя доходчиво ответить.
    Когда человек начинает решать более сложные задачи, он начинает понимать, зачем усложнять решения. До этого момента объяснить невозможно.

    Просто на вскидку по вашему примеру:
    1. Что скрывается в этих include и как сливаются конфиги?
    2. Зачем выносить в код, то что конфигурируется?
    3. Как сделать промежуточные конфиги между base и последним?
    4. Что если у нас не HTTP-запрос, а CRON или какая-то утилита командной строки? Там не будет HTTP_HOST.

    vasa_c, 8.06.2011, 12:27

  • 1) Наследование действительно удобно делать инклудом и переопределением (тоесть include все-таки лучше чем ключ parent.
    4) А если не HTTP запрос, то нужно проверять наличие переменной

    Использую вот такое самописное решение
    https://github.com/ivan1986/quickfw/blob/a64d640d1933b3d598afff24fac0c2a8e8f85016/QFW/QuickFW/Config.php
    Плюс в том, что легко открепляется — нужно только поправить функцию files, которая генерирует имена проверяемых файлов — поддерживает все что написано в статье + удобно конфигурируется и легко отсоединяется.
    Можно написать классы для чтения из произвольного источника и возвращать их в файлах внутри папки, но этого не требовалось пока.

    Ivan1986, 12.06.2011, 0:22

  • >1) Наследование действительно удобно делать инклудом и переопределением (тоесть include все-таки лучше чем ключ parent.
    Мотивируйте.

    >4) А если не HTTP запрос, то нужно проверять наличие переменной
    Каждый раз в каждом месте где это надо, проверять тип запроса, основываясь на каких-то смутных проверках, ИМХО, не комильфо.
    Тип запроса должен быть определён один раз в самом начале и в зависимости от него определены нужные настройки, которые потом и надо использовать.

    vasa_c, 12.06.2011, 9:26

  • >> 1) Наследование действительно удобно делать инклудом и переопределением (тоесть include все-таки лучше чем ключ parent.
    > Мотивируйте.
    Оно более наглядно, можно комбинировать несколько конфигов разными инклудами, у нас уходит сокральное знание о параметре __parent__ и заменяется всем известным include, который в случае с __parent__ просто находится в конфигураторе

    > Тип запроса должен быть определён один раз в самом начале и в зависимости от него определены нужные настройки, которые потом и надо использовать.
    Разумеется, тоесть просто в том коде нужно поставить if (isset($_SERVER[‘HTTP_HOST’])) или как вариант в моем классе настроено по стандарту — при наличие переменной и файла он подгружается

    Ivan1986, 12.06.2011, 12:13

  • >Оно более наглядно
    Подкрепите примером

    vasa_c, 14.06.2011, 16:07

  • Ну имхо

    include __DIR__.’/dev.php’;
    $config[‘hostname’] = ‘site.my’;

    более наглядно чем

    $config[‘__perent__’] = ‘dev’;
    $config[‘hostname’] = ‘site.my’;

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

    Ivan1986, 14.06.2011, 18:23

  • >и потом кода меньше в конфиге
    Ну вот как раз в этом случае мы начинаем приносить код в конфиг и заставлять конфиг знать о реализации доступа к нему.

    Разрастётся наш конфиг и захочется нам разбить его на несколько файлов.
    И вот include приходится писать в каждом файле и в случае необходимости изменять в каждом.
    Причём теперь у нас include(__DIR__.’/../base_config/subconfig.php’);

    Как-то нужно сливать эти файлы и делать это придётся в самих конфиг-файлах. На другие форматы хранения прозрачно не перейти.

    Ну и сам формат с return array(); куда нагляднее, чем $config[‘param’]=value; а в этом случае его использовать нельзя.

    vasa_c, 19.06.2011, 21:02

  • Васяц, хз что наглядней, это дело вкуса. Инклуд в конфиге плох только тем, что он возможен только в php. Для прочих ini и xml это невозможно :)
    Если действительно хочется использовать разные форматы представления, то да, нужен __parent__.

    Я бы в таком раскладе таки вынес обработку ‘__parent__’ из класса «конфигурация» вовне, в скрипт инициализации, там бы всё клеил и скармливал готовый массив своему классу «конфигурация».

    // Iterate configurations from descendant to ancestor
    // Child values overload the parent ones
    $config = array();
    while ($env) {
    $conf = include($dir . ‘config/’ . $env . ‘.php’);
    $env = isset($conf[‘__parent__’]) ? $conf[‘__parent__’] : FALSE;
    unset($conf[‘__parent__’]);
    $config = array_replace_recursive($conf, $config);
    }
    Config::apply($config);

    Это работает.

    artoodetoo, 28.06.2011, 6:56

  • artoodetoo, вообще-то я в прошлом камменте налегал не на ini и xml, а на разбиение одного конфига на множество файлов.

    Про код не до конца понял. Ты только обработку выносишь? __parent__ в конфиге оставляешь?

    vasa_c, 30.06.2011, 11:15

  • Код выше это вариант отказа от вложенных include в пользу итерации.
    Инклуд вынес из конфига, а парент, соответственно, внес. Хранилище конфигурации по прежнему не знает о моей магии, все происходит снаружи него. Я считаю это важным.

    artoodetoo, 4.07.2011, 20:46

  • >> разбиение одного конфига на множество файлов
    в том прошлом посте ты использовал слова «приходится» и «необходимость». я вижу здесь проблему не реализации, а планирования. если увлечься, то можно потерять контроль над картиной в целом.
    я бы предпочел иметь или «горизонтальное» разделение по функионалльной принадлежности или «вертикальное» по сфере использования, но не одновременно.

    artoodetoo, 4.07.2011, 21:15

  • >Хранилище конфигурации по прежнему не знает о моей магии, все происходит снаружи него.
    У нас есть:
    1. Хранилище конфигурации, с этими самыми return array()
    2. Класс, занимающийся сбором этой конфигурации и предоставляющий интерфейс для доступа к ней.

    Ты о том же или я запутался?

    vasa_c, 5.07.2011, 12:02

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

    vasa_c, 5.07.2011, 12:05

  • >> Ты о том же или я запутался?
    Наверное. Или я запутался. )))

    Олег, ты очень продвинутый чел. Я бы почел за честь чему-то научиться у тебя.
    [ Пошел ломать сайтик ГО чтобы взглянуть на его внутренности ]

    artoodetoo, 6.07.2011, 8:39

  • Но не такой, как Фабьен)

    Костег, 6.07.2011, 9:02

  • >Олег, ты очень продвинутый чел. Я бы почел за честь чему-то научиться у тебя.
    Спасибо за тёплые слова, но я просто задрот :)

    vasa_c, 6.07.2011, 22:32

Leave a comment