Kohana: Контроллер для CLI

Kohana - в первую очередь фреймворк, т.е. каркас приложений, и не обязательно для Web. Результатом работы Веб приложения может быть не только гипертекст(HTML) но и JSON, XML, изображения, простой текст, так и вовсе ничего.

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

Всё-таки оказалось, что kohana сильно ориентирован на вэб, хотя казалось бы, компоненты должны быть отдельно друг от друга.

Для написания консольного приложения(впрочем как и "демона") на основе Kohana нужно немного "допилить" контроллер. По-прежнему всю логику будет выполнять именно он, за одним исключением - если приложение и будет что то выводить - то это сообщения для отладки.

Важным моментом в этом является отлавливание ошибок. Стандартный отлов ошибок и исключений будет работать по-прежнему, но при малейшем E_NOTICE у вас в консоль отобразиться как минимум строк сто HTML кода - страница ошибки. Рано или поздно надоест прокручивать вверх и разбираться в коде - почему бы не представить ошибку в более понятном и читаемом виде?

Итак, привожу свой класс-прослойку над основным контроллером приложения (BaseController) и любым консольным приложением или демоном. В нём переоределяются обработчики ошибок и исключений, а также отключается буферезация вывода(иначе текста вы никогда не увидите!) - а в консоль нужно выводить незамедлительно - иначе нет смысла в такой информации.

Также добавил в класс поля для раскраски консоли(для *nix серверов), если сервер на windows, то нужно присвоить всем полям _c* = ''


abstract class ConsoleController_Core extends BaseController_Core {
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);

protected $_cReset = "\033[0m";
protected $_cFile = "\033[37m";
protected $_cClass = "\033[36m";
protected $_cLine = "\033[33m";
protected $_cTextError = "\033[35m\033[4m";

/**
* Конструктор
* Устанавливает обработчики
*/
public function __construct() {
parent::__construct();
set_error_handler(array(&$this, '_error_handler'));
set_exception_handler(array(&$this, '_exception_handler'));
}

/**
* Метод, вызываемый после конструктора, перед методом
* Очищает буфера Kohana
*/
public function _before() {
parent::_before();
if (ob_get_level() > 0) while (ob_get_level()) ob_end_clean();
Kohana::close_buffers();
}

/**
* Обработчик ошибок
* @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 ;
if (in_array($errno, $this->skipErrors)) return;

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

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

die();
}

/**
* Обработчик неотловленных исключений
* @param Exception $ex исключение
*/
public function _exception_handler($ex) {
$errfile = str_ireplace(realpath(APPPATH.'..'), '', @$ex->getFile());
echo "Exception {$this->_cClass}". get_class($ex) . "{$this->_cReset} ";
echo "in {$this->_cFile}{$errfile}{$this->_cReset}:";
echo "{$this->_cLine}{$ex->getLine()}{$this->_cReset}\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 $this->_cClass."{$d['class']}\033[0m{$d['type']}";
}
echo "\033[31m{$d['function']}\033[0m";
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 {$this->_cFile}{$errfile}{$this->_cReset}:";
echo "{$this->_cLine}{$d['line']}{$this->_cReset}";
}
echo "\n";
}
echo "\n";

}

}

Ещё одно важное примечание - метод _before() - вызываемый после конструктора контроллера, с использованием событийной модели Kohana. Если такого(или аналогичного) метода у вас не определенно в базовом контроллере, то можно внутри ConsoleController добавить вызов $this->_before(); в конец тела конструктора.

Файлик с контроллером следует называть ConsoleController и положить в папку с библиотеками(application/libraries)

Ваше приложение/демон должно расширять класс ConsoleController_Core и размещаться как обычно в application/controllers

Ещё есть один момент - желательно сделать отдельную входную точку для не Web-приложений и снять ограничение по времени выполнения. Этот файл нужно положить на один и тот же уровень с index.php.
Например так выглядит мой cli.php:

<?php
set_time_limit(0);
$_GET['debug'] = 'false'; // disable debug_toolbar module
require 'kohana/index.php';
?>

Например:


class Cron_Controller extends ConsoleController_Core {
public function __construct() {
parent::__construct();
}

public function a($lolo = 123) {
$this->b($lolo, "some string", new stdClass);
}

public function b($a1, $a2, $cl) {
sdfsdafsd;
}

. . . .
}

Запускаю скрипт:

$ php monitor.php cron/a

ERROR
[NOTICE] IN /application/controllers/cron.php:87
Use of undefined constant sdfsdafsd - assumed 'sdfsdafsd'
Stack
Cron_Controller->b( 123, 'some string', stdClass object ) IN /application/controllers/cron.php:83
Cron_Controller->a()
ReflectionMethod->invokeArgs( Cron_Controller object, Array(0) ) IN /kohana/system/core/Kohana.php:291
Kohana::instance()
call_user_func( Array(2) ) IN /kohana/system/core/Event.php:209
Event::run( 'system.execute' ) IN /kohana/system/core/Bootstrap.php:55
require( '/usr/home/stat/pu...' ) IN /kohana/index.php:106
require( '/usr/home/stat/pu...' ) IN /monitor.php:7

А вот скриншот:

Разумеется, если немного изменить это же будет работать и в codeIgniter, впрочем, прикрутить этот обработчик можно к любому фреймворку.


Изначально я пытался вчитываться
в html код странцы ошибки, который
kohana заботливо выдавала в консоль,
а теперь - глаза не ломаются.
Странно что это как - то не предусмотрели.