Kohana: AJAX контроллер с ловлей ошибок

Продолжая тему написания custom контроллеров не для html вывода предлагаю мой вариант обработчика AJAX-запросов. Напомню, в предыдущий раз я описывал контроллер для консольного демона

Помимо удобной для меня выдаче данных(я предпочитаю в 99% случаев выдвать JSON) есть ещё ряд фитч:

  • Код контроллера многократно используется наследниками класса, которые при необходимости могут перегрузить необходимые методы. В них написать простой ответ клиенту с помощью не сложных функций можно в пару строк - не нужно отвлекаться на создание json ответа, а писать логику.
  • Дефолтные ответы об успешном выполнении запроса, предупреждения или ошибки
  • Ловятся ошибки фреймворка и формируется адекватный отчёт об ошибке, а не километровый HTML от Kohana - просто идеально при использовании fireBug или аналогичных средств отладки.
  • Если вызываемый метод не реализован в наследнике, то будет выдан user-friendly ответ(опять же если использовать регламент возвращаемых структур json)

Регламент

Для того, чтоб было удобно пользоваться требуется соблюдать регламент. Любой json ответ должен содержать boolean поле result, отвечающее на вопрос, успешно ли прошла операция. При этом на стороне клиента обязательно проверять его сразу же как вызван callback.

Если result=false, то у меня выдаётся сообщение об ошибке(например через alert) с текстом, присланным в поле error

Если result=true, то операция успешно завершилась. При этом если присутствует не пустое поле waring - то нужно выдать предупреждение пользователю.

Разумеется, помимо result нужно передать любую необходимую информацию.

Методы throw* немедленно выдают клиенту ответ и завершают работу скрипта

Code

Класс контроллера выглядит следующим образом

class Ajax_Controller extends BaseController_Core {
/**
* В конструкторе так же устанавливаются обработчики ошибок
* Надоело читать кохановский хтмл в фаербаге.
*/
public function __construct() {
parent::__construct();
set_error_handler(array(&$this, '_error_handler'));
set_exception_handler(array(&$this, '_exception_handler'));
}

/**
* Ловим любой вызываемый метод
*/
public function __call($method, $arg) {
if (method_exists($this, $method)) {
call_user_func_array(array(&$this, $method), $arg);
} else {
$this->throwError('Увы. Запрошенный вами запрос не может '
. 'быть выполнен, т.к он не обрабатывается. '
. 'Напишите, пожалуйста, администрации.');
}
}

/**
* Выбросить ошибку и прекратить выполнение
* @param string $text текст ошибки
*/
protected function throwError($text) {
$dat = array('result' => false, 'error' => $text);
die(json_encode($dat));
}

/**
* Завершить аджакс запрос успешно
* @param string|array|object $info что то вернуть?
* Если $info строка то в результирующий json будет добавлен info
* Если $info массив или объект то он будет конвертироваться в json как есть
*/
protected function throwSuccess($info = '') {
$dat = array('result' => true);
if (is_array($info)) {
if (!isset($info['result'])) $info['result'] = true;
die(json_encode($info));
}
if (is_object($info)) {
if (!isset($info->result)) $info->result = true;
die(json_encode($info));
}
if (is_string($info) && strlen($info)) $dat['info'] = $info;
die(json_encode($dat));
}

/**
* Завершить работу успешно, но выдать предупреждение клиенту
* @param string $waring
*/
protected function throwWaring($waring = '') {
$dat = array('result' => true);
if (strlen($waring)) $dat['waring'] = $waring;
die(json_encode($dat));
}

private $errorType = array (
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSING ERROR',
E_NOTICE => 'NOTICE',
E_CORE_ERROR => 'CORE ERROR',
E_CORE_WARNING => 'CORE WARNING',
E_COMPILE_ERROR => 'COMPILE ERROR',
E_COMPILE_WARNING => 'COMPILE WARNING',
E_USER_ERROR => 'USER ERROR',
E_USER_WARNING => 'USER WARNING',
E_USER_NOTICE => 'USER NOTICE',
E_STRICT => 'STRICT NOTICE',
E_RECOVERABLE_ERROR => 'RECOVERABLE ERROR'
);

protected $skipErrors = array(E_STRICT);

/**
* Обработчик ошибок
* @param int $errno номер ошибки (константы E_*)
* @param string $errstr описание строки ошибки
* @param string $errfile файл в котором произошла обшика
* @param int $errline номер строки в файле
*/
public function _error_handler($errno, $errstr, $errfile, $errline) {
//if ($errno == E_NOTICE) return ;
header("HTTP/1.1 500 Internal Server Error in ajax proccess");
if (in_array($errno, $this->skipErrors)) return;

$errfile = str_ireplace(realpath(APPPATH.'..'), '', $errfile);
echo "\n";
echo "ERROR\n";
echo " [{$this->errorType[$errno]}] IN ";
echo "{$errfile}:";
echo "{$errline} \n";
echo " {$errstr} \n";

$this->_print_stack(array_slice(debug_backtrace(), 1), 1);

die();
}

/**
* Обработчик неотловленных исключений
* @param Exception $ex исключение
*/
public function _exception_handler($ex) {
header("HTTP/1.1 500 Internal Server Error in ajax proccess(exception)");

$errfile = str_ireplace(realpath(APPPATH.'..'), '', @$ex->getFile());
echo "Exception ". get_class($ex) . " ";
echo "in {$errfile}:";
echo "{$ex->getLine()}\n";
echo "ERROR\n";
$cols = getenv('COLUMNS');
if (!$cols) $cols = 80;
for($e = $ex->getMessage(), $i = 0; $i < strlen($e) / $cols; $i++) {
echo " " . substr($e, $i * $cols, $cols) . "\n";
}

$this->_print_stack($ex->getTrace());

die();
}

/**
* Вывести стек в stdout
* @param array $stack стек - массив массивов
* @param int $skip_top сколько методов пропустить в верхнушке стека
* обычно это 2 - сам _print_stack и обработчик ошибки
*/
protected function _print_stack($stack, $skip_top = 2) {
echo "Stack \n";
foreach($stack as $d) {
if(!isset($d['file'])); $d['file'] = '';
$errfile = str_ireplace(realpath(APPPATH.'..'), '', $d['file']);
if (empty($d['file'])) $errfile = '';
if (!isset($d['line'])) $d['line'] = '';
echo ' ';

if (!empty($d['class'])) {
echo "{$d['class']}{$d['type']}";
}
echo "{$d['function']}";
echo '(';
if (isset($d['args']) && count($d['args'])) {
echo ' ';
$args = array();
foreach($d['args'] as $arg) {
ob_start();
if (is_null($arg)) echo 'NULL';
else if (is_object($arg)) echo get_class($arg) . ' object';
else if (is_string($arg)) {
$s = str_replace("\n", '\n', $arg);
if (strlen($s) > 20) $s = substr($s, 0, 17) . '...';
echo "'{$s}'";
} else if (is_scalar($arg)) echo $arg;
else if (is_array($arg)) echo 'Array(' . count($arg) . ')';
$args[] = ob_get_clean();
}
echo implode(', ', $args);
echo ' ';
}
echo ')';
if ($errfile && $d['line']) {
echo " IN {$errfile}:";
echo "{$d['line']}";
}
echo "\n";
}
echo "\n";

}
}

Этот код следует поместить в application/controllers/ajax.php

Пусть не смущает то, что этот контроллер наследуется от BaseController_Core, можно безболезненно поменять на базовый Kohan'ы - Controller_Core. [offtopic] Я предпочитаю максимум группировать классы, у которых есть нечто общее. В частности BaseController_Core добавляет многие методы, общие для web, cli, ajax отображений - такие как отладка в фоне, установка обработчиков событий до выполнения метода контроллера и после(любой наследник может перегрузить), подгрузка конфигурации и многое другое. [/offtopic]

Wanna example!

Пример наследованного класса, который делает нечто:

class Ausers_Controller extends Ajax_Controller {
public function getById($id) {
$user = new User_Model($id);
if (!$user->loaded) $this->throwError("Sorry, user doesn't exists!");
$this->throwSuccess(array('name' => $user->name));
}

public function getInfoById($id) {
$user = new User_Model($id);
if (!$user->loaded) $this->throwError("Sorry, user doesn't exists!");
$result = array('posts' => $user->posts,
'friends_count' => $user->friends->count(),
'fio' => "{$user->name} {$user->surname}");
$this->throwSuccess(array('name' => $user->name));
}

public function setStatus() {
$status = $this->input->post('status');
$this->_getCurrentUser()->status = $status;
if (empty ($status)) $this->throwWaring("Your status has been removed");
else $this->throwSuccess();
}
}

Я надеюсь, тут всё очень наглядно и названия объектов говорят за себя.

$this->throwSuccess() может принимать как объект так и массив - то, что будет переданно в json - как вам удобнее. И в нём вовсе не должно быть установленно поле result.

Файл с контроллером можно положить в controllers стандартно. По мне - лучше вынести все ajax-контроллеры отдельно и дописать "A" в начале имени класса - наглядно и удобно в навигации по коду.

Обработка ошибок

Пример ошибки из fireBug:

Удобно, не правда ли? Сложно не согласиться, особенно тем, кто дебажил до этого, вчитываясь в html =)

Мне очень нравится Kohana за возможность строить иерархии классов, не как в codeIgniter. В нём было очень проблематично расширять классы - приходилось использовать костыли

На базе этого класса можно построить ещё один абстрактный, например в котором инициализируется шаблонный движок в конструкторе или происходит проверка прав(авторизация), или ... много-много всего, в зависимости от задачи и фантазии