Наследование конфигов в Zend_Config

    Для тех, кому лень читать длинное предисловие: перемотайте до последней части «Простая идея, которая пришла мне в голову».
    Я хотел поставить якорь, но хабрапарсер не разрешает :(

    Zend_Config и секции


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

    На первый взгляд, такая идея кажется разумной, но я столкнулся с некоторыми ограничениями этого подхода…

    Вот пример конфигурационного файла из документации:
    ; Production site configuration data
    [production]
    webhost                  = www.example.com
    database.adapter         = pdo_mysql
    database.params.host     = db.example.com
    database.params.username = dbuser
    database.params.password = secret
    database.params.dbname   = dbname

    ; Staging site configuration data inherits from production and
    ; overrides values as necessary
    [staging : production]
    database.params.host     = dev.example.com
    database.params.username = devuser
    database.params.password = devsecret


    Итак, мы определяем полный список параметров для в секции production, а в секции staging переопределяем лишь несколько параметров для доступа к базе данных.

    Ограничения секционного подхода


    На практике, во-первых, хранить пароль доступа к продакшен БД в общем конфиге — не самая хорошая идея, а во-вторых, конфиг в таком виде в буквальном смысле невозможно хранить в мейнстримных системах контроля версий (СКВ) типа Git или Subversion.

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

    Традиционные сценарии хранения конфига в СКВ


    Чтобы общий конфиг не засорялся личными настройками девелоперов, обычно его называют как-то типа config.default.ini или config.ini.default, кладут в репозиторий, а затем каждый девелопер в своём рабочем каталоге делает его копию, которую называет config.ini и которую добавляет в список игнорируемых файлов СКВ.

    Казалось бы, вот оно — счастье: один раз скопировал дефолтный конфиг, вбил туда личные настройки и забыл о нём навсегда…

    Как бы не так!

    Проходит месяц и кто-то из разработчиков решает добавить в кофиг какую-нибудь полезную (или не очень) настройку.
    Он молча коммитит эту настройку в дефолтный конфиг, затем вносит изменения в код проекта, который полностью ломается, если в конфиге эта настройка не указанал, и с чувством выполненного долга уходит в отпуск…

    Догадываетесь, что происходит на следующий день? :)

    Даже если рассматривать самую утопическую ситуацию, когда изменивший конфиг девелопер не забывает отправить письмо в корпоративную рассылку с темой «Please update your local config.ini», а получившие это письмо разработчики не забывают его прочитать и, что немаловажно, осознать и выполнить — всё равно всем им приходится вручную обновлять свои локальные конфиги, порой используя утилиты типа diff, так как со временем конфиги только увеличиваются в размерах и следить за всеми изменениями вручную не удается.

    Простая идея, которая пришла мне в голову


    Я решил расширить класс Zend_Config и реализовать в нём наследование конфигов.
    Но как только я заглянул в его код, то сразу понял, что всё необходимое для наследования заложено в него изначально.

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

    Общий конфиг проекта — config.common.ini:
    [production]
    resources.db.adapter                = "PDO_MySQL"
    resources.db.params.dbname          = "system"
    resources.db.params.username        = "root"
    resources.db.params.password        = ""
    phpSettings.display_startup_errors  = 0
    phpSettings.display_errors          = 0

    [development : production]
    phpSettings.display_startup_errors  = 1
    phpSettings.display_errors          = 1


    Мой личный конфиг — config.ini:
    [production]
    [development : production]
    resources.db.params.dbname          = "system_laggyluke"
    resources.db.params.username        = "laggyluke"
    resources.db.params.password        = "mySecretPassword"


    Пример реализации наследования конфигов:
    // загружаем общий конфиг
    // третий аргумент true означает, что конфиг не будет открыт в режиме read-only
    $config = new Zend_Config_Ini('config.common.ini', 'development', true);
    // проверяем, существует ли личный конфиг
    if (file_exists('config.ini')) {
        // если существует - загружаем его...
        $configCustom = new Zend_Config_Ini('config.ini', 'development');
        // ...и сливаем два конфига в один
        $config->merge($configCustom);
    }
    // возвращаем конфиг в режим read-only, на всякий случай
    $config->setReadOnly();


    * This source code was highlighted with Source Code Highlighter.


    Общий конфиг config.common.ini хранится в СКВ и любые новые настройки, которые добавляются в него, сразу же попадают ко всем разработчикам. В то же время, каждый разработчик может переопределить любую настройку в своём личном config.ini, который игнорируется СКВ.

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

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

    UPD.
    stfalcon подсказал, что нечто подобное уже реализовано в Zend_Application, начиная с версии 1.8.2.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 21

      +1
      Соглашусь, самый правильный вариант. Также хочу добавить, что в питоне (django) подобное делаем с помощью строки в конце конфигурационного файла:
      try:
          from local_settings import *
      except ImportError:
          pass
      

      Понятно, что local_settings централизовано добавлен в игнор и репозитории его нет.
        0
        Хорошая идея. Сейчас учту это в своем проекте.
          +1
          Хорошая статья, спасибо.
            0
            почему бы тогда не переписать конструктор так, чтобы он сам загружал рекурсивно конфиги и сливал данные?

            a.b.c.d.ini -> b.c.d.ini -> c.d.ini -> d.ini

            м?
              0
              Можно и так, просто меня полностью устроила эта реализация.
              Мне тяжело представить себе ситуацию, когда каскадное наследование будет полезно — скорее, оно будет стимулировать разработчиков к тому, чтобы… эмм… использовать его :), что может привести к появлению многоуровневой иерархии конфигов в проекте — имхо, перебор.

              Кроме того, как сказал какой-то умный человек: самый лучший код — это тот код, которого нет. Он не содержит ошибок, его не надо тестировать, отлаживать, поддерживать и т.д. :)
              Так что как только я понял, что для получения необходимой мне функциональности стандартную реализацию Zend_Config наследовать не обязательно, я был только рад.
                0
                >> «Мне тяжело представить себе ситуацию, когда каскадное наследование будет полезно»
                а о чём ты написал топик тогда? :-) выроди мой пример до 2х уровней и будет абсолютно твой пример :-)

                ps: можно и не наследовать, кстати. ещё лучше реализовать некое подобие the builder pattern, класса, который будет собирать нужный тебе конфиг :-)
                  0
                  а о чём ты написал топик тогда? :-)
                  А это топик-сюрприз: название говорит «наследование», а на самом деле речь идёт о том, как два конфига слить в один :)

                  На самом деле, я хотел сделать акцент на идеологию, а не на реализацию.
                  Плюс руководствовался принципом KISS.
                    0
                    как мне кажется, идея именно с наследованием весьма клёвая :-) особенно в контексте «своего» и «продакшн» конфигов.

                    ps: перефразирую вас
                    «а на самом деле речь идёт о том, как два конфига слить в один»
                    — >>
                    «о, а в конфигах зенда есть merge()!»
                    ;-)
                      +1
                      На самом деле, когда я перечитывал топик перед опубликованием и осознал, насколько он раздутый получился, именно такая мысль у меня и возникла: удалить всё нафиг, написать в теме «У Zend_Config есть метод merge()!» и в контенте — «subj» :)
              +4
              Короче, Склифасовский :) Имхо, примера с кодом было бы достаточно.
                +1
                Уж простите, люблю всё «разжёвывать», чтобы даже у человека «не в теме» вопросов не оставалось.
                А первые пару строчек ведь не зря написал :)
                0
                Красивое решение! Тоже задумывался о решении такой проблемы. Хоть она для меня пока не актуальна, но все равно спасибо, думаю пригодится.

                ЗЫ. Только локальный конфиг я бы назвал типа «config.local.ini», а основной оставил бы так как есть «config.ini». Так привычнее.
                  –1
                  Нда, наследование конфигов — интересная идея.

                  У меня вот другой подход к данной проблеме: для констант юзаем фукнцию

                  GetConstant($sKey,$sDafultValue) — и в конфиге только настройки базы. Все остальное правится любо заказчиком в модуле админки Константы, либо програмерами в том же модуле.
                    –1
                    Код на пхп:
                    //-----------------------------------------------------------------------------------------------
                    /**
                    * Updates or creates constants used all over the project
                    */
                    function UpdateConstant($sKey, $sValue) {
                    Base::$db->Execute(«insert into constant(key_,value) values ('».$sKey."','".mysql_real_escape_string($sValue)."')
                    on duplicate key update value='".mysql_real_escape_string($sValue)."'");
                    }
                    //-----------------------------------------------------------------------------------------------
                    function GetConstant($sKey,$sDefaultValue='') {
                    if (!isset(Base::$aConstant[$sKey]['value'])) {
                    Base::$db->Execute(«insert into constant(key_,value) values ('$sKey','$sDefaultValue')»);
                    Base::$aConstant[$sKey]['value']=$sDefaultValue;
                    return $sDefaultValue;
                    }
                    return Base::$aConstant[$sKey]['value'];
                    }
                    //-----------------------------------------------------------------------------------------------
                      –1
                      таблица:
                      CREATE TABLE IF NOT EXISTS `constant` (
                      `id` int(11) NOT NULL auto_increment,
                      `key_` varchar(255) NOT NULL,
                      `value` varchar(255) NOT NULL,
                      PRIMARY KEY (`id`),
                      UNIQUE KEY `key_` (`key_`)
                      )
                      –1
                      Забыл самое главное сказать — каждый, кто вводит константу — добавляет дефолтное значение, чтобы у других не поламался код.
                    • UFO just landed and posted this here
                        +1
                        Прикольна идея мерджить конфиги, оссобенно учитывая зенд_аппликейшин и описание ресурсов в конфиге… там дефолтный конфиг получается на 1к строк…
                          +1
                          опять наткнулся на эту тему. оказалось, что с выходом ZF 1.8.2 все можно делать намного проще. просто в конфиге написать что-то типа:
                          config = APPLICATION_PATH "/config/conf.local.ini"

                          вот эта тема zendframework.ru/forum/index.php?topic=1302.0
                          веллкам на форум зендфреймоврк.ру ;)
                            0
                            Хм, интересно.
                            Совсем недавно изучал код Zend_Application, но видать эту пару строчек пропустил.
                            Хотя тут возникают сложности, например локальный конфиг обязательно должен присутствовать — использовать только общий конфиг уже не получится.

                            Тем не менее, спасибо за наводку, сейчас обновлю статью.
                              0
                              А в Zend 1.11.2 я столкнулся с багом, при котором локальные конфиги не работают… А в 1.10dev все было ок :)

                              В результате оказалось что в 11 версии они почему то поменяли порядок массивов при merge, в результате локальные настройки затирались…

                            Only users with full accounts can post comments. Log in, please.