Доработка модели Post ===================== Модель `Post`, сгенерированная при помощи `Gii`, нуждается в следующих изменениях: - метод `rules()`: задаёт правила валидации атрибутов модели; - метод `relations()`: задаёт связи с другими объектами. > Info|Информация: [Модель](/doc/guide/ru/basics.model) состоит из набора атрибутов, каждый из которых ассоциируется с соответствующим полем в таблице БД. Атрибуты могут быть описаны явно как переменные класса, либо использоваться без какого-либо описания. Изменение метода `rules()` -------------------------- В первую очередь необходимо определить правила валидации, которые позволят убедиться в том, что данные, введённые пользователем, корректны до их сохранения в БД. К примеру, атрибут `status` модели `Post` должен быть целым числом, равным 1, 2 или 3. Консоль `Gii` генерирует правила валидации для каждой модели. При этом используется структура БД, поэтому некоторые правила могут оказаться неточными. Основываясь на анализе требований, изменим метод `rules()` следующим образом: ~~~ [php] public function rules() { return array( array('title, content, status', 'required'), array('title', 'length', 'max'=>128), array('status', 'in', 'range'=>array(1,2,3)), array('tags', 'match', 'pattern'=>'/^[\w\s,]+$/', 'message'=>'В тегах можно использовать только буквы.'), array('tags', 'normalizeTags'), array('title, status', 'safe', 'on'=>'search'), ); } ~~~ В коде выше мы определили, что атрибуты `title`, `content` и `status` являются обязательными для заполнения. Длина `title` не должна превышать 128 символов. Значение `status` может быть 1 (черновик), 2 (опубликовано) или 3 (в архиве). В `tags` могут содержаться только буквы, запятые и пробелы. Вводимые пользователем теги дополнительно нормализуются при помощи `normalizeTags`. Это делается для того, чтобы теги были уникальными и правильно разделялись запятыми. Последнее правило используется поиском и будет описано позже. Валидаторы, такие как `required`, `length`, `in` и `match` являются стандартными валидаторами Yii. Валидатор `normalizeTags` использует определённый в классе `Post` метод. За дополнительной информацией о том, как описывать правила валидации вы можете обратиться к [полному руководству](/doc/guide/ru/form.model#declaring-validation-rules). ~~~ [php] public function normalizeTags($attribute,$params) { $this->tags=Tag::array2string(array_unique(Tag::string2array($this->tags))); } ~~~ где `array2string` и `string2array` - новые методы, которые мы должны определить в классе модели `Tag`: ~~~ [php] public static function string2array($tags) { return preg_split('/\s*,\s*/',trim($tags),-1,PREG_SPLIT_NO_EMPTY); } public static function array2string($tags) { return implode(', ',$tags); } ~~~ Правила, описанные в методе `rules()`, вызываются по очереди при вызове методов модели [validate()|CModel::validate] или [save()|CActiveRecord::save]. > Note|Примечание: Важно помнить, что атрибуты, описываемые в `rules()` > должны вводиться пользователем. Другие атрибуты модели `Post`, такие как `id` или `create_time`, заполняемые в коде или напрямую в БД не должны присутствовать в `rules()`. Подробнее это описано в разделе [Безопасное присваивание значений атрибутам](/doc/guide/ru/form.model#securing-attribute-assignments). После того, как мы сделали описанные изменения, мы можем зайти на страницу создания записи и проверить, что новые правила валидации работают. Изменение метода `relations()` ------------------------------ Далее укажем в методе `relations()` связанные с записью объекты. После этого мы сможем использовать [реляционную ActiveRecord (RAR)](/doc/guide/ru/database.arr) для получения связанных с записью данных, таких как информацию об авторе и комментарии. Сложные SQL запросы с JOIN в этом случае не потребуются. Определим метод `relations()`: ~~~ [php] public function relations() { return array( 'author' => array(self::BELONGS_TO, 'User', 'author_id'), 'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'condition'=>'comments.status='.Comment::STATUS_APPROVED, 'order'=>'comments.create_time DESC'), 'commentCount' => array(self::STAT, 'Comment', 'post_id', 'condition'=>'status='.Comment::STATUS_APPROVED), ); } ~~~ Также, в классе модели `Comment` мы описываем две константы, которые используются в приведённом выше методе: ~~~ [php] class Comment extends CActiveRecord { const STATUS_PENDING=1; const STATUS_APPROVED=2; ...... } ~~~ Связи, описанные в методе `relations()`, означают следующее: * Запись принадлежит автору(`User`), связь с которым устанавливается на основе поля записи `author_id`; * Запись может содержать много комментариев(`Comment`), связь с которыми устанавливается на основе поля комментария `post_id`. Комментарии сортируются по времени их создания. * Связь `commentCount` является особенной так как возвращает результат агрегации, то есть число комментариев записи. Задав описанные выше связи, мы можем получить информацию об авторе и комментариях к записи следующим образом: ~~~ [php] $author=$post->author; echo $author->username; $comments=$post->comments; foreach($comments as $comment) echo $comment->content; ~~~ Более подробно использование и определение связей описано в [полном руководстве](/doc/guide/ru/database.arr). Добавляем свойство `url` ------------------------ Каждой записи соответствует уникальный URL. Вместо повсеместного вызова [CWebApplication::createUrl] для формирования этого URL, мы можем добавить свойство `url` модели `Post` и повторно использовать код для генерации URL. Позже мы опишем, как получить красивые URL. Использование свойства модели позволит реализовать это максимально удобно. Для того, чтобы добавить свойство `url`, мы добавляем геттер в класс `Post`: ~~~ [php] class Post extends CActiveRecord { public function getUrl() { return Yii::app()->createUrl('post/view', array( 'id'=>$this->id, 'title'=>$this->title, )); } } ~~~ В дополнение к ID записи, в URL через GET-параметр мы выводим заголовок. Делается это главным образом для оптимизации под поисковые алгоритмы (SEO). Подробнее это будет описано в разделе «[человекопонятные URL](/doc/blog/final.url)». Так как [CComponent] является предком класса `Post`, геттер `getUrl()` позволяет нам писать код вроде `$post->url`. При обращении к `$post->url` будет вызван геттер и мы получим результат его выполнения. Более подробно это описано [в полном руководстве](/doc/guide/ru/basics.component). Текстовое представление для статуса ----------------------------------- Так как статус записи хранится в БД в виде числа, нам необходимо получить его текстовое представление для отображения пользователям. Для больших систем такое требование является довольно типичным. Для хранения связей между целыми числами и их текстовым представлением, необходимым другим объектам данных, мы используем таблицу `tbl_lookup`. Для более удобного получения текстовых данных изменим модель `Lookup` следующим образом: ~~~ [php] class Lookup extends CActiveRecord { … private static $_items=array(); public static function items($type) { if(!isset(self::$_items[$type])) self::loadItems($type); return self::$_items[$type]; } public static function item($type,$code) { if(!isset(self::$_items[$type])) self::loadItems($type); return isset(self::$_items[$type][$code]) ? self::$_items[$type][$code] : false; } private static function loadItems($type) { self::$_items[$type]=array(); $models=self::model()->findAll(array( 'condition'=>'type=:type', 'params'=>array(':type'=>$type), 'order'=>'position', )); foreach($models as $model) self::$_items[$type][$model->code]=$model->name; } } ~~~ Мы добавили два статичных метода: `Lookup::items()` и `Lookup::item()`. Первый возвращает список строк для заданного типа данных, второй — конкретную строку для заданного типа данных и значения. В базе данных блога есть два типа данных: `PostStatus` и `CommentStatus`. Первый содержит возможные статусы записи, второй — статусы комментария. Для того, чтобы сделать код более читаемым мы описываем константы, соответствующие целочисленным значениям статуса. Эти константы необходимо использовать в коде вместо соответствующих им целых значений. ~~~ [php] class Post extends CActiveRecord { const STATUS_DRAFT=1; const STATUS_PUBLISHED=2; const STATUS_ARCHIVED=3; ...... } ~~~ Следовательно, для получения списка всех возможных статусов записи (массива строк с ключами, равными соответствующим им значениям), мы можем воспользоваться кодом `Lookup::items('PostStatus')`. А для получения конкретной строки — кодом `Lookup::item('PostStatus', Post::STATUS_PUBLISHED)`.