Yii — обмен опытом: модели

  • Tutorial

Материал в данной статье предназначен для разработчиков, которые уже имеют навыки работы с YII фреймворком. Опытным программистам материал может показаться неинтересным.
Статья написана для фреймворка версии YII 1.1.14. Под катом много текста с фрагментами кода.


Начнем


Как правило начинать построение моделей следует с проектирования таблиц исходя из поставленной задачи. Это увлекательное занятие мы оставим за рамками статьи, предположим что у нас уже есть требования к таблице. Модель строится уже по готовой таблице, поэтому начинать следует с миграции.

Миграции

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

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

Создание класса модели

Это занятие можно доверить генератору gii, он выполнит для Вас это с радостью. Однако после генерации модели ее, как правило, надо прилично доработать.

Первое что делаем — меняем класс от которого наследуется модель. Если у Вас в каталоге application.components еще не существует файл ActiveRecord.php, то самое время его создать, это будет базовый класс для всех наших моделей.

class ActiveRecord extends CActiveRecord
{
    const SCENARIO_CREATE = 'insert';
    const SCENARIO_UPDATE = 'update';
    const SCENARIO_SEARCH = "search";

    /** Return model ID
    *
    * @return integer
    */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Returns the static ActiveRecord model.
     * Please note that you should have this exact method in all your CActiveRecord descendants!
     * @param string $className active record class name.
     * @return ActiveRecord the static model class
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
}


В этот класс следует вынести все повторяющиеся методы из наследованных моделей для соблюдения DRY принципа. Не забываем оставлять комментарии к методам с указанием директивы return, это не только для PHPDoc но и для IDE.

Также для удобства написания кода в IDE созданы константы с названиями сценариев, в этом случае автодополнение помогает избежать опечаток, особенно когда сценариев становится сильно много (используется как правило в валидации, но и не только).

Базовые модели

Если модель вышла очень большая и содержит множество атрибутов такую модель удобно разбить на две. Базовую модель и основную, наследованную от базовой.
Например цепочка наследования может иметь вид:
User -> BaseUser -> ActiveRecord -> CActiveRecord
При этом в базовую модель выносятся методы, необходимые для работы YII (реляционные связи, правила проверок, метки атрибутов), а свои методы помещаем в основной класс модели.
В базовом и основном классе модели обязательно выполнить переопределение статического метода model() как показано ниже. В комментарии к методу в теге return нужно не забыть указать свой класс.

    /**
     * Returns the static BaseUser model
     * 
     * @param string $className active record class name.
     * @return BaseUser
     */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }


Перевод названий атрибутов

Хорошая практика — привыкать к разработке с учетом интернационального интерфейса, для этого используем метод Yii::t(). В качестве категории можно указывать класс модуля или компонента к которому принадлежит модель, к сожалению переопределять этот класс в модели нежелательно, так как консольный парсер при построении словарей не будет знать категории (однако это возможно, но тогда придется вызывать консольную команду с дополнительной конфигурацией).

        /**
         * @return array customized attribute labels (name=>label)
         */
        public function attributeLabels()
        {
            return array(
                'id' => Yii::t('BaseUser','ID'),
                'firstName' => Yii::t('BaseUser','First Name'),
                'middleName' => Yii::t('BaseUser','Middle Name'),
                'lastName' => Yii::t('BaseUser','Last Name'),
                'phone' => Yii::t('BaseUser','Phone Number'),
                'email' => Yii::t('BaseUser','E-mail'),
                // and more...
            );
        }


Аналогично методу attributeLabels() нужно создавать и свои, например для полей статуса statusLabels. Это пригодится при построении выпадающих списков на форме.

Константы в моделях

Константы необходимо использовать для полей статуса записи, флаговых полей (битовые маски), полей прогресса и т.д. А именно для тех полей по которым ведется индексация и значение там сжато и не очевидно.

    const STATUS_DRAFT  = 1;
    const STATUS_PUBLIC = 2;
    const STATUS_TRASH  = 3;

    const FLAG_HAS_CAR = 1;
    const FLAG_HAS_HOUSE = 2;
    const FLAG_HAS_CREDIT = 4;
    const FLAG_HAS_DEPOSIT = 8;


Очевидно, что встречая такие константы в исходниках они нам сразу говорят с чем мы имеем дело.

Методы доступа к данным

При разработке моделей, необходимо использовать геттеры и сеттеры и не использовать чтение атрибутов и присвоение им значений напрямую. Это даст разработчикам больший контроль над расширением проекта. Сеттер может делать тихую фильтрацию при установке значения, или приведение типа данных, а геттер может форматировать выходные данные в удобном формате. Сеттер должен только присваивать значение атрибуту, но не должен делать сохранение модели. Если сеттер будет возвращать $this, то таким образом можно строить текучий интерфейс для модели, для геттера это естественно неприменимо.

    /**
     * Set user status
     *
     * @param int $status
     * @return \User
     */
    public function setStatus($status){
        $this->status = intval($status);
        return $this;
    }

    /**
     * Return First Name
     *
     * @return string  
     */
    public function getFirstName()
    {
        return (string)$this->upperFirst($this->firstName);
    }


Мелкие операции

Иногда над моделью производятся мелкие операции над небольшим количеством атрибутов, такие методы можно начинать с префикса make и такие методы могут выполнять сохранение модели внутри себя, а также выбрасывать события для навешивания обработчиков.
    /**
     * Make user active
     *
     * @return bool
     */
    public function makeActive(){
       if($this->hasEventHandler('onBeforeCount')){
            $this->onBeforeCount(new CEvent($this));
       }

        $result = $this->setStatus(self::STATUS_ACTIVE)
             ->save();
        
        if($this->hasEventHandler('onAfterMakeActive')){
			$this->onAfterMakeActive(new CEvent($this));
        }
        return $result;
    }



На этом пожалуй прервусь, чтобы узнать мнение аудитории.

UPD: Внес правки от mitaichik и Fesor.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Мне продолжать статьи обмена опытом по разработке на YII?
Поделиться публикацией
Комментарии 46
    +4
    CActiveRecord::attributeLabels()
    

    выполняется при каждому обращению к названию атрибута, если у Вас представлении форма из 10 атрибутов, то этот метод выполнится соответствующее число раз.

    Я к тому чтоб Вы осторожнее относились к рекомендациям по этому поводу.

    Тем более что перевод это совсем не дело Model, а скорее View слоя.

    ЗЫ: Не до конца прочитал, уже ночь, может еще завтра будут комментарии. Спокойной ночи.
      +4
      /**
       * Благодаря этому методу можно не определять его явно в каждой модели-наследнике.
       * Спасибо PHP 5.3 с его get_called_class()
       */
      public static function model ($className = __CLASS__) {
      
          return parent::model(get_called_class());
      
      }
      


      А в классах-наследниках останется только добавить строку в PHPDoc для автодополнение в IDE:
       * @method static {model_name} model() model(string $className = __CLASS__)
      
        0
        О, model() в PHPDoc получился лишний.
          0
          Да, вы правы, я сам использую такой метод. Но при подходе, который указал автор и который показан в примерах на оф сайте данный метод имеет смысл переопределять только в конечном классе.

          Кстати, не объясните более подробно, какие именно надо прописать PHPDoc что бы класс, возвращаемый model(), определялся корректно. Я что то в ваш пример не понял.
            +1
            вместо {model_name} надо прописать собственно сам класс.
              0
              понял, теперь разобрался. спасибою
          +1
          Извиняюсь, не туда, отвечал на habrahabr.ru/post/212681/#comment_7313825
            +3
            Метод в таком виде недопустим, т.к. перестает работать код
            $post = ActiveRecord::model('Post')->find();
            

            более гибкий подход
            public static function model ($className = null) {
            
                return parent::model($className ?: get_called_class());
            
            }
            
              +2
              Я использую такой подход:

              $models = Post::model()->find();
              


              На мой взгляд так читабельность выше.
          +1
          Из своей практики, переопределяю метод getAttributeLabel() и уже там подставляю Yii::t()
            +3
            Скажите, а зачем вы переопределяете метод model() в классе ActiveRecord? Его все равно необходимо переопределить в каждом наследнике. Можетэтот класс стоит вообще объявить как абстрактный?

            И если вам не трудно, пишите чуть более сжато. Я понимаю, тема для новичков, но статья вышла немаленькая, а разобранно не так много. Лучше пусть поконкретнее, но о большем рассказать.

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

            Спасибо за статью.
            +1
            Когда только начинал работать с Yii тоже создавал наследника CActiveRecord и от него уже наследовал все другие модели. Да и вообще любил порасширять фреймворк везде где только можно через наследование. Потом понял что слишком увлекся, код приложения стал несколько отличается от типичного на Yii, причем не всегда в лучшую сторону. Обычно Behavior является более предпочтительным решением.
              +1
              На самом деле очень полезно наследовать свои классы не от фреймворка, а от своих прослоек. Не все, конечно, но большинство.

              У меня к примеру есть для таких классов отдельная директория. в том же ActiveRecord переопределены магические методы, которые позволяют обращаться к вот_таки_полям таблицы в горбатомСтиле.
              В классе Controller объявлены методы для форматированной передачи данных на клиент при ajax запросах и методы для автоматического подключения assets файлов.

              Так же обычно такие классы имеют абистрактные методы, чем задают основу архитектуры.
              +1
              А зачем метод model() переопределять в каждом наследнике? Я так понимаю это наследние php 5.2, где не было late static binding?
                0
                Да, именно так. Для 5.3+ можно переопределить model в базовом классе и больше не париться.
                  0
                  К сожалению 5.2 еще стоит на серверах у клиентов (иногда там CentOS), а полномочий на обновление нам не дают, так что этот подход рекомендован разработчиками Yii и работает для обратной совместимости. Также у новичков с этим могут возникнуть проблемы при разворачивании.
                    0
                    Соболезную. Но все же большая часть цивилизованного мира уже на 5.4+
                0
                Использование сеттеров в моделях конечно хорошо, но, стоит не забывать что populateRecord про них ничего не знает
                  0
                  populateRecord и не должен знать ничего о них. populateRecord нужен для данных из таблицы, без дополнительных манипуляций над ними. геттеры и сеттеры, наоборот нужны для дополнительными манипуляциями над данными из таблицы или виртуальными атрибутами.
                  0
                  Некоторые вещи можно автоматизировать используя giix extension. Например сразу будет создаваться базовая модель и наследующая ее модель в которой можно дописывать свои методы.
                    0
                    Раз уж интересуетесь мнением общественности, выскажу пожелание. Напишите серию статей для новичков, типа «что такое Yii», «что такое MVC», как это все работает, как работать с базами данных и т.д. Обязательно все с примерами, но не как здесь. Не надо постить куски кода с которыми не понятно что делать. Пример должен быть цельным, т.е. вот код модели, вот контроллер и вьюха, копипастю это все себе в проект и вижу результат. Чем подробнее все разжуете (вплоть до того в какой папке и с каким названием файл создать) тем лучше.
                    Я только на этой неделе начал разбираться с yii обрадовался увидев эту статью, однако совершенно ничего здесь не понял…

                    P.S. Я в курсе, что эта статья для бывалых, я просто высказал пожелание :)
                    0
                    Предположим, что мы поддались соблазну и в одной миграции создали таблицу и индексы в ней. Тогда при опечатке в функции создания индекса миграция будет прервана выброшенным прерыванием, и повторно запустить миграцию не получится так как таблица уже создана. Тут если есть возможность следует либо использовать механизм транзакций, либо делать множество мелких миграций.

                    safeUp/safeDown же. Обрамляет миграцию в транзакцию.
                      +3
                      транзакции работают только для CRUD операций, при изменении схемы базы данных вам safeUp/safeDown не помогут.
                    0
                    Иногда над моделью производятся мелкие операции над небольшим количеством атрибутов, такие методы можно начинать с префикса make и такие методы могут выполнять сохранение модели внутри себя

                    Не уверен, что хорошая идея мешать в одном методе изменение состояния и его сохранение.
                      0
                      public function setStatus(int $status)
                      

                      Разве скалярные типы можно использовать для контроля типов аргументов?
                        0
                        увы нет. насколько я помню почти все предложения в rfc изьяты/отклонены.
                          0
                          В этом случае Yii выбросит Recoverable error
                            +2
                            Вы может веткой ошиблись? Yii ничего не может поделать с отсутствием тайп-хинтинга для скалярных типов.
                              –1
                              нет не ошибся
                              Вот такой код:
                              function geta(int $a){
                                  return $a;
                              };
                              
                              echo geta("test");
                              


                              при вызове без YII выводит такое сообщение
                              PHP Catchable fatal error: Argument 1 passed to geta() must be an instance of int, string given, called in /home/my/in.php on line 7 and defined in /home/my/in.php on line 3

                              Если этот код будет внутри приложения YII то он будет показано сообщение Recoverable error

                              PHP 5.3.10-1ubuntu3.9 with Suhosin-Patch (cli) (built: Dec 12 2013 04:27:25) Copyright (c) 1997-2012 The PHP Group Zend Engine v2.3.0, Copyright (c) 1998-2012 Zend Technologies
                                0
                                фраза instance of int означает что у вас должен быть класс int, экземпляр которого и должен передаваться.
                                class int{
                                
                                }
                                
                                function foo(int $a) {
                                }
                                
                                foo('test');// выбросит исключение
                                foo(42); // выбросит исключение
                                foo(new int()); // отработает
                                
                                  0
                                  вы правы, я исправлю в записи.
                        –1
                        Я бы добавил то, что надо в названии модели добавлять слово Model, как делается это с контроллерами, на пример UserModel.php
                        Так не будем путать модели с другими всякими классами. Получается очень удобно.
                          +1
                          С какими другими? Для других добавляем суффикс, обозначающий их роль в приложении, а классі модели моделируют предметную область, и к приложению как бі не относятся.
                            0
                            Компоненты, классы расширений и т.д… что может быть мало классов в приложении, что за дурацкий вопрос…
                            жаль не могу минусовать, а то как-то не справедливо получается…
                              0
                              Ну так тем, которые выполняют служебную роль и присваиваем суффикс, отражающий эту роль. А модель предметной области — это основное в приложении, незачем его ставить наравне со всем остальным.
                              P.S. Про справедливость не понял. Я вас не минусовал, если вы на это намекаете.
                                0
                                Не пойму о чем вы… то пишите, что к приложению не относятся, то пишите, что модель это основное в приложении… определитесь, пожалуйста…
                                У нас есть классы приложения команд, контроллеров, у них мы пишем Command, Controller, речь о том, что модели тоже нужно обозначать Model, так гораздо удобней
                                PS, да, подумал что минусовали вы, извиняюсь
                                  +1
                                  Модель относится к уровню бизнес-логики, а не к уровню логики приложения или отображения. Бизнес-логика основное в приложение, без нёё оно точно никому не нужно. В общем на вкус и цвет, но мне удобнее что основными сущностями приложения оперируешь без суффиксов, показывающих что они основные.
                          0
                          Такое писать в базовом классе не правильно:
                              public function getId()
                              {
                                  return $this->id;
                              }
                          

                          Далеко не все таблицы имеет поле id и могут иметь составной PK.

                          Так же мне кажется довольно сомнительным использовать для всего геттеры и сеттеры в описанном в статье виде, ибо setAttributes эти геттеры и сеттеры использовать не будет. Что бы этот подход работал нужно использовать название поле например, начинающиеся с символа '_' — а это тот ещё изврат.

                          Так же лишние вызовы методов — это увеличение времени работы приложения, и поэтому такие вызовы должны быть оправданы.
                            0
                            Оправдание простое — абстрагирование от деталей реализации, инкапсуляция, контроль доступа. Даже если в начале ничего не нужно, то потом может понадобится. Имхо, это для использования паблик-свойств нужно обоснование типа «поднимет производительность всего приложения на 10%», а не наоборот.

                              0
                              Ещё раз, это геттеры и сеттеры не будут работать во всяких setAttributes и прочих местах, например при использовании хелпера CHtml::activeTextFiled() и т.д. И получается не понятно, какой прок от них.
                              +1
                              лишние вызовы методов

                              использование магических методов __get/__set намного дороже обходится, и все равно это капля в море. Сомнительно что это может быть узким местом производительности.
                                0
                                Именно поэтому я написал это в конце начиная со слов «Так же», т.е. это не имеет конечно сильное значение.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое