
Настройка фреймворка
Для того что бы начать обрабатывать ошибки фреймворка Kohana вам нужно будет в файле application/bootstrap.php в методе Kohana::init добавить директиву обработки ошибок ‘errors’ => TRUE. Эта директива сообщит фреймворку о том что нужно конвертировать PHP ошибки в исключения которые понадобятся нам в дальнейшем.
1. Создаем свой обработчик исключений
Для того, что бы наше приложение смогло обрабатывать ошибки фреймворка нам нужно переопределить метод обработки ошибок системы, для этого нужно создать собственный обработчик ошибок и разместить его в каталоге классов приложения applicationclasseskohanaexception.php.
Код обработчика ошибок:
<?php defined('SYSPATH') or die('No direct script access.');
class Kohana_Exception extends Kohana_Kohana_Exception {
public static function handler(Exception $e)
{
if (Kohana::DEVELOPMENT === Kohana::$environment)
{
parent::handler($e);
}
else
{
try
{
Kohana::$log->add(Log::ERROR, parent::text($e));
$attributes = array
(
'action' => 500,
'message' => rawurlencode($e->getMessage())
);
if ($e instanceof HTTP_Exception)
{
$attributes['action'] = $e->getCode();
}
// Error sub-request.
echo Request::factory(Route::get('error')->uri($attributes))
->execute()
->send_headers()
->body();
}
catch (Exception $e)
{
// Clean the output buffer if one exists
ob_get_level() and ob_clean();
// Display the exception text
echo parent::text($e);
// Exit with an error status
exit(1);
}
}
}
} // End Kohana_Exception
Давайте кратко разберем что делает данный класс. Единственное что мы тут делаем это переопределение системного метода фреймворка:
public static function handler(Exception $e)
В теле этого метода мы анализируем в каком режиме находится наше приложение и как будут обрабатываться ошибки системы. Если наше приложение находится в режиме разработки то мы выполняем стандарный метод системы для обработки ошибок (это мы делаем для того что бы видеть красивую и информативную стандартную страницу ошибки в фреймворке), иначе:
- Добавляем текст ошибки в лог
- Устанавливаем параметры маршрута по умолчанию
- Если произошла HTTP ошибка, устанавливаем ее код как параметр маршрута action
- Выполняем внутренностный запрос в системе (используем HMVC)
По умолчанию в качестве action для ошибок будет использоваться метод контроллера action_500 (внутренняя ошибка сервера), в случае если произошла HTTP ошибка в качестве действия контролера будет выполняться метод action_<Код HTTP ошибки> это позволит обрабатывать такие ошибки системы как 404, 403 и т. д.
Добавляем маршрут для обработки ошибок в файл application/bootstrap.php перед маршрутом по умолчанию.
Код маршрута:
Route::set('error', 'error/<action>(/<message>)', array('action' => '[0-9]++', 'message' => '.+'))
->defaults(array(
'controller' => 'error_handler'
));
3. Создаем контроллер для обработки ошибок
Обработкой наших ошибок будет заниматься специальный контроллер расположенный в файле applicationclassescontrollererrorhandler.php.
Код файла контроллера:
<?php defined('SYSPATH') or die('No direct script access.');
class Controller_Error_Handler extends Controller_Template {
public $template = 'error';
public function before()
{
parent::before();
$this->template->page = URL::site(rawurldecode(Request::$initial->uri()));
// Если внутренний запрос
if (Request::$initial !== Request::$current)
{
if ($message = rawurldecode($this->request->param('message')))
{
$this->template->message = $message;
}
}
else
{
$this->request->action(404);
}
// устанавливаем HTTP статус
$this->response->status((int) $this->request->action());
}
public function action_404()
{
$this->template->title = '404 Страница не найдена';
// тут мы проверяем пришли попали ли мы на 404 страницу с нашего сайта
if (isset ($_SERVER['HTTP_REFERER']) AND strstr($_SERVER['HTTP_REFERER'], $_SERVER['SERVER_NAME']) !== FALSE)
{
// устанавливаем влаг о том что 404 ошибка была с внутренней сслки
$this->template->local = TRUE;
}
// устанавливаем HTTP статус
$this->response->status(404);
}
public function action_503()
{
$this->template->title = 'Сервис недоступен';
}
public function action_500()
{
$this->template->title = 'Внутренняя ошибка сервера';
}
} // End Error_Handler
Давайте кратко посмотрим что делает наш контроллер.
В методе before мы определяем каким методом пытаются выполнить действие нашего контроллера используется HMVC (внутренный запрос системы) или кто то пытается получить доступ к действиям нашего контроллера через адресную строку, и в случае если у нас не внутренний запрос то мы выполняем действие action_404. Так же мы создали на разные типы ошибок собственные действия которые устанавливают переменные для использования в шаблоне и нужные HTTP коды для ответа сервера.
4. Создаем вид отображения ошибок
Последнее что нам осталось сделать это создать вид для отображения ошибок для этого создадим файл application/views/error.php с следующим содержанием:
<?php defined('SYSPATH') or die('No direct script access.'); ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf8" />
<meta name="author" content="cyberapp.ru" />
<title><?php echo $title ?></title>
<style type="text/css">
* { margin: 0; padding: 0; }
html, body{ width: 100%; height: 100%; }
#wrap{ width: 900px; margin: 100px auto 0 auto; }
</style>
</head>
<body>
<div id="wrap">
<h1><?php echo $title ?></h1>
<?php if (isset($local) AND $local) : ?>
404 ошибка произошла по ссылке с нашего сайта.
<?php endif; ?>
</div>
</body>
</html>
Итог:
Вот собственно и все, что нужно сделать для того что бы наше приложение научилось отображать собственные страницы ошибок. Ниже представлено несколько примеров того как можно генирировать ошибки системы
// генерируем 503 ошибку
throw new HTTP_Exception_503('Сайт не работает');
// генерируем 404 ошибку
throw new HTTP_Exception_404(':file не найден.', array(':file' => 'Имя файла'));
// генерируем системную ошибку
throw new Kohana_Exception('Каталог :dir должен быть доступен для записи', array(':dir' => Debug::path(Kohana::$cache_dir)));
Kohana_Exception
extends Kohana_Kohana_Exception
extends Exception
This class is a transparent base class for Exception and
should not be accessed directly.
Kohana exception class. Translates exceptions using the I18n class.
- package
- Kohana
- category
- Exceptions
- author
- Kohana Team
- copyright
- © 2008-2012 Kohana Team
- license
- https://kohana.top/license
Class declared in SYSPATH/classes/Kohana/Exception.php on line 3.
Methods
- __construct()
- __toString()
- _handler()
- handler()
- log()
- response()
- text()
- __wakeup()
- getCode()
- getFile()
- getLine()
- getMessage()
- getPrevious()
- getTrace()
- getTraceAsString()
- __clone()
Properties
-
public static
string$error_view -
error rendering view
-
string(12) "kohana/error" -
public static
string$error_view_content_type -
error view content type
-
string(9) "text/html" -
public static
array$php_errors -
PHP error code => human readable name
-
array(9) ( 1 => string(11) "Fatal Error" 256 => string(10) "User Error" 4 => string(11) "Parse Error" 2 => string(7) "Warning" 512 => string(12) "User Warning" 2048 => string(6) "Strict" 8 => string(6) "Notice" 4096 => string(17) "Recoverable Error" 8192 => string(10) "Deprecated" )
-
protected
$code - Default value:
integer 0
-
protected
$file - Default value:
NULL
-
protected
$line - Default value:
NULL
-
protected
$message - Default value:
string(0) ""
Methods
public __construct( [ string $message = string(0) «» , array $variables = NULL , integer|string $code = integer 0 , Exception $previous = NULL ] )
(defined in Kohana_Kohana_Exception)
Creates a new translated exception.
throw new Kohana_Exception('Something went terrible wrong, :user', [':user' => $user]);
Parameters
-
string
$message
= string(0) «» — Error message -
array
$variables
= NULL — Translation variables -
integer|string
$code
= integer 0 — The exception code -
Exception
$previous
= NULL — Previous exception
Return Values
void
Source Code
public function __construct($message = "", array $variables = null, $code = 0, Exception $previous = null)
{
// Set the message
$message = __($message, $variables);
// Pass the message and integer code to the parent
parent::__construct($message, (int) $code, $previous);
// Save the unmodified code
// @link http://bugs.php.net/39615
$this->code = $code;
}
public __toString( )
(defined in Kohana_Kohana_Exception)
Magic object-to-string method.
echo $exception;
Tags
Return Values
string
Source Code
public function __toString()
{
return Kohana_Exception::text($this);
}
public static _handler( Throwable $e )
(defined in Kohana_Kohana_Exception)
Exception handler, logs the exception and generates a Response object
for display.
Parameters
-
Throwable
$e
required — $e
Tags
Return Values
Response
Source Code
public static function _handler($e)
{
try {
// Log the exception
Kohana_Exception::log($e);
// Generate the response
$response = Kohana_Exception::response($e);
return $response;
} catch (Exception $e) {
/**
* Things are going *really* badly for us, We now have no choice
* but to bail. Hard.
*/
// Clean the output buffer if one exists
ob_get_level() AND ob_clean();
// Set the Status code to 500, and Content-Type to text/plain.
header('Content-Type: text/plain; charset=' . Kohana::$charset, true, 500);
echo Kohana_Exception::text($e);
exit(1);
}
}
public static handler( Throwable $e )
(defined in Kohana_Kohana_Exception)
Inline exception handler, displays the error message, source of the
exception, and the stack trace of the error.
Parameters
-
Throwable
$e
required — $e
Tags
Return Values
void
Source Code
public static function handler($e)
{
$response = Kohana_Exception::_handler($e);
// Send the response to the browser
echo $response->send_headers()->body();
exit(1);
}
public static log( Throwable $e [, int $level = integer 0 ] )
(defined in Kohana_Kohana_Exception)
Logs an exception.
Parameters
-
Throwable
$e
required — $e -
int
$level
= integer 0 — $level
Tags
Return Values
void
Source Code
public static function log($e, $level = Log::EMERGENCY)
{
if (is_object(Kohana::$log)) {
// Create a text version of the exception
$error = Kohana_Exception::text($e);
// Add this exception to the log
Kohana::$log->add($level, $error, null, ['exception' => $e]);
// Make sure the logs are written
Kohana::$log->write();
}
}
public static response( Throwable $e )
(defined in Kohana_Kohana_Exception)
Get a Response object representing the exception
Parameters
-
Throwable
$e
required — $e
Tags
Return Values
Response
Source Code
public static function response($e)
{
try {
// Get the exception information
$class = get_class($e);
$code = $e->getCode();
$message = $e->getMessage();
$file = $e->getFile();
$line = $e->getLine();
$trace = $e->getTrace();
/**
* HTTP_Exceptions are constructed in the HTTP_Exception::factory()
* method. We need to remove that entry from the trace and overwrite
* the variables from above.
*/
if ($e instanceof HTTP_Exception AND $trace[0]['function'] == 'factory') {
extract(array_shift($trace));
}
if ($e instanceof ErrorException) {
/**
* If XDebug is installed, and this is a fatal error,
* use XDebug to generate the stack trace
*/
if (function_exists('xdebug_get_function_stack') AND $code == E_ERROR) {
$trace = array_slice(array_reverse(xdebug_get_function_stack()), 4);
foreach ($trace as & $frame) {
/**
* XDebug pre 2.1.1 doesn't currently set the call type key
* http://bugs.xdebug.org/view.php?id=695
*/
if (!isset($frame['type'])) {
$frame['type'] = '??';
}
// Xdebug returns the words 'dynamic' and 'static' instead of using '->' and '::' symbols
if ('dynamic' === $frame['type']) {
$frame['type'] = '->';
} elseif ('static' === $frame['type']) {
$frame['type'] = '::';
}
// XDebug also has a different name for the parameters array
if (isset($frame['params']) AND ! isset($frame['args'])) {
$frame['args'] = $frame['params'];
}
}
}
if (isset(Kohana_Exception::$php_errors[$code])) {
// Use the human-readable error name
$code = Kohana_Exception::$php_errors[$code];
}
}
/**
* The stack trace becomes unmanageable inside PHPUnit.
*
* The error view ends up several GB in size, taking
* serveral minutes to render.
*/
if (
defined('PHPUnit_MAIN_METHOD')
OR
defined('PHPUNIT_COMPOSER_INSTALL')
OR
defined('__PHPUNIT_PHAR__')
) {
$trace = array_slice($trace, 0, 2);
}
// Instantiate the error view.
$view = View::factory(Kohana_Exception::$error_view, get_defined_vars());
// Prepare the response object.
$response = Response::factory();
// Set the response status
$response->status(($e instanceof HTTP_Exception) ? $e->getCode() : 500);
// Set the response headers
$response->headers('Content-Type', Kohana_Exception::$error_view_content_type . '; charset=' . Kohana::$charset);
// Set the response body
$response->body($view->render());
} catch (Exception $e) {
/**
* Things are going badly for us, Lets try to keep things under control by
* generating a simpler response object.
*/
$response = Response::factory();
$response->status(500);
$response->headers('Content-Type', 'text/plain');
$response->body(Kohana_Exception::text($e));
}
return $response;
}
public static text( Throwable $e )
(defined in Kohana_Kohana_Exception)
Get a single line of text representing the exception:
Error [ Code ]: Message ~ File [ Line ]
Parameters
-
Throwable
$e
required — $e
Return Values
string
Source Code
public static function text($e)
{
return sprintf('%s [ %s ]: %s ~ %s [ %d ]', get_class($e), $e->getCode(), strip_tags($e->getMessage()), Debug::path($e->getFile()), $e->getLine());
}
final public getCode( )
(defined in Exception)
final public getFile( )
(defined in Exception)
final public getLine( )
(defined in Exception)
final public getMessage( )
(defined in Exception)
final public getPrevious( )
(defined in Exception)
final public getTrace( )
(defined in Exception)
final public getTraceAsString( )
(defined in Exception)
final private __clone( )
(defined in Exception)
First of all, you need to make sure you are loading your module by including it in the modules section of your application/bootstrap.php file like so
Kohana::modules(array(
'my'=>MODPATH.'my'
)
);
The fact that you mentioned going directly to the url for your error handler controller triggers a 404 error makes me think your module has not been loaded.
I would also suggest a few more changes.
http_response_exception.php does not need to extend Kohana_Exception, since this class is not an exception, but an exception handler. Along those same lines, a more appropriate class name might be Exception_Handler, since the class is not representing an exception, but handling them. Secondly, because of how you’ve named this file, it should be located in modules/my/classes/http/response/exception.php. Other than that, the code for this class looks ok.
Similarly, because of how you’ve named your controller, it should be located and named a bit differently. Move it to modules/my/classes/controller/error/handler.php
Remember that underscores in a class name means a new directory, as per http://kohanaframework.org/3.2/guide/kohana/conventions
Finally, I don’t think you really need to extend the Kohana_Core class here, but instead just register your own custom exception handler. You can register your custom exception handler in either your application’s bootstrap file, or in your module’s init file with the following generic code:
set_exception_handler(array('Exception_Handler_Class', 'handle_method'));
Here’s a customer exception handler I use, which is pretty similar to yours:
<?php defined('SYSPATH') or die('No direct script access.');
class Exception_Handler {
public static function handle(Exception $e)
{
$exception_type = strtolower(get_class($e));
switch ($exception_type)
{
case 'http_exception_404':
$response = new Response;
$response->status(404);
$body = Request::factory('site/404')->execute()->body();
echo $response->body($body)->send_headers()->body();
return TRUE;
break;
default:
if (Kohana::$environment == Kohana::DEVELOPMENT)
{
return Kohana_Exception::handler($e);
}
else
{
Kohana::$log->add(Log::ERROR, Kohana_Exception::text($e));
$response = new Response;
$response->status(500);
$body = Request::factory('site/500')->execute()->body();
echo $response->body($body)->send_headers()->body();
return TRUE;
}
break;
}
}
}
Permalink
Cannot retrieve contributors at this time
Обработка ошибок/исключений
Kohana предоставляет обработчик как для исключений, так и для ошибок (он превращает ошибку в исключение с помощью стандартного PHP-класса ErrorException). Обработчик показывает множество подробностей и внутреннее состояние приложения:
- Класс исключения
- Уровень ошибки
- Текст ошибки
- Исходный код, вызвавший ошибку, соответствующая строка подсвечивается
- Трассировка хода выполнения
- Подключенные файлы, загруженные расширения и глобальные переменные
Пример
Нажмите по любой ссылке для раскрытия блока дополнительной информации:
{{userguide/examples/error}}
Отключение обработчика ошибок/исключений
Если Вы не хотите использовать встроенный обработчик ошибок, отключите его с помощью [Kohana::init]:
Kohana::init(array('errors' => FALSE));
ORM в Kohana
Одним из самых важных модулей 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 типа связей для объектов:
-
Один к одному (
has one). Данный тип связей используется, когда для одной записи из одной таблицы имеется одна запись из другой. Пример реализации: у каждого автора из таблицыusersесть 1 запись из таблицыpassports. -
Много к одному (
belongs to). Этот тип связи используется, когда для многих записей из одной таблицы имеется одна запись из другой. Пример: для каждой записи из таблицыpostsимеется одна категория из таблицыcategories. Другими словами, можно сказать, что запись (posts) принадлежит (англ. belongs to) к категории (categories). -
Один к многим (
has many). Тип связи подразумевает, что у одной записи есть много записей из другой таблицы, причем принадлежащие только ей. Например, у одного города (cities) может быть несколько жителей (users). Тем не менее, один житель может принадлежать только к одному городу. -
Много ко многому (
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', 'mail@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;
Дата публикации: 2015-04-14 01:41:53
