Ловим баги

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

Но все же, сколько ни старайся, всех ошибок не найдёшь. Поэтому было бы неплохо узнавать о тех ошибках, которые происходят в рабочем проекте. Это сделать, кстати, довольно просто при правильном подходе к программированию. Под правильным подходом я имею ввиду грамотную обработку ошибок и исключений. Приведу пример того, как я решал эту проблему. Решение довольно простое и, наверняка не ново, но всё-таки.

Итак есть сайт, который пишется на PHP с использованием Zend Framework. Для любой ситуации, в которой ход работы отклоняется от намеченного (проще говоря, какая-либо функция вернула недопустимый результат или переменная оказалась «пустой»), мы бросаем исключение. Причем, мы не обрабатываем исключение тут же (это не касается случаев, когда обрабатываются ошибки ввода данных; в таких случаях нужно фильтровать исключения по типу). Исключения должны быть обработаны либо в стандартном контроллере ErrorController (для нашей ситуации), либо на самом верхнем try-блоке. А обработка этих исключений сводится к выводу какого-нибудь сообщения о технических неполадках на сайте и отправке письма на заданный почтовый ящик с дампом исключения.

Почему лучше не обрабатывать исключения по ходу, производя какие-либо шаманские действия, чтобы «замять» ошибку? Потому что таких ситуаций возникать не должно в принципе. Лучше сразу получить сообщение об ошибке, чем гадать почему отвалился какой-нибудь блок на сайте.

Вот пример реализации скрипта отправки сообщения:

<?

class My_BugReport
{
	public function __construct(Exception $exception, $email, $subject='Bug report')
	{
		$mail = new Zend_Mail('utf-8');
		$mail->setSubject($subject);
		$mail->addTo($email);
		$mail->setBodyText($this->_explainException($exception));
		
		$mail->send();
	}

	protected function _explainException(Exception $exception)
	{
		$cwd = rtrim(getcwd(), '/');
		
		ob_start();
		echo 'Error: ' . '(' . $exception->getCode() . ') ' . $exception->getMessage() . "\n";
		echo 'File: .' . str_replace($cwd, '', $exception->getFile()) . "\n";
		echo 'Line: ' . $exception->getLine() . "\n\n";
		echo "Trace: \n\n";
		foreach ($exception->getTrace() as $call) {
			if (! empty($call['class'])) {
				echo $call['class'] . $call['type'];
			}
			echo $call['function'];
			echo '(';
			foreach ($call['args'] as $n=>$arg) {
				if ($n) echo ', ';
				$this->_explainArg($arg);
			}
			echo ")\n";
			echo '.' . str_replace($cwd, '', $call['file']) . ':' .  $call['line'] . "\n\n";
		}
		
		echo "\n\n";
		echo "Additional:\n\n";
		
		echo "Host: " . @$_SERVER['HTTP_HOST'] . "\n";
		echo "URI: " . @$_SERVER['REQUEST_URI'] . "\n";
		echo "Query: " . @$_SERVER['QUERY_STRING'] . "\n";
		
		if (!empty($_GET)) {
			echo "\n";
			echo "Get:\n";
			foreach ($_GET as $k=>$v) {
				echo "\t$k=$v\n";
			}
		}
		
		if (!empty($_POST)) {
			echo "\n";
			echo "Post:\n";
			foreach ($_POST as $k=>$v) {
				echo "\t$k=$v\n";
			}
		}
		
		if (!empty($_COOKIE)) {
			echo "\n";
			echo "Cookie:\n";
			foreach ($_COOKIE as $k=>$v) {
				echo "\t$k=$v\n";
			}
		}
		
		return ob_get_clean();
	}
	
	protected function _explainArg($arg) 
	{
		if (is_string($arg)) {
			echo "'$arg'";
		} elseif (is_int($arg) || is_float($arg) || is_double($arg)) {
			echo $arg;
		} elseif (is_bool($arg)) {
			echo $arg ? 'true' : 'false';
		} elseif (is_object($arg)) {
			echo 'object ' . get_class($arg);
		} elseif (is_array($arg)) {
			echo 'array(';
			$first = true;
			foreach ($arg as $k=>$v) {
				if ($first) {
					$first = false;
					echo ', ';
				}
				if (! is_numeric($k)) {
					echo "'$k' => ";
				}
				$this->_explainArg($v);
			}
			echo ')';
		} else {
			echo gettype($arg);
		}
	}
}

Схема обработки исключений примерно следующая:

<?

# Подключаем все необходимое
require '...';

Zend_Loader::registerAutoload();
try {
	# Здесь у нас вся работа
} catch (Exception $e) {
	# Отправляем сообщение и выводим сообщения с извинениями и т.д.
	new My_BugReport($e, 'moemilo@gmail.com');
	include './crash.php';
}

В контроллере ErrorController обработка ошибки будет выглядеть примерно так:

switch ($errors->type) {
	case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
	case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
		// 404 error -- controller or action not found
		# Обработка 404 ошибки
		break;
	default:
		new My_BugReport($errors->exception, 'moemilo@gmail.com');
		$this->view->assign('message', $errors->exception->getMessage());
		break;
}

Остается только время от времени проверять почту. Этот способ хорош также для поиска ошибок в рабочей версии сайта. Если, например, Вы знаете где ошибка, но нужно определить ситуацию, при которой она возникает, Вам не нужно будет выводить промежуточные результаты или еще что-либо, переживая не увидит ли этого посетитель, или пытаться повторить ошибку, которую Вы вдруг заметили.