Pull to refresh

Yii обмен опытом: модели (окончание)

Reading time 5 min
Views 27K

Продолжение предыдущего поста

Область ответственности модели и ее место в проекте

Модели предназначены только для оперирования данными (поиск, сохранение, удаление), а также связанными статическими файлами, так как они тоже неотъемлемая часть структуры данных (фотография, видеоролик). Естественно что при удалении записи, связанные с ней медиафайлы должны удаляться, для этого удобно использовать 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);
     }

}




Пример виджета
class UserAvatar extends CWidget
{
    public $emptyPhotoUrl = "/static/images/no-photo.png";
    public $model;
    public $htmlOptions;
    
    public function run(){
         $avatar = $this->emptyPhotoUrl;
         if($this->model->hasAvatar())
         {
                $avatar = $this->model->getAvatarUrl();
         }
         echo CHtml::image($avatar,$this->model->getUserName(),$this->htmlOptions);
    }
}




Виджет и хелпер удобно использовать в представлениях, хелпер удобен при использовании внутри виджета CGridView.

Журналирование моделей

Для отладки моделей, особенно для отлова трудновоспроизводимой ошибки иногда полезно иметь возможность узнать хронологию событий.
Для этого в модель можно добавить методы журналирования, и на все операции производимые с данными вести журнал (лог-файл). Реализация этой несложной механики может быть любой, однако полезно вспомнить про компонент CLogRoute и настроить один из маршрутов для своей модели, лог можно вести как в базу (осторожно, снижает производительность) так и в простые файлы. Также в лог полезно писать данные пользователя от имени которого совершается операция, это упростит разбор полетов.
Tags:
Hubs:
+24
Comments 48
Comments Comments 48

Articles