From 2d5db95149ed711ceca24144aa2208cd4561674b Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 13 Feb 2013 03:51:29 +0400 Subject: [PATCH] More on error handling: - Correct exit code for require errors in CLI mode. It was 0 even in case of error. - Solved handling fatals and displaying custom error template w/o output buffering. - If XDEBUG is available, error screen will display trace (implementation inspired by Kohana and Nette). - Added ErrorException to store/display type of the error and user-friendly messages. - Fatals are still logged in PHP log. - Turned off native display_errors since we're now catching all errors. - Added reserving memory (256kb) . Can be removed safely. In case of removal we're losing only memory exhausted error when allocating last very small chunk of memory. - Properly handled errors in __toString (exception can't be thrown in this case). - In the YII_DEBUG===false mode it's bad to display exception name even in page title. - In the YII_DEBUG===true mode it's still useful to get user-friendly message additionally to exception name. --- framework/base/Application.php | 88 ++++++++++++++++++++++++++++++--------- framework/base/ErrorException.php | 47 +++++++++++++++++++++ framework/base/Exception.php | 3 +- framework/views/error.php | 7 ++-- framework/views/exception.php | 5 ++- 5 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 framework/base/ErrorException.php diff --git a/framework/base/Application.php b/framework/base/Application.php index b0167df..7c06e79 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -97,6 +97,12 @@ class Application extends Module private $_language; /** + * @var string Used to reserve memory for fatal error handler. This memory + * reserve can be removed if it's OK to write to PHP log only in this particular case. + */ + private $_memoryReserve; + + /** * Constructor. * @param string $id the ID of this application. The ID should uniquely identify the application from others. * @param string $basePath the base path of this application. This should point to @@ -110,6 +116,7 @@ class Application extends Module $this->setBasePath($basePath); if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); set_exception_handler(array($this, 'handleException')); set_error_handler(array($this, 'handleError'), error_reporting()); } @@ -126,7 +133,6 @@ class Application extends Module */ public function init() { - ob_start(); $this->preloadComponents(); } @@ -139,27 +145,54 @@ class Application extends Module */ public function end($status = 0, $exit = true) { - $lastError = error_get_last(); - - if(isset($lastError['type']) && in_array($lastError['type'], array(E_ERROR, E_PARSE))) { - ob_end_clean(); - $exception = new \ErrorException($lastError['message'], 0, $lastError['type'], $lastError['file'], $lastError['line']); - $this->logException($exception); - - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } - - die(1); - } - if (!$this->_ended) { $this->_ended = true; $this->afterRequest(); } - ob_end_flush(); + + if(YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); + + if(isset($error['type']) && in_array($error['type'], ErrorException::getFatalCodes())) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + + if(function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); + foreach($trace as &$frame) { + if(!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } + + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if(!isset($frame['type'])) { + $frame['type'] = '::'; + } + + // XDebug has a different key name + $frame['args'] = array(); + if(isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } + + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($exception, $trace); + } + + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + $status = 1; + } + } + if ($exit) { exit($status); } @@ -173,6 +206,9 @@ class Application extends Module public function run() { $this->beforeRequest(); + // Allocating twice more than required to display memory exhausted error + // in case of trying to allocate last 1 byte while all memory is taken. + $this->_memoryReserve = str_repeat('x', 1024*256); register_shutdown_function(array($this,'end'),0,false); $status = $this->processRequest(); $this->afterRequest(); @@ -394,12 +430,24 @@ class Application extends Module * @param string $message the error message * @param string $file the filename that the error was raised in * @param integer $line the line number the error was raised at - * @throws \ErrorException the error exception + * + * @throws ErrorException */ public function handleError($code, $message, $file, $line) { if (error_reporting() !== 0) { - throw new \ErrorException($message, 0, $code, $file, $line); + $exception = new ErrorException($message, $code, $code, $file, $line); + + // in case error appeared in __toString method we can't throw any exception + $trace = debug_backtrace(false); + array_shift($trace); + foreach($trace as $frame) { + if($frame['function'] == '__toString') { + $this->handleException($exception); + } + } + + throw $exception; } } diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php new file mode 100644 index 0000000..073c1c3 --- /dev/null +++ b/framework/base/ErrorException.php @@ -0,0 +1,47 @@ + + * @since 2.0 + */ +class ErrorException extends \ErrorException +{ + public static function getFatalCodes() + { + return array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING); + } + + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + $names = array( + E_ERROR => \Yii::t('yii|Fatal Error'), + E_PARSE => \Yii::t('yii|Parse Error'), + E_CORE_ERROR => \Yii::t('yii|Core Error'), + E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), + E_USER_ERROR => \Yii::t('yii|User Error'), + E_WARNING => \Yii::t('yii|Warning'), + E_CORE_WARNING => \Yii::t('yii|Core Warning'), + E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), + E_USER_WARNING => \Yii::t('yii|User Warning'), + E_STRICT => \Yii::t('yii|Strict'), + E_NOTICE => \Yii::t('yii|Notice'), + E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), + E_DEPRECATED => \Yii::t('yii|Deprecated'), + ); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); + } +} diff --git a/framework/base/Exception.php b/framework/base/Exception.php index 0088a55..7dcd7c5 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -24,5 +24,4 @@ class Exception extends \Exception { return \Yii::t('yii|Exception'); } -} - +} \ No newline at end of file diff --git a/framework/views/error.php b/framework/views/error.php index c1700f2..28c50f1 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -4,12 +4,13 @@ * @var \yii\base\ErrorHandler $owner */ $owner = $this->owner; +$title = $owner->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); ?> - <?php echo get_class($exception)?> + <?php echo $title?>