Продолжаем разговор из прошлых двух частей.
Конфигурация сайта 1: введение
Конфигурация сайта 2.1: совместная разработка (платформы)
В прошлой части у нас массив, соответствующий какой-то платформе (например, vasya), вливался в массив некой базовой конфигурации (config.php
). Назовём это: «vasya
наследуется от config
».
Раньше я делал так:
config
— конфигурация системы на рабочем сервере, от неё наследуются конфигурации разработчиков.
Оно не всегда удобно. Лучше наследоваться от какой-то базовой конфигурации, а рабочей сервер поставить в один ряд с разработчиками. Тем более в нашем примере рабочих серверов у нас несколько.
Наследуем всё от базового конфига и введём ещё промежуточные этапы:
Все платформы, что разработческие, что рабочие, наследуются от одной базовой структуры.
Зачем промежуточные dev
и prod
? В dev'е
собраны девелоперы, в prod'е
продакшен-серверы. У каждой из этих групп могут быть свои общие для всей группы настройки, отличные от другой группы. Например, в dev
включена отладка и отключен кэш для js и css. Что, впрочем, не помешает Пете у себя отладку отключить, когда она ему надоест.
Пример
И вот есть у нас умозрительная система, давайте её хорошенько поконфигурируем.
Будем конфигурировать:
- Базу данных
- Вывод отладочной информации
- Пути к некоторым каталогам
- Инверсию зависимостей
С инверсией зависимостей ничего придумывать навороченного не будем. Пускай наша система на верхнем уровне состоит из 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.
И вот у нас с конфигами всё стало хорошо. Они наследуемые и гибкие.
Но не всегда. Иногда приходится наворачивать ещё больше сложностей. Но об этом в следующий раз.
Ура! у тебя заработала подсветка кода! Я такой радый!
Олег Горбунов, 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