diff --git a/src/ErrorHandler.php b/src/ErrorHandler.php index 436f8d4..a3c3f0b 100644 --- a/src/ErrorHandler.php +++ b/src/ErrorHandler.php @@ -108,4 +108,124 @@ protected function setExceptionTrace(\Exception $exception, array $trace): void $traceReflection->setAccessible(true); $traceReflection->setValue($exception, $trace); } + + public function shouldRenderErrorAsJson(): bool + { + return !empty($_SERVER['HTTP_ACCEPT']) && strcasecmp($_SERVER['HTTP_ACCEPT'], 'application/json') === 0; + } + + /** + * Renders current error information as JSON output. + * This method will display information from current {@see getError()} value. + * + * > Note: this method does NOT terminate the script. + */ + public function renderErrorAsJson(): void + { + $error = $this->getError(); + if (empty($error)) { + return; + } + + unset($error['trace']); + + $responseData = [ + 'error' => $this->getHttpHeader($error['code']), + 'code' => $error['code'], + ]; + + $jsonFlags = 0; + if (YII_DEBUG) { + $jsonFlags = JSON_PRETTY_PRINT; + + $error['traces'] = $this->filterErrorTrace($error['traces']); + + $responseData = array_merge($responseData, $error); + } + + header('Content-Type: application/json; charset=utf-8'); + + echo json_encode($responseData, $jsonFlags); + } + + /** + * @param array $trace raw exception stack trace. + * @return array simplified stack trace. + */ + private function filterErrorTrace(array $trace): array + { + /** @var ErrorTraceFilter $traceFilter */ + $traceFilter = Yii::createComponent([ + 'class' => ErrorTraceFilter::class, + ]); + + return $traceFilter->filter($trace); + } + + /** + * {@inheritDoc} + */ + protected function renderException(): void + { + $exception = $this->getException(); + + if ($this->errorAction !== null) { + Yii::app()->runController($this->errorAction); + + return; + } + + if ($exception instanceof \CHttpException || !YII_DEBUG) { + $this->renderError(); + + return; + } + + if ($this->shouldRenderErrorAsJson()) { + $this->renderErrorAsJson(); + + return; + } + + if ($this->isAjaxRequest()) { + Yii::app()->displayException($exception); + + return; + } + + $this->render('exception', $this->getError()); + } + + /** + * {@inheritDoc} + */ + protected function renderError(): void + { + if ($this->errorAction !== null) { + Yii::app()->runController($this->errorAction); + + return; + } + + if ($this->shouldRenderErrorAsJson()) { + $this->renderErrorAsJson(); + + return; + } + + $data = $this->getError(); + if ($this->isAjaxRequest()) { + Yii::app()->displayError($data['code'], $data['message'], $data['file'], $data['line']); + + return; + } + + if (YII_DEBUG) { + $this->render('exception', $data); + + return; + } + + $this->render('error',$data); + } } \ No newline at end of file diff --git a/src/ErrorTraceFilter.php b/src/ErrorTraceFilter.php new file mode 100644 index 0000000..b1db0aa --- /dev/null +++ b/src/ErrorTraceFilter.php @@ -0,0 +1,97 @@ + + * @since 1.0 + */ +class ErrorTraceFilter extends CComponent +{ + /** + * @var int maximum number of trace source code lines to be displayed. Defaults to 10. + */ + public $maxTraceSourceLines = 10; + + public function filter(array $trace): array + { + $trace = array_slice($trace, 0, $this->maxTraceSourceLines); + + $result = []; + foreach ($trace as $entry) { + if (array_key_exists('args', $entry)) { + $entry['args'] = $this->simplifyArguments($entry['args']); + } + + $result[] = $entry; + } + + return $result; + } + + /** + * Converts arguments array to their simplified representation. + * + * @param array $args arguments array to be converted. + * @return string string representation of the arguments array. + */ + private function simplifyArguments(array $args) : string + { + $count = 0; + + $isAssoc = $args !== array_values($args); + + foreach ($args as $key => $value) { + $count++; + + if ($count >= 5) { + if ($count > 5) { + unset($args[$key]); + } else { + $args[$key] = '...'; + } + + continue; + } + + $args[$key] = $this->simplifyArgument($value); + + if (is_string($key)) { + $args[$key] = "'" . $key . "' => " . $args[$key]; + } elseif ($isAssoc) { + $args[$key] = $key.' => '.$args[$key]; + } + } + + return implode(', ', $args); + } + + /** + * @param mixed $value + * @return mixed + */ + private function simplifyArgument($value) + { + if (is_object($value)) { + return get_class($value); + } elseif (is_bool($value)) { + return $value ? 'true' : 'false'; + } elseif (is_string($value)) { + if (strlen($value) > 64) { + return "'" . substr($value, 0, 64) . "...'"; + } else { + return "'" . $value . "'"; + } + } elseif (is_array($value)) { + return '[' . $this->simplifyArguments($value) . ']'; + } elseif ($value === null) { + return 'null'; + } elseif (is_resource($value)) { + return 'resource'; + } + + return $value; + } +} \ No newline at end of file