За долгое время работы с Yii Framework накопилось некоторое количество полезного опыта. Хочу им поделиться с читателим. Всё что ниже написано — плоды рефакторинга и трезвого взгляда на код.
То, о чем я расскажу под катом:
- Открытие одной и той же странички: через ajax-запрос (без layout) и обычное открытие странички вместе с layout
- Кеширование моделей без кода в каждой модели
- Как сделать логирование логики с минимальным кодом
- Как обернуть всё в транзакции с минимальным кодом
- Как сделать так, чтобы на каждом сервере (с экземпляром приложения) не менять файл основного конфига приложения. Упрощаем деплой
Открытие одной и той же странички: через ajax-запрос и обычное открытие странички с layout
Описание: Очень полезно бывает уметь открывать одну и ту же страницу (одна и таже view + action) и как фрагмент страницы запрошенный через ajax ( $.load() ) и как цельную страницу вместе с layout.
Пример применения: Табы на bootstrap с реализацией через ajax. Они сделаны в виде <li><a></a></li> — соответственно их можно «открыть в новом окне» — тут будет полезно уметь отдать страничку вместе с layout.
Код:
# protected/components/LController.php
class LController extends CController {
public function init() {
parent::init();
if (Yii::app()->request->getIsAjaxRequest()) {
$this->layout = '//layouts/clear';
}
}
}
# themes/classic/views/layouts/clear.php:
<?= $content ?>
# в контроллерах, где это нужно, наследуемся от LController:
class DefaultController extends LController {
….
}
Кеширование моделей без кода в каждой модели
Описание: Все мы (разработчики под Yii) в какой то степени используем findByPk и довольно сильно нагружаем базу запросами. Пусть они и по Primary Key, но сам факт запроса неприятен. Здесь я покажу как в Yii раз и навсегда засунуть модель в кеш без лишнего кода в самих моделях
Применение: Любое приложение, где требуется кеширование. При условии, что вся работа с базой делается только через ActiveRecord и изменения производятся только через методы save() и delete() экземпляра модели.
Код:
# protected/components/LActiveRecord.php
class LActiveRecord extends CActiveRecord {
public static function getByPk($pk){
$cache = Yii::app()->cache->get(‘activerecord.’.self::$clss.’.’.$pk);
if ($cache) $cache = unserialize($cache);
if ($cache) return $cache;
$clss = self::$class
$entity = $clss::model()->findByPk($pk);
Yii::app()->cache->set(‘activerecord.’.self::$clss.’.’.$pk,serialize($entity));
}
public function save($runValidation = true, $attributes = NULL){
$result = parent::save($runValidation, $attributes);
$self = get_class($this);
Yii::app()->cache->set(‘activerecord.’.$self::$clss.’.’.$pk, serialize($this));
return $result;
}
public function delete() {
$self = get_class($this);
$ret = parent::delete();
Yii::app()->cache->delete(‘activerecord.’.$self::$clss.’.’.$pk);
return $ret;
}
}
#все модели наследуем от LActiveRecord:
class User extends LActiveRecord {
public final static $clss = “User”; //прописываем имя класса для дальнейших обращений (были в классе-родителе)
...
}
#все выборки по Primary Key делаем так:
$user = User::getByPk(1);
Логирование логики
Описание: Иногда полезно писать логи действий с моделями — что, когда, с чем. Полезно и для отладки и для анализа популярности функционала и для анализа производительности.
Применение: почти везде где требуется вышесказанное
Код:
В указанный выше класс LActiveRecord добавляем следующее:
# в метод save (на место старого parent::save):
$ts = microtime(true);
if ($this->isNewRecord) {
$action = 'create/' . $this->id;
} else {
$action = 'update/' . $this->id;
}
$result = parent::save($runValidation, $attributes);
$ts = microtime(true)-$ts;
$this->logMe($action.’ time: ’.ts);
# в метод delete (на место старого parent::delete):
$ts = microtime(true);
$ret = parent::delete();
$ts = microtime(true)-$ts;
$this->logMe('delete/' . $this->id.’ time:’.$ts);
#и добавляем метод logMe:
private function logMe($action){
$model_log = Yii::app()->params['model_log_file'];
$table = $this->tableName();
$str = date('[Y-m-d H:i:s]') . ' [' . $table . '] ' . ' ' . $action . "rn";
error_log($str, 3, $model_log);
}
#в наши params добавляем параметр с именем файла для лога:
‘'model_log_file’ => ‘/var/www/application/logs/model.log’;
И снова все наши модели должны быть унаследованы от LActiveRecord.
Как обернуть всё в транзакции с минимальным кодом
Описание: Мне довольно часто нужно сделать так, чтобы все действия с базой были транзакционными, чтобы не писать код начала и конца транзакции везде, я вынес его в один метод, который yii вызывает автоматически.
Применение: везде где страшно получить кашу в базе
Код:
В указанный выше LController добавляем метод:
public function run($actionID) {
//начинается всё с копипасты из ядра yii:
if(($action=$this->createAction($actionID))!==null) {
if(($parent=$this->getModule())===null)
$parent=Yii::app();
if($parent->beforeControllerAction($this,$action)) {
//здесь начинается код отличный от кода yii, в нем мы запускаем транзакцию:
try {
$transaction=Yii::app()->db->beginTransaction();
$this->runActionWithFilters($action,$this->filters()); //запускаем экшин
$transaction->commit(); //всё ок
} catch (Exception $e){
$transaction->rollback(); //всё упало
throw $e;
}
//продолжается всё стандарным кодом yii:
$parent->afterControllerAction($this,$action);
}
} else
$this->missingAction($actionID);
}
#соответственно все контроллеры, где должны быть транзакции надо унаследовать от LController
Метод run в контроллере yii вызывает каждый раз, когда запускает контроллер, он реализован в классе CController. Нам повезло с тем, что он публичный и мы можем его переопределить. Из-за переопределения приходится частично возвращать в него код Yii, но в этом переопределении ничего плохого нет — этот код стабилен и уже работает.
Как сделать так, что бы не менять на каждом сервере с экземпляром приложения файл основного конфига. Упрощаем деплой
Описание: У нас есть задача — деплой кода на N серверов с помощью git pull. При этом у нас могут добавлятся строки в конфиг (с появлением новых модулей и прочего). Явно не хочется каждый раз руками выправлять адрес базы, memcached, других локальных настроек.
Применение: Везде где приложение развернуто в нескольких местах с разными настройками, хоть у 2х девелоперов и на продакшине.
Код:
#/protected/config/main.php
#в самом начале файла добавляем строку:
$local_config = require(dirname(__FILE__).’/local.php’);
#/protected/config/local.php:
return array(
‘db’ => array(
//копируем сюда строки с настройкой нашей БД, 'connectionString', 'username', ‘password’
),
‘memcache_servers’ => array(
array('host'=>'127.0.0.1', 'port'=>11211, 'weight'=>60), //ваши настройки memcached
)
)
# в /protected/config/main.php делаем изменения:
# 1. находим ключ ‘db’ и делаем его таким:
'db'=>$local_config[‘db’];
# 2. находим ключ ‘servers’ внутри массива ‘cache’ и делаем его таким:
'servers'=>$local_config[‘memcache_servers’]
потом добавляем в .gitignore файл local.php. Перед деплоем не забываем на каждом сервере создать этот файл и внести локальные настройки.
Это всё работает и используется?
Да все эти методы уже используются в проекте note-space.com уже больше года. Работает это на Yii 1.1.8, возможно еще более новых версиях, возможно нет — но это не мешает Вам адаптировать код под Вашу версию фреймворка.
P.S. Если Вы будете использовать этот код и возникнут проблемы, то можно смело обращаться за поддержкой прямо ко мне — всегда рад помочь и сделать код лучше.
Автор: piromanlynx