Доработка модели 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)`.