ORM

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

ORM в Kohana

Реализация ORM в Kohana позволяет представить запись из базы данных в виде экземпляра объекта, который имеет различные методы взаимодействия, такие как изменение, сохранение, удаление и некоторые другие. Помимо того, с помощью незагруженного (пустого) экземпляра объекта может производится поиск (загрузка) одной или нескольких записей. Каждый объект представляется в виде PHP-класса, унаследованного от класса ORM и должен располагаться в папке classes/model/.

Конфигурация

ORM не имеет общих для себя настроек, и нуждается только в подключении себя из папки модулей, более подробно об этом написано в руководстве по настройке. Также, не забудьте подключить модуль Database, он является обязательным для использования ORM.

Стандарты именования

Рекомендуется соблюдать следующие стандарты именования таблиц, полей и классов при работе с ORM:

  • Название модели должно быть в единственном числе. Например, Model_Post, Model_Category, Model_User_Token. Однако, существуют некоторые исключения, связанные с особенностью английского языка, например, Model_News.
  • Название таблицы должно быть во множественном числе. К примеру, posts, categories, users_tokens.
  • Название полей, предназначенных для связи с другими таблицами, должны быть в единственном числе и оканчиваться на _id. Например, не post.category, а post.category_id, не token.user, а token.user_id, не product.default_photo, а product.default_photo_id.

Базовое использование

Базовое использование подразумевает простейшие действия с объектами — чтение, изменение, запись.

Определение модели

Чтобы определить модель ORM, создайте в папке classes/module класс, унаследованный от ORM:

class Model_City extends ORM { }

На этом, формально, создание модели можно окончить. Остальные настройки (название таблицы, первичный ключ, названия столбцов и т.д. Kohana сгенерирует сама, исходя из названия модели (в данном случае, это — city). Тем не менее, мы рассмотрим их далее.

Название таблицы

Название таблицы хранится во внутреннем параметре объекта _table_name:

protected $_table_name = 'custom_table';

Первичный ключ

Названия поля первичного ключа находится в параметре _primary_key и по умолчанию оно установлено в id:

protected $_primary_key = 'custom_pk';

Первичные ключи, состоящие из нескольких полей, в данный момент не поддерживаются. Их реализация должна появиться с выходом Kohana 3.3

База данных

По умолчанию используется стандартная БД, но это можно поменять в параметре _db_group:

protected $_db_group = 'customdb';

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

Создание модели происходит с помощью метода ORM::factory($model), где $model — название модели, либо прямым созданием экземпляра объекта:

$city = ORM::factory('city');
$city = new Model_City;

После выполнения кода будет создана пустая модель сity.

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

ORM::factory('city')->method1()->method2()->...

Доступ к модели

После создания модели, мы можем обратиться к ее публичным параметрам как к столбцам в таблице:

$city = ORM::factory('city');
echo $city->name;

Также, мы можем преобразовать объект в массив:

$city = ORM::factory('city')->as_array();
echo $city['name'];

Загрузка модели

Чтобы загрузить модель, зная ее первичный ключ, можно использовать тот же ORM::factory($model, $id), где $model — название модели, а $id — значение первичного ключа, либо через стандартный конструктор классов:

$product = ORM::factory('product_photo', 4);
$product = new Model_Product_Photo(13);

Если первичный ключ неизвестен, и нужно подобрать запись, соответствующую некоторым условиям, можно использовать метод find():

$city = ORM::factory('city')->find();

В данном случае, условий нет, и будет возвращена первая запись из таблицы. Условия ставятся напрямую к таблице используя Kohana Query Builder с помощью методов where, having, order_by, join, select, group_by, limit, offset и т.д. Пример использования:

$city = ORM::factory('category')
	->where('name', '=', 'Dresses')
	->where('active', '=', TRUE)
	->limit(20)
	->order_by('posted', 'DESC')
	->find();

Также, условия можно передавать в метод ORM::factory или конструктор через массив:

$city = ORM::factory('category', array('name' => 'Dresses', 'active' => TRUE));

В этом случае все переданные условия будут вставлены с помощью AND WHERE.

Если для заданного условия не нашлось ни единой записи, то будет возвращен пустой объект. Когда нужно проверить, загружен ли объект из базы или нет, нужно использовать метод loaded():

$category = ORM::factory('category', 10);
 
if( ! $category->loaded())
{
	throw new HTTP_Exception_404('Category not found');
}

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

Загрузка нескольких моделей

Практически всегда требуется загрузить несколько моделей, отвечающим условиям, для этого Kohana предоставляет метод find_all():

$dogs = ORM::factory('dog')
	->where('name', '=', 'Bob')
	->find_all();

В результате мы получим объект класса Database_Result, который будет содержать найденные записи в виде отдельных объектов, их можно обходить как массив:

$dogs = ORM::factory('dog')
	->where('home', '!=', NULL)
	->limit(15)
	->find_all();
 
foreach($dogs as $dog)
{
	echo $dog->name;
}

Более подробно об использовании Database_Result можно прочитать в руководстве по базам данных.

Изменение модели

Для изменения модели доступно несколько методов. Первый из них — прямое обращение к публичным параметрам модели:

$city->name = 'Moscow';

Также доступен метод set($column, $value), где $column — имя столбца, а $value — устанавливаемое значение, в основном используется для создания подобных конструкций:

$city = ORM::factory('city')
	->set('name', 'Moscow')
	->set('popularity', 20000000);

И для массовой загрузки значений доступен метод values($data, $excepted), где $data — массив, индексами которого являются названия столбцов, а элементами — их значения, и необязательный параметр $excepted — массив значений, которые будут взяты из массива, если параметр пустой, то будут взяты все значения:

$city = ORM::factory('country')->values(array(
	'name' => 'Russia',
	'popularity' => 140000000,
));

Второй параметр полезен для случаев, когда данные вставляются в модель сразу от пользователя (например, из $_POST):

$account = ORM::factory('account')
	->set('balance', 0);
	->values($_POST, array('name', 'country'));

В данном примере, из всех пользовательских данных в модель попадут только name и country, а balance, даже если и будет присутствовать в $_POST, все равно установится в 0.

Сохранение модели

После изменения модели, чаще всего, требуется ее сохранять. Для этого есть методы update(), который обновляет запись, create(), который создает запись в базе данных, и save(), который обновляет запись, если она уже существует, либо создает ее. Пример использования:

ORM::factory('user', 6)
	->set('balance', 60)
	->save();

Удаление модели

Для удаления существует единственный метод delete() — который производит удаление записи из базы данных и стирание объекта. пример:

$users = ORM::factory('user')
	->where('last_activity', '<', time() - Date::YEAR)
	->find_all();
 
foreach($users as $user)
{
	$user->delete();
}

Подсчет всех записей

Чтобы подсчитать количество всех записей в базе доступен метод count_all():

$active_users = ORM::factory('user')
	->where('last_activity', '>', time() - Date::MONTH)
	->count_all();

В результате будет сгенерирован следующий запрос:

SELECT COUNT(*) AS `records_found` FROM `users` AS `user` WHERE `last_activity` > 1323458064

Будьте аккуратны с условиями having, group_by и limit. Они могут неоднозначно повлиять на результат подсчета.

Связи

Связи в ORM позволяют строить связи между таблицами как вложенные параметры объектов. Например, существуют таблицы city (город) и country (страна). При должной настройке ORM-связей, вы сможете производить действия над моделью country напрямую из city:

$city = ORM::factory('city', 8);
echo $city->title.', '.$city->country->title;

Результатом может быть строка Mosow, Russia. Чтобы создать связь, необходимо, чтобы все связываемые таблицы имели объявление модели ORM.

Kohana предоставляет 4 типа связей для объектов:

  1. Один к одному (has one). Данный тип связей используется, когда для одной записи из одной таблицы имеется одна запись из другой. Пример реализации: у каждого автора из таблицы users есть 1 запись из таблицы passports.
  2. Много к одному (belongs to). Этот тип связи используется, когда для многих записей из одной таблицы имеется одна запись из другой. Пример: для каждой записи из таблицы posts имеется одна категория из таблицы categories. Другими словами, можно сказать, что запись (posts) принадлежит (англ. belongs to) к категории (categories).
  3. Один к многим (has many). Тип связи подразумевает, что у одной записи есть много записей из другой таблицы, причем принадлежащие только ей. Например, у одного города (cities) может быть несколько жителей (users). Тем не менее, один житель может принадлежать только к одному городу.
  4. Много ко многому (has many through). Связь используется, когда у многих записей из одной таблицы может быть много записей из другой. Например, у каждой записи в блоге (posts) существуют теги (tags). Причем, один и тот же тег может принадлежать нескольким записям одновременно. Для построения подобной связи используется вспомогательная таблица, название которой составляется из двух соединяемых таблиц. В этом случае, например, название может быть posts_tags.

Настройка связей производится с помощью внутренних (protected) параметров класса _has_one, _belongs_to и _has_many при определении модели. Параметры должны быть заданы в виде массива, где индексы — это названия связей, а элементами — настройки для связи, которые мы разберем ниже. Таким образом, мы можем настраивать неограниченное количество связей разного типа:

class Model_City extends ORM {
	protected $_belongs_to = array(
		'country' => array(...),
	);
 
	protected $_has_many = array(
		'users' => array(...),
		'houses' => array(...),
	);
}

Таким образом, приведенная в примере модель города city может принадлежать стране (country) и содержать несколько пользователей (users) и домов (houses). После составления модели, мы сможем обращаться к связям, как к параметрам объекта:

$city = ORM::factory('city')->where('name', '=', 'London')->find();
echo $city->country->name;
$users_list = $city->users;

Один к одному

Для создания этих типов связей используется параметр _has_one, для каждой связи доступны следующие параметры:

  • model — имя модели ORM связываемой таблицы.
  • foreign_key — название столбца в связываемой таблице, по которому будет проходить связь с первичным ключом первой таблицы. По умолчанию, это поле устанавливается в значение «имя первой таблицы в единственном числе + _id». Например, при связи таблицы users с passports, это поле по умолчанию установится в user_id.

Дальнейшая логика происходит следующим образом: при обращении к связываемому объекту, происходит запрос к базе данных и создается объект. При дальнейших обращениях к объекту, новых обращений к базе данных не происходит, а возвращается сохраненный объект.

Пример реализации:

class Model_User extends ORM {
	protected $_has_one = array(
		'passport' => array(
			'model' => 'passport',
			'foreign_key' => 'user_id',
		),
	);
}
class Model_Passport extends ORM {}
$user = ORM::factory('user', 63);
echo $user->passport->id; // 1
echo $user->passport->registration_date; // 2012-01-25

В данном случае, будет сгенерировано два sql запроса:

SELECT `user`.* FROM `users` AS `user` WHERE `user`.`id` = 63 LIMIT 1;
SELECT `passport`.* FROM `passports` AS `passport` WHERE `passport`.`user_id` = '63' LIMIT 1;

Много к одному

Настройки этих связей хранятся в параметре _belongs_to, каждый из элементов также имеет свои настройки:

  • model — имя модели ORM связываемой таблицы.
  • foreign_key — название столбца в первой таблице, по которому происходит связь с первичным ключом второй таблицы. По умолчанию, также генерируется имя, состоящее из «названия второй таблицы в единственном числе + суффикс _id». Например, для связи posts с categories установится значение category_id.

Дальнейшая логика происходит примерно также, как и в связи «один к одному». Рассмотрим сразу пример реализации:

class Model_Post extends ORM {
	protected $_belongs_to = array(
		'category' => array(
			'model' => 'category',
			'foreign_key' => 'category_id',
		),
	);
}
class Model_Category extends ORM {}
$post = ORM::factory('post', 2);
echo $post->category->name; // Auto
echo $post->category->id; // 4

После запуска этого примера, будет выполнены следующие sql-запросы:

SELECT `post`.* FROM `posts` AS `post` WHERE `post`.`id` = 2 LIMIT 1
SELECT `category`.* FROM `categories` AS `category` WHERE `category`.`id` = '4' LIMIT 1

Один ко многим

Для хранения настроек этого типа связи используется параметр _has_many. Доступные настройки при этом следующие:

  • model — имя модели ORM связываемой таблицы.
  • foreign_key — название столбца во второй таблице, по которому будет идти связь с первичным ключом первого. По умолчанию, оно устанавливается в «название первой таблицы в единственном числе + суффикс _id». Примером для связи cities с users будет значение city_id.

Последующая логика несколько отличается от логики предыдущих связей. После обращения к связываемому объекту, запроса к базе не происходит, а возвращается пустой экземпляр связываемой модели с записанными условиями для выборки. То есть, мы можем либо дополнить условия, либо сразу вызвать метод find_all() для нахождения всех связанных моделей. Рассмотрим на примере:

class Model_City extends ORM {
	protected $_has_many = array(
		'users' => array(
			'model' => 'user',
			'foreign_key' => 'city_id',
		),
	);
}
class Model_User extends ORM {}
$city = ORM::factory('city', 1);
$users = $city->users->find_all();
foreach($users as $user)
{
	echo 'user '.$user->id.' ';
} // user 63

После выполнения этого кода, будет сгенерированы следующие sql-запросы:

SELECT `city`.* FROM `cities` AS `city` WHERE `city`.`id` = 1 LIMIT 1
SELECT `user`.* FROM `users` AS `user` WHERE `user`.`city_id` = '1'

Многие ко многим

Для создания данного типа связи используется параметр _has_many, как и для связи «Один ко многим», с единственным отличием, что настроек больше:

  • model — имя модели ORM связываемой таблицы.
  • through — имя промежуточной таблицы, через которую будет осуществляться связь между первичными ключами двух моделей. Является обязательной для указания, иначе выполнится связь «Один ко многим». Принято имена таблиц указывать в стиле «название первой таблицы во множественном числе _ название второй таблицы во множественном числе». В нашем примере для связи между posts и tags это будет posts_tags.
  • foreign_key — имя столбца в промежуточной таблице, который будет выполнять связь с первичным ключом первой таблицы. Если параметр не указать, он будет составлен из «названия первой таблицы в единственном числе + суффикс _id», например, post_id.
  • far_key — имя столбца в промежуточной таблице, который будет выполнять связь с первичным ключом второй таблицы. Если параметр не указать, он будет составлен из «названия второй таблицы в единственном числе + суффикс _id», например, tag_id.

Логика этой связи строится на выборе из второй таблицы всех записей, для которых существует запись посредством связи их первичного ключа и far_key в промежуточной таблице такая, что foreign_key равен первичному ключу найденного объекта.

С помощью языка sql это можно объяснить следующим образом (first — имя первой таблицы, second — имя второй таблицы, through — имя промежуточной таблицы, а 63 — первичый ключ найденного объекта):

SELECT `second`.* FROM `second` JOIN `through` ON (`through`.`far_key` = `second`.`id`) WHERE `through`.`foreign_key` = 63

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

Например:

class Model_Post extends ORM {
	protected $_has_many = array(
		'tags' => array(
			'model' => 'tag',
			'through' => 'posts_tags',
		),
	);
}
class Model_Tag extends ORM {}
$post = ORM::factory('post', 63);
$tags = $post->tags->find_all();
foreach($tags as $tag)
{
	echo $tag->id.', ';
} // 1, 2, 3, 

В данном случае, будет сгенерированы следующие sql-запросы:

SELECT `post`.* FROM `posts` AS `post` WHERE `post`.`id` = 2 LIMIT 1
SELECT `tag`.* FROM `tags` AS `tag` JOIN `posts_tags` ON (`posts_tags`.`tag_id` = `tag`.`id`) WHERE `posts_tags`.`post_id` = '2'

Добавление связи

Чтобы добавить к текущему объекту связь с другом объектом, доступен метод add($alias, $far_keys), где $alias — имя связи, а $far_keys — объект (объекты) для связи. Последний параметр можно указать несколькими способами:

  • Первичный ключ объекта
  • Массив первичных ключей нескольких объектов
  • Сам объект (загруженный)

Пример добавления связи с загруженным объектом:

$post = ORM::factory('post', 63);
$tag = ORM::factory('tag')
	->where('name', '=', 'Breaking bad')
	->find();
$post->add('tag', $tag);

После добавления связи сохранять объект не нужно, запрос к базе данных генерируется в момент запуска метода add.

Удаление связи

Чтобы удалить связь с другим объектом, доступен метод remove($alias, $far_keys), где $alias — имя связи и $far_keys — объект или объекты. Пследний параметр также можно задать несколькими способами:

  • Первичный ключ объекта
  • Массив первичных ключей нескольких объектов
  • Сам объект (загруженный)
  • NULL, в этом случае удалятся связи со всеми объектами

Пример удаления связей с несколькими объектами:

$post = ORM::factory('post', 63);
$post->remove('tag', array(555, 666));

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

Проверка наличия связи

Для того, чтобы проверить, существует ли у объекта связь с другим или другими, совершенно необязательно производить поиск. Kohana предоставляет метод has($alias, $far_keys), который позволяет проверить, существует ли у текущего объекта связь с объектами $far_keys через связь $alias. Параметр $far_keys, опять же, можно задать любым удобным способом:

  • Первичный ключ объекта
  • Массив первичных ключей нескольких объектов
  • Сам объект (загруженный)

Метод возвращает TRUE, если существует связь сразу со всеми заданными объектами, и FALSE в другом случае.

Пример реализации проверки связи:

$post = ORM::factory('post', 63);
if($post->has('tag', 4))
{
	// Запись с id = 63 имеет связь с тегом с id = 4
}

Подгрузка связей

Подгрузка связей работает только со связями типа «Один к одному» (has one) и «Много к одному» (belongs to).

Генерация еще одного sql запроса при каждом обращении к связям не всегда может устраивать, поэтому kohana предоставляет метод with($target_path), позволяющий подгрузить связь $target_path в одном запросе к базе данных, используя конструкцию sql JOIN. Пример подгрузки связи «много к одному»:

$posts = ORM::factory('post')
	->with('category')
	->find_all();
foreach($posts as $post)
{
	echo $post->category->name;
	echo $post->category->id;
}

В данном случае сгенерируется всего 1 sql-запрос:

SELECT `category`.`id` AS `category:id`, `category`.`name` AS `category:name`, `post`.* FROM `posts` AS `post`
LEFT JOIN `categories` AS `category` ON (`category`.`id` = `post`.`category_id`)

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

class Model_Post extends ORM {
	protected $_load_with = array('category');
	...

После этого, следующий код сгенерирует тот же самый один запрос:

$posts = ORM::factory('post')->find_all();
foreach($posts as $post)
{
	echo $post->category->name;
	echo $post->category->id;
}

У связанного объекта тоже могут быть связи. Чтобы подгрузить в одном запросе и их, необходимо указать их методу with() через двоеточие ::

$posts = ORM::factory('post')
	->with('category:country')
	->find_all();

Можно комбинировать и подгружать несколько связей вместе:

$cities = ORM::factory('city')
	->with('region:country')
	->with('category')
	->find_all();
foreach($cities as $city)
{
	echo $city->region->country;
	echo $city->category;
}

Вложенность и количество этих связей ничем не ограничена.

Валидация

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

Составление правил

Составление правил происходит в публичном методе rules() при определении модели. Метод должен отдавать массив, индексами которого должны являться названия полей объекта, а элементами — массивы правил для них. Приведем пример:

class Model_Post extends ORM {
	public function rules()
	{
		return array(
			'title' => array(
				array('not_empty'), array('max_length', array(':value', 200)),
			);
		);
	}
}

В этой модели для поля title будут установлены два правила: not_empty(:value) и max_length(:value, 200) (:value — значение поля).

Зачем объявление правил сделано в методе, а не в параметре объекта? Дело в том, что с помощью метода можно реализовать наследование правил при наследовании классов. Это бывает очень полезно, например, в реализации класса Model_Account (аккаунт) объявить правила для полей логина и пароля, а в классе Model_Account_User extends Model_Account (аккаунт пользователя) добавить правила для имени и фамилии.

Пример:

class Model_Account {
	public function rules()
	{
		return array(
			'password' => array(...),
			'username' => array(...),
		);
	}
}
class Model_Account_User extends Model_Account {
	public function rules()
	{
		return array_merge(parent::rules(), array(
			'firstname' => array(...),
			'lastname' => array(...),
		));
	}
}

В итоге, в классе Model_Account_User будут правила для полей password, username, firstname и lastname.

Проверка

Проверка запускается автоматически при сохранении объекта (методы save, update, create), а также через отдельный метод check. Вне зависимости, какие поля были изменены, проверка идет полностью по всем заданным правилам. Пример:

$user = ORM::factory('user')
	->set('email', 'xdelphist@gmail.com');
$user->check();

Если после последней проверки вы не изменяли объект, то при сохранении новой проверки не будет.

Для всех вышеперечисленных методов существует единственный опциональный аргумент $extra_validation — произвольный объект валидации, который будет проверен вместе с объектом.

Для чего он нужен? Часто, в самой модели невозможно предусмотреть проверку некоторых полей. Например, таких, как поле с проверочными символами (captcha), чекбокс о принятии пользовательского соглашения и т.д.

Проверка этих полей должна выноситься в отдельный объект валидации (Validation) и передаваться любому из этих методов, и в случае возникновения ошибки в объекте либо в дополнительной валидации, объект сохранен (проверен) не будет.

Приведем пример:

$extra_validation = Validation::factory($_POST)
	->rule('terms_agree', 'not_empty'); // Дополнительная валидация, что поле terms_agree не пустое
$user = ORM::factory('user')
	->values($_POST, array('username', 'email', 'password')); // достаем из $_POST поля username, email, password
$user->save($extra_validation); // Обе валидации запускаются в этом методе

В этом примере, помимо валидации данных модели, идет проверка, что пользователь отметил чекбокс terms_agree (пользовательское соглашение). В случае, если чекбокс не отмечен, либо какое-то другое поле не заполнено (в зависимости от внутренних правил модели user), будет вызвана ошибка валидации.

Перехват ошибок

При возникновении ошибки Kohana генерирует исключение ORM_Validation_Exception, которое содержит методы для получения списка ошибок и работы с ними. Поэтому, метод, содержащий проверку всегда нужно заключать в конструкцию try … catch:

try
{
	$model->save();
	echo 'Модель успешно сохранена';
}
catch(ORM_Validation_Exception $e)
{
	echo 'Произошла ошибка';
}

Перехватываемое исключение содержит метод errors($directory, $translate), с двумя необязательными аргументами $directory — директорию, из которой брать сообщения ошибок (messages) и $translate — флаг, при котором будет произведен еще и перевод сообщений.

Пример:

try
{
	$model->save($some_extra_validation);
	$this->request->redirect('successpage');
}
catch(ORM_Validation_Exception $e)
{
	$errors = $e->errors();
	// выводим список ошибок $errors в своем представлении
}

Проверка нескольких моделей одновременно

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

Для начала, создадим пустой объект исключения:

$error = new ORM_Validation_Exception('', Validation::factory(array()));

В первом аргументе мы передаем пустое имя валидации, а во втором — пустой объект валидации. Таким образом, у нас есть исключение об ошибке, не содержащее никаких ошибок. Затем, мы создаем сами модели и вставляем в них значения:

$ticket = ORM::factory('support_ticket')->values($_POST, array('subject', 'category_id'));
$reply = ORM::factory('support_reply')->values($_POST, array('message'));

Как должно быть понятно из названий объектов и параметров в этом коде, мы создаем тикет (support_ticket) в систему поддержки, и сразу же добавляем к нему одно сообщение (support_reply). Понятно, что данные из формы должны удовлетворять правилам как тикета support_ticket (это поля категории category_id и темы subject), так и первого в нем сообщения support_reply (поле сообщения message).

Далее нам потребуется созданное нами исключение. Мы будем проверять обе модели с помощью метода check(), и в случае перехвата ошибки, объединять ее с нашим пустым исключением с помощью метода исключения merge:

try
{
	$ticket->check();
}
catch(ORM_Validation_Exception $e)
{
	$error->merge($e);
}
try
{
	$reply->check();
}
catch(ORM_Validation_Exception $e)
{
	$error->merge($e);
}

Чтобы проверить, какие появились ошибки, нужно вызвать метод errors(), который мы разбирали выше. В случае, если ошибок не возникало, этот массив будет пустым, иначе ошибки для каждого объекта будут записаны в отдельном элементе. Поэтому, завершить этот код можно следующим образом:

$errors = $error->errors();
if( ! $errors)
{
	$ticket->save();
	$reply->set('ticket', $ticket)->save();
	// Сохранение прошло успешно
}
else
{
	// Массив $errors содержит все допущенные ошибки
}

Фильтры

Нередко, перед сохранением записи в базу данных, нужно совершить определенные действия над ее полями. Например, перекодировать строку, сохранить файл на диске сервера, преобразовать произвольную дату в понятный базе данных тип timestamp и т.д. Для этого существует фильтры — функции (методы), которые выполнятся перед сохранением записи, и результат выполнения которых будет значением определенных полей. Как и правила валидации, фильтры объявляются через специальный публичный метод filters(), который должен возвратить массив, индексами которого будут названия полей, а элементами — массивы фильтров для них.

Фильтр задается в виде массива, первый элемент которого — название функции, и, опциональный второй — массив аргументов, передаваемых функции.

Название фильтра можно указать несколькими способами:

  • Название php функции, например, intval или trim.
  • Статический метод класса через ::, например, Text::random
  • Массив, первым элементом которого является экземпляр класса, а вторым — строковое название его метода, например, array($this, 'some_method').

По умолчанию, если не задать список аргументов для фильтра, для него передастся только один аргумент — значение поля. Для автоматической подстановки доступны следующие выражения:

  • :field — название поля.
  • :model — текущая модель.
  • :value — значение поля.

Пример задания фильтров:

class Model_Post extends ORM {
public function filters() 
{
	return array(
		'posted' => array(
			array('strtotime'),
		),
		'title' => array(
			array('Custom_Class::clean', array(':model', ':value')),
		),
	);
}

В данном случае, поле posted преобразуется из произвольного формата времени в unix timestamp с помощью функции strtotime, а поле title обработается статическим методом clean класса Custom_Class.

Если некоторые фильтры нужно применить для всех полей сразу, необходимо поместить их в элемент массива с индексом TRUE:

class Model_Post extends ORM {
public function filters() 
{
	return array(
		TRUE => array(
			array('trim'),
		),
	);
}

В данном случае, все поля подвергнутся обработке их функцией trim.

Кеширование

Кеширование запросов к БД — одна из первых задач, возникающих при работе приложения. С помощью Kohana можно реализовать несколько способов кеширования.

  • Кеширование записей полностью. В этом случае в кеше сохраняются записи вместе со значениями полей из базы данных. При использовании этого вида кеширования запросов к БД не происходит.
  • Кеширование только id записей подходит, если вам нужны актуальные значения из базы данных, но существует сложный алгоритм сортировки или выборки. В этом случае, при запросе кеша происходит несколько запросов к базе данных, которые запрашивают запись напрямую по ее id.

Полное кеширование

Для реализации полного кеширования, необходимо в определении модели установить внутренний параметр _reload_on_wakeup в FALSE — это означает, что модели не будут подгружаться из базы данных заново после получения из кеша:

class Model_Track extends ORM {
	protected $_reload_on_wakeup = FALSE;

Реализация непосредственного кеширования выглядит так:

// Проверяем, есть ли эти записи в кеше.
if( ! $most_active_users = unserialize(Cache::instance()->get('widget.most_active_users')))
{
	// Записей нет, поэтому выбираем их из БД
	$most_active_users = ORM::factory('user')
		->limit(5)
		->order_by('activity_sum', 'DESC')
		->find_all();
 
	// Сохраняем записи в кеш на 10 минут
	Cache::instance()->set('widget.most_active_users', serialize($most_active_users), Date::MINUTE * 10);
}
else
{
	// Записи получены из кеша. В этом блоке можно что-нибудь с ними сделать
}
// В этом участке кода записи будут загружены в любом случае

После выполнения этого кода нужные данные будут доступны в переменной $most_active_users.

Кеширование по id

Для реализации этого типа кеширования нужно установить тот же самый внутренний параметр _reload_on_wakeup в TRUE. Собственно, так он и стоит по умолчанию:

class Flower extends ORM {
	protected $_reload_on_wakeup = TRUE;

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *