Yii и хранение настроек в базе данных
Как многим известно, для хранения настроек приложения в Yii выделен специальный раздел params
в конфигурационном файле. Это решение достаточно простое, но оно не позволяет легко менять настройки самому пользователю в панели управления сайта. Очередной вопрос на русском форуме Yii натолкнул меня поделиться своим вариантом решения упомянутого там вопроса.
Итак, представим, что в определённый момент мы всё-таки решились хранить настройки в базе данных, чтобы иметь возможность изменять их в админке. В своём вопросе автор упоминает о хранении массива значений в публичном поле $settings
базового класса контроллера. Этот подход, конечно, имеет право на существование, но мы попытаемся пойти более универсальным путём.
Использование публичного поля контроллера имеет небольшой недостаток: наличие этого поля и инициализирующего метода, записывающего в него параметры из базы, немного «захламляет» контроллер и отвлекает его от своих непосредственных обязанностей. Настройки могут понадобиться не только контроллеру, но и любому другому компоненту (например, число элементов нужно виджету вывода последних комментариев, а адрес администратора может пригодиться какой-нибудь модели для отправки уведомлений). Для них обращение к полю контроллера будет не очень красивым:
Yii::app()->controller->settings['param'];
Всё это даёт лишнюю и не очень удобную привязку всех подсистем к контроллеру.
Таким образом, более логично и более правильно вынести эту ответственность хранения параметров в отдельный специализированный компонент.
Создадим таблицу в базе данных для хранения наших настроек
CREATE TABLE IF NOT EXISTS `config` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `param` varchar(128) NOT NULL, `value` text NOT NULL, `default` text NOT NULL, `label` varchar(255) NOT NULL, `type` varchar(128) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `param` (`param`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
и сгенерируем к ней стандартную модель Config
:
<?php /** * This is the model class for table "{{config}}". * * The followings are the available columns in table '{{config}}': * @property string $id * @property string $param * @property string $value * @property string $default * @property string $label * @property string $type */ class Config extends CActiveRecord { public static function model($className=__CLASS__) { return parent::model($className); } public function tableName() { return '{{config}}'; } public function rules() { return array( array('value', 'safe'), array('id, param, value, label, type, default', 'safe', 'on'=>'search'), ); } }
В таблицу кроме полей param
и value
мы добавили поля default
для значения по умолчанию и label
для хранения названия настройки. В панели управления от стандартного CRUD оставляем только действие actionUpdate()
с возможностью изменять поле value
. Операций добавления или удаления там быть не должно, так как содержимое этой таблицы вбивает вручную прямо в базу данных разработчик приложения. Кроме того, имеется атрибут type
для указания типа значения (например string, text, checkbox) для вывода нужного типа поля в форме редактирования настроек.
Теперь для удобного чтения параметров нужно сделать компонент:
<?php /** * @author ElisDN <mail@elisdn.ru> * @link https://elisdn.ru */ class DConfig extends CApplicationComponent { protected $data = array(); public function init() { $items = Config::model()->findAll(); foreach ($items as $item){ if ($item->param) $this->data[$item->param] = $item->value === '' ? $item->default : $item->value; } parent::init(); } public function get($key) { if (array_key_exists($key, $this->data)){ return $this->data[$key]; } else { throw new CException('Undefined parameter '.$key); } } public function set($key, $value) { $model = Config::model()->findByAttributes(array('param'=>$key)); if (!$model) throw new CException('Undefined parameter '.$key); $this->data[$key] = $value; $model->value = $value; $model->save(); } }
и подключить его к приложению в файле конфигурации:
'components'=>array( // ... 'config'=>array( 'class' => 'DConfig' ), ),
При инициализации компонента все настройки считаются в массив $data
. Для автоматической загрузки компонента в начале работы приложения можно поместить его имя в список preload
:
// preloading components 'preload'=>array( 'log', 'config' ),
Теперь достаточно вбить в базу данных все параметры и считывать в любом месте приложения вызовом метода get()
:
Yii::app()->config->get('NEWS_PER_PAGE');
По возможности лучше закешировать чтение из базы в методе init()
:
$items = Config::model()->cache(3600)->findAll();
Но тогда не забывайте сбрасывать кэш при каждом изменении настроек. Также для экономии памяти при работе с сотнями параметров можно заменить чтение с использованием СActiveRecord::findAll()
на запрос DAO.
Небольшой совет: При увеличении числа параметров может возникнуть ситуация, когда нескольким из них нужно дать похожие имена. При этом нужно следить, чтобы имена не совпадали, а это может привести к появлению сложных имён типа
NEWS_PER_PAGE
,POSTS_PER_PAGE
,NEWS_PER_PAGE_IN_HOME
. Также при выводе в списке (например, в CGridView) все параметры трудно «красиво» отсортировать. Для решения этих проблем можно называть параметры по шаблону ГРУППА.ПАРАМЕТР вродеNEWS.PER_PAGE
,BLOG.PER_PAGE
,SHOP.PER_PAGE
,NEWS.PER_HOMEPAGE
. Это одновременно избавляет от случайного повторения имён и выводит параметры группами при сортировке их имён по алфавиту.
Присутствующий метод set()
позволяет при необходимости менять значение любого параметра. Это, например, может пригодиться для изменения курса доллара по планировщику в вашем интернет-магазине:
$current_curs = ... ; if ($current_curs && Yii::app()->config->get('SHOP.DOLLAR_CURS') != $current_curs) Yii::app()->config->set('SHOP.DOLLAR_CURS', $current_curs);
При разработке приложения программист вбивает в таблицу нужные параметры, указывает их значения по умолчанию и отдаёт сайт заказчику. Теперь заказчик может изменять параметры в соответствующем разделе панели управления не влезая в код.
Пойдём дальше и сделаем более полноценную версию компонента (с использованием DAO для выборки и кэшированием):
<?php /** * @author ElisDN <mail@elisdn.ru> * @link https://elisdn.ru */ class DConfig extends CApplicationComponent { public $cache = 0; public $dependency = null; protected $data = array(); public function init() { $db = $this->getDbConnection(); $items = $db->createCommand('SELECT * FROM {{config}}')->queryAll(); foreach ($items as $item) { if ($item['param']) $this->data[$item['param']] = $item['value'] === '' ? $item['default'] : $item['value']; } parent::init(); } public function get($key) { if (array_key_exists($key, $this->data)) return $this->data[$key]; else throw new CException('Undefined parameter ' . $key); } public function set($key, $value) { $model = Config::model()->findByAttributes(array('param'=>$key)); if (!$model) throw new CException('Undefined parameter ' . $key); $model->value = $value; if ($model->save()) $this->data[$key] = $value; } public function add($params) { if (isset($params[0]) && is_array($params[0])) { foreach ($params as $item) $this->createParameter($item); } elseif ($params) $this->createParameter($params); } public function delete($key) { if (is_array($key)) { foreach ($key as $item) $this->removeParameter($item); } elseif ($key) $this->removeParameter($key); } protected function getDbConnection() { if ($this->cache) $db = Yii::app()->db->cache($this->cache, $this->dependency); else $db = Yii::app()->db; return $db; } protected function createParameter($param) { if (!empty($param['param'])) { $model = Config::model()->findByAttributes(array('param' => $param['param'])); if ($model === null) $model = new Config(); $model->param = $param['param']; $model->label = isset($param['label']) ? $param['label'] : $param['param']; $model->value = isset($param['value']) ? $param['value'] : ''; $model->default = isset($param['default']) ? $param['default'] : ''; $model->type = isset($param['type']) ? $param['type'] : 'string'; $model->save(); } } protected function removeParameter($key) { if (!empty($key)) { $model = Config::model()->findByAttributes(array('param'=>$key)); if ($model) $model->delete(); } } }
Для активации кэширования настройте кэш приложения и установите нужные значения атрибутам cache
и dependency
'components'=>array( // ... 'config'=>array( 'class'=>'DConfig', 'cache'=>3600, ), ),
Этот компонент дополнен методами add()
и delete()
для непосредственного добавления и удаления параметров. Этим методам можно передавать как один параметр:
Yii::app()->config->add(array( 'param'=>'BLOG.POSTS_PER_PAGE', 'label'=>'Записей на странице', 'value'=>'10', 'type'=>'string', 'default'=>'10', )); Yii::app()->config->delete('BLOG.POSTS_PER_PAGE');
так и массив:
Yii::app()->config->add(array( array( 'param'=>'BLOG.POSTS_PER_PAGE', 'label'=>'Записей на странице', 'value'=>'10', 'type'=>'string', 'default'=>'10', ), array( 'param'=>'BLOG.POSTS_PER_HOME', 'label'=>'Записей на главной странице', 'value'=>'5', 'type'=>'string', 'default'=>'5', ), )); Yii::app()->config->delete(array( 'BLOG.POSTS_PER_PAGE', 'BLOG.POSTS_PER_HOME', ));
Предположим, что разрабатывая нами система должна поддерживать автоматическую установку (и удаление) модулей. Для этого, например, в каждом модуле мы можем создать методы install()
и uninstall()
и прописать в них добавление и удаление нужных параметров и создание директорий для загрузки файлов:
class BlogModule extends CWebModule { public function init() { $this->setImport(array( 'blog.components.*', 'blog.models.*', )); } public function install() { Yii::app()->config->add(array( array( 'param'=>'BLOG.POSTS_PER_PAGE', 'label'=>'Записей на странице', 'value'=>'10', 'type'=>'string', 'default'=>'10', ), array( 'param'=>'BLOG.POSTS_PER_HOME', 'label'=>'Записей на главной странице', 'value'=>'5', 'type'=>'string', 'default'=>'5', ), )); $path = Yii::getPathOfAlias('webroot.upload.blog'); if (!is_dir($path)) @mkdir($path, 755); } public function uninstall() { Yii::app()->config->delete(array( 'BLOG.POSTS_PER_PAGE', 'BLOG.POSTS_PER_HOME', )); } }
В контроллере раздела «Управление модулями» можно производить их установку и удаление вызывая соответствующий метод модуля:
$module = Yii::app()->getModule($moduleName); $module->install();
или
$module = Yii::app()->getModule($moduleName); $module->uninstall();
На этом всё. Теперь необходимые каждому модулю параметры будут добавляться в базу автоматически при установке модуля.
Вываливается ошибка: Fatal error: Class 'DConfig' not found in Z:\home\yii\framework\YiiBase.php on line 219. Расскажите ка избавится.
Это значит, что системе не удалось найти этот класс. Убедитесь, что класс находится в файле DConfig.php и он доступен для импорта. Для этого его можно положить в папку protected/components и настроить импорт всех классов из неё:
или положить в произвольную директорию (например protected/components/config) и указать путь к ней непосредственно в строке подключения
В Вашем листинге в начале отсутствует <?php
Резюме: тупое копирование приводит к вышеописанным результатам.
Спасибо за статью
Автор - респект и уважуха. Сайт - супер! Статьи - просто прелесть. Продолжай в том же духе. Сайт однозначно в закладки.
Изложение просто замечательное!!! Молодец!
Ого)) Спасибо, стараюсь.
Дмитрий, не мог бы ты привести описание экшена по редактированию настроек из конфига?
Ничего специфичного в экшене нет. Обычная работа с ActiveRecord моделью Config. Подойдёт стандартный сгенерированный в gii CRUD контроллер с выводом списка в CGridView и редактированием модели в форме CActiveForm. Только операции create и delete из него нужно удалить.
Отличие только в выводе поля $model->value в форме. Там надо вывести input, textarea или checkbox в зависимости от типа поля $model->type. Например, так:
Я у себя использую табличный ввод для отображения всех параметров в одной большой форме.
Это понятно... Непонятно как использовать совместно с YiiBooster.
Потому как echo $form->textFieldRow($model, 'value', естественно не отображает label поля...
Конечно можно использовать HTML разметку из бутстрапа, но как то... как то не кошерно получается, не по Yii-шному :)
Понятно. Всё-таки проще вручную label подписать. Но это сделка с совестью :)
Пришла мысль, что замечательно было бы построить грид с Ajax редактированием: щёлкаешь по параметру, а его поле выскакивает во всплывающем окне.
ну собственно так и сделал:
Кстати на сайте невозможно зарегистрироваться, после активации вот что:
Ошибка 500
Не определено свойство "User.user".
Поправь. У тебя очень интересные статьи, приятно читать...
C отсутствующей аватаркой проблемы были. Исправил.
скажите, а сеттер где добавляется? не получается произвести обновление
спасибо.
В смысле? Не работает конструкция
или другая проблема?
Дмитрий, есть ли разница где мне установить
- вьюшка, модель, контроллер. и.пр.? (для теста)
я добавил во вьюшку, выдается ошибка подкл. файла.
по идее должен везде подгружать?
спасибо
Разницы никакой нет. Всё доступное по Yii::app() работает везде. Ошибка говорит, что Yii не смог найти ActiveRecord класс модели Config. Вы точно её создали?
странно.
данные выводит, но переделывая на set и добавляя новые значения, выдает нет файла!
может проблема в модульности?
хотя, опять же, данные то выводятся!!!
Добавьте в начале DConfig.php строку импорта модели, например:
не, так не работатет
Yii::import('applications.modules.admin.models.Config');
, я попробовал перед сеттером установить Yii::app()->getModule('admin');
сработало как нужно.
Спасибо за труд, качественные статьи.
Статья супер! Впрочем как и все остальные!
Спасибо!
Здравствуйте! Статья отличная, но у меня возник вопрос: А что делать если я хочу хранить в настройках такие данные как host, bdname,bduser,bdpassword,host?
Интересный вопрос. Тогда Вы никогда не подключитесь к базе данных...
Ну да, тупанул. Придётся при установке просить указать эти настройки. Без последующей возможности изменить их не трогая main.php
Отличное решение, спасибо! Вечно мучал этот вопрос. Скажите, а как Вы очищаете кэш при сохранении настроек? Или просто используете dependecies?
П.С. у CApplicationComponent в init надо вроде parent::init вызвать.
Можно очищать весь кэш вручную в админке или, если один запрос не будет лишним, вообще не кэшировать. Либо использовать тэгирование кэша и в actionUpdate сбрасывать кэш только этого компонента по тегу.
А можно как-то задать настройки внутри конфига, например:
?
Прямо так присвоить значение не получится, так как в index.php конфигурационный файл считывается до запуска приложения. Но можно вписать туда анонимную функцию в качестве обработчика какого-либо события приложения, и уже в ней производить любые операции:
Статья отличная!
Скажите, а как лучше выводить эти настройки по группам? Допустим, в разных контроллерах.
Общие настройки, Настройки новостей, Настройка рассылки.
И меня имена всех параметров имеют вид МОДУЛЬ.ПАРАМЕТР, то есть для блога:
Поэтому при сортировке по алфавиту они у меня сгруппировываются сами. Общие параметры можно «обозвать» как GENERAL или MAIN.
Но можно добавить отдельное поле для групп и сортировать по нему:
Дмитрий, благодарю за ответ. Но вы наверное меня не правильно поняли.
В текущей версии у вас все настройки выводятся на одной странице. А если у меня 100 надстроек? Тогда скорее всего нужно в модуле настроек сделать несколько контроллеров BlogController, PostController и т.д. чтобы разбить настройки по страницам. Вариантов осуществить это масса. Но меня интересует как бы поступили вы?
Именно поля для ввода всех параметров на одной странице выводить не обязательно. Меня бы устроил обычный CGridView с разбивкой на страницы с выпадающем списком-фильтром в столбце «Группа».
Хм... понятно. Но я вот хочу доработать ваше решение создав в нем возможность редактировать настройки на разных страницах модуля. Но так как я не опытный yiiст, по скорее всего попрошу вас взглянуть на мое решение, если вам будет интересно=)
Вот дела... не могу опубликовать код. Вышлю на почту.
У меня в базе value IS NULL, в $this->data['my_param'] = null; Из-за этого метод всегда выбрасывает исключение. Проверять лучше, в т.ч. из-за скорости, через array_key_exists.
ЗЫ: спасибо за модуль
Спасибо. Заменил.
Добрый день.
Что-то никак сообразить не могу, а как в параметры в БД засунуть список?
вот такой, например:
чтобы при этом не сильно ломать другой код, где это перечисление используется так:
Спасибо.
Можно сериализовать, например.
да, спасибо.
я про JSON прочитал. Наверно что-то в этом направлении надо делать.
Я сишник много-много лет :), а с PHP и YII всего месяц занимаюсь - жизнь заставила. Поэтому и вопросы возникают. Идеология программирования совершенно другая :)
Даже без JSON можно. Обычные serialize() и unserialize() подойдут.
Не могли бы вы показать пример кода. А то я не совсем понимаю где должен храниться сериализованный массив и его значение.
Дмитрий, спасибо огромное за Ваш ресурс. Искренне желаю Вам успехов ибо на мой взгляд Ваш сайт один из полезнейших ресурсов рунета. Особенно радуют статьи по Yii. А еще радует приятный внешний вид, как Ваш, так и сайта в целом.
Здравствуйте, Дмитрий!
Спасибо за интересную статью. Я изучаю Yii и попробовал реализовать обработку настроек Вашим методом. У меня через раз получается ошибка:
Подскажите, в чем дело?
С уважением.
А с CFileCache работает?
Да, на нем и остановился.
Какие расширения для Yii порекомендуете посмотреть для новичка, так сказать маст хэв?
Добрый день, подскажите пожалуйста, где красивее или правильнее писать данные строки
Yii::app()->config->get('NEWS_PER_PAGE');
По возможности лучше закешировать чтение из базы в методе init():
$items = Config::model()->cache(3600)->findAll();
И еще вопрос - что такое NEWS_PER_PAGE ? Вроде как подразумевается параметр для постраничной навигации, но в таблице такого имени поля нет, объясните пожалуйста.
Пишем прямо там, где используются, например:
Закешировать – заменить первую строку в методе init компонента:
NEWS_PER_PAGE - это просто я так назвал параметры (в поле param).
Спасибо, Дмитрий, изучаю Yii по урокам на вашем сайте.
Сделал компонент по примеру, но возник вопрос:
Как использовать поле label и type в форме редактирования?
Об этом ничего не сказали.
Можно делать дополнительные запросы из вьюхи, но это по моему костыль.
Дмитрий вопрос (пришел сюда из Yii2).
1. Обязательно ли создавать поле id. Может ли поле param выступать в роли первичного ключа?
2. Хочу создать еще одну таблицу config_description (config_id, text) для описания настройки, но делать отдельно чтобы не тянуть это во всем приложении, а запрашивать только в админке для помощи в настройке.
Как правильно написать ограничение внешнего ключа для таблицы config_decription? То есть чтобы удалялся или изменялся при изменении param таблицы config?
1. Можно любое поле. Просто привычка называть id.
2. С поля config_param на param.
А чем плох такой вариант? правда уже на Yii2
В админке сделал модельку настроек и созхраняю их в файл
ну и вывожу потом
в конфиге приложения $params = unserialize(file_get_contents(__DIR__ . '/params.php'));
Классные статьи, автору спасибо.
Как вот эту строку под Yii2 правильно переписать?
$db = Yii::app()->db->cache($this->cache, $this->dependency);
Да и как в Yii2 лучше поступить с
$module = Yii::app()->getModule($moduleName);
$module->install();
и так далее?
Также Yii::$app->getModule($moduleName).
Правильно ли я вообще понял идею.
Через компонент мы можем получить только value или default по имени параметра? Если нам надо остальные поля, как label и так далее, то придётся через модель действовать? Что-то вроде такого
$result = ConfigModel::findOne(['param' => 'SITE.BASE_ADDR']);
$result->label;
Только начал Yii2 изучать, ещё не всё понятно, но фреймворк понравился)))
Да, остальное через обычный Activerecord.
Дмитрий, спасибо за статью!
А можете рассказать как хранить настройки в Symfony?
Также сделать сервис Config.
А если он нужен почти в каждом контроллере и во многих других сервисах - протаскивать его туда через DI?
Можно сразу доставать из него значения в контейнере:
Благодарю!