Продолжение предыдущего поста
Область ответственности модели и ее место в проекте
Модели предназначены только для оперирования данными (поиск, сохранение, удаление), а также связанными статическими файлами, так как они тоже неотъемлемая часть структуры данных (фотография, видеоролик). Естественно что при удалении записи, связанные с ней медиафайлы должны удаляться, для этого удобно использовать beforeDelete() или afterDelete().
Несложную бизнес-логику можно реализовать в модели, для сложной — лучше использовать сервисную прослойку (Service Layer) так вы избавите модель от массы зависимостей и не будете раздувать из нее божественный объект.
В моделях не должны появляться:
- HTML-верстка, CSS, javascript
- Суперглобальные массивы ($_GET, ...)
- Глобальные переменные
- Проверка прав доступа (этот момент, возможно, спорный, практика показывает что контроль доступа к операции нужно проводить в контроллере)
- Текстовые строки не обернутые в Yii::t()
Модели должны иметь как можно меньше зависимостей от других компонентов, так мы сможем сделать ее переносимой, и использовать в других проектах. При этом компоненты от которых модель зависит должны устанавливаться сеттерами (например User::setEmailComponent('email')). Назначать компоненты можно и в User::init() но при этом обязательно сделать проверку на его наличие. Если компонент — второстепенный и недоступен — модель обязана работать без шума, однако если компонент первостепенный, то на стадии инициализации следует выбросить прерывание.
Эти несложные правила позволят поддерживать ваш код командой программистов, и достаточно гибко расширять функционал.
Проверка состояния статуса
Обычно проверку делают в разных местах, как в контроллерах так и в представлениях и проверка часто выглядит так:
// простой вариант
if($model->status == 1){ ... }
// более продвинутый вариант
if($model->getStatus() === Model::STATUS_ACTIVE){ ... }
// страшный вариант
if($model->status !== 1 && $model->status !== 2 && $model->status !== 4){ ... } //да-да-да бывает и такое
Однако если создать метод isActive() который делает эту проверку, то код станет более контролируемым, особенно в условиях часто меняющегося ТЗ. Также иногда полезным окажется создание метода, в котором описана проверка на возможность перехода в иное состояние статус. Например: если запись имеет статус — «Спам», то перевести ее в статус «Активна» нельзя, однако ее можно перевести в статус «На проверку». Всю эту логику желательно размещать также в моделях. (Это может быть полезно при построении workflow систем)
Использование флаговых полей
Для экономии размера таблицы (числа колонок) и для того чтобы иметь более гибкий функционал расширения модели используют поля с битовыми флагами.
Такие поля как правило представлены в базе типом integer (но необязательно) и способны разместить довольно большое число флагов. Флаговые поля я уже затронул в предыдущей статье. Теперь покажу практическое применение. (Детально битовые маски описаны в этой статье).
Пример: Есть модель пользователя в которой для пользователя можно назначить несколько одновременных флагов — «лучший автор», «подтвержден телефон», «подтвержден email», «оставил запрос на пост модератора»
public $flags = 0;
const FLAG_CONFIRM_EMAIL = 1; // 00000001
const FLAG_CONFIRM_PHONE = 2; // 00000010
const FLAG_BEST_AUTHOR = 4; // 00000100
const FLAG_BECOME_MODERATOR = 8; // 00001000
/**
* Устанавливает флаг
* @param integer $flag
* @return User
*/
public function setFlag($flag)
{
$this->flag += intval($flag);
return $this;
}
/**
* Снимает флаг
* @param integer $flag
* @return User
*/
public function unsetFlag($flag)
{
$this->flag -= intval($flag);
return $this;
}
/**
* Проверяет наличие флага
* @param integer $flag
* @return boolean
*/
public function hasFlag($flag)
{
return ($this->flags & intval($flag));
}
/**
* Возвращает человеческие названия флагов
* @return array
*/
public function getFlagsLabels(){
return array(
self::FLAG_CONFIRM_EMAIL => Yii::t('User',"Email подтвержден"),
self::FLAG_CONFIRM_PHONE => Yii::t('User',"Телефон подтвержден"),
self::FLAG_BEST_AUTHOR => Yii::t('User',"Лучший автор"),
self::FLAG_BECOME_MODERATOR => Yii::t('User',"Заявка на пост модератора"),
);
}
/**
* Используется для выборки по флагам
* @param integer $flag
* @return User
*/
public function withFlags($flag=0)
{
$this->getDbCriteria()->mergeWith(array(
'order'=>'flags & :flag',
'params' => array(':flag' => $flag),
));
return $this;
}
В запросе MySQL проверка примет вид:
SELECT * FROM `User` WHERE STATUS & 6
В примере создана именованная группа условий это позволит производить поиск как показано ниже:
User::model()->withFlags(User::FLAG_CONFIRM_EMAIL)->findAll();
Реляционные связи
Реляционные связи хорошо описаны в официальной документации.
На практике возникают такие нюансы.
- Если реляционная связь построена правильно с использованием FK то все должно работать четко, так как целостность данных обеспечена БД.
- Если реляционная связь построена только средствами YII ( и нет возможности использовать FK), в этом случае не следует использовать прямой доступ к переменной обозначающей связь, вместо этого следует использовать CActiveRecord::getRelated().
- Проверку правильности связей (правильные ID) необходимо контролировать при валидации и в beforeSave() обработчике.
- Удаление связанных данных необходимо реализовать в beforeDelete() обработчике.
Представления для атрибутов
В коде моделей иногда встречается HTML-код — ему там не место. Если есть необходимость вывести HTML-форматированный вид атрибута для этого следует использовать или виджет или создать хелпер для модели (класс со статическими методами). примеры показаны ниже.
class UserHelper{
/**
* Возвращает гиперссылку на профиль пользователя
* @return string
*/
public static function getProfileLink(User $model, $htmlOptions = array())
{
return CHtml::link($model->getUserName(),$model->getProfileUrl(),$htmlOptions);
}
}
{
public $emptyPhotoUrl = "/static/images/no-photo.png";
public $model;
public $htmlOptions;
public function run(){
$avatar = $this->emptyPhotoUrl;
if($model->hasAvatar())
{
$avatar = $model->getAvatarUrl();
}
echo CHtml::image($avatar,$model->getUserName(),$this->htmlOptions);
}
}
Виджет и хелпер удобно использовать в представлениях, хелпер удобен при использовании внутри виджета CGridView.
Журналирование моделей
Для отладки моделей, особенно для отлова трудновоспроизводимой ошибки иногда полезно иметь возможность узнать хронологию событий.
Для этого в модель можно добавить методы журналирования, и на все операции производимые с данными вести журнал (лог-файл). Реализация этой несложной механики может быть любой, однако полезно вспомнить про компонент CLogRoute и настроить один из маршрутов для своей модели, лог можно вести как в базу (осторожно, снижает производительность) так и в простые файлы. Также в лог полезно писать данные пользователя от имени которого совершается операция, это упростит разбор полетов.
Автор: Geos