Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 33 additions & 126 deletions src/Tracer/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,16 @@

namespace Quantum\Tracer;

use Quantum\Storage\Contracts\LocalFilesystemAdapterInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Quantum\Renderer\Exceptions\RendererException;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\App\Exceptions\BaseException;
use Quantum\View\Factories\ViewFactory;
use Quantum\Di\Exceptions\DiException;
use Quantum\Http\Enums\StatusCode;
use Quantum\Logger\Logger;
use ReflectionException;
use Psr\Log\LogLevel;
use ErrorException;
use ParseError;
use Throwable;
use Exception;

Expand All @@ -38,32 +35,20 @@
*/
class ErrorHandler
{
/**
* Number of lines to be returned
*/
public const NUM_LINES = 10;

/**
* @var array<int, string>
*/
public const ERROR_TYPES = [
E_ERROR => 'error',
E_WARNING => 'warning',
E_PARSE => 'error',
E_NOTICE => 'notice',
E_CORE_ERROR => 'error',
E_CORE_WARNING => 'warning',
E_COMPILE_ERROR => 'error',
E_COMPILE_WARNING => 'warning',
E_USER_ERROR => 'error',
E_USER_WARNING => 'warning',
E_USER_NOTICE => 'notice',
E_STRICT => 'notice',
E_RECOVERABLE_ERROR => 'error',
];

private ?Logger $logger = null;
private ?OutputInterface $cliOutput = null;
private ExceptionSeverityResolver $severityResolver;
private WebExceptionRenderer $webExceptionRenderer;

public function __construct(
?ExceptionSeverityResolver $severityResolver = null,
?StackTraceFormatter $stackTraceFormatter = null,
?WebExceptionRenderer $webExceptionRenderer = null
) {
$this->severityResolver = $severityResolver ?? new ExceptionSeverityResolver();
$formatter = $stackTraceFormatter ?? new StackTraceFormatter();
$this->webExceptionRenderer = $webExceptionRenderer ?? new WebExceptionRenderer($formatter);
}

private function __clone()
{
Expand Down Expand Up @@ -110,30 +95,36 @@ public function handleException(Throwable $throwable): void
private function handleCliException(Throwable $throwable): void
{
$output = $this->cliOutput ?? new ConsoleOutput();
$output->writeln('<error>' . $throwable->getMessage() . '</error>');

if (!is_debug_mode()) {
$output->writeln('<error>' . $throwable->getMessage() . '</error>');
return;
}

$output->writeln('<error>' . get_class($throwable) . ': ' . $throwable->getMessage() . '</error>');
$output->writeln('In ' . $throwable->getFile() . ':' . $throwable->getLine());
$output->writeln($throwable->getTraceAsString());
}

/**
* @throws ConfigException|RendererException|DiException|BaseException|ReflectionException|Exception
* @throws DiException|BaseException|ReflectionException|Exception
*/
private function handleWebException(Throwable $throwable): void
{
$view = ViewFactory::get();
$errorType = $this->getErrorType($throwable);

if (is_debug_mode()) {
$errorPage = $view->renderPartial('errors' . DS . 'trace', [
'stackTrace' => $this->composeStackTrace($throwable),
'errorMessage' => $throwable->getMessage(),
'severity' => ucfirst($errorType),
]);
} else {
$errorPage = $view->renderPartial('errors' . DS . '500');
if (!is_debug_mode()) {
$this->logError($throwable, $errorType);
}

response()->html($errorPage);
response()->send();
try {
$errorPage = $this->webExceptionRenderer->render($throwable, $errorType);
response()->html($errorPage, StatusCode::INTERNAL_SERVER_ERROR);
response()->send();
} catch (Throwable $e) {
response()->html('Internal Server Error', StatusCode::INTERNAL_SERVER_ERROR);
response()->send();
}
}

private function logError(Throwable $e, string $errorType): void
Expand All @@ -147,95 +138,11 @@ private function logError(Throwable $e, string $errorType): void
$this->logger->$logMethod($e->getMessage(), ['trace' => $e->getTraceAsString()]);
}

/**
* Composes the stack trace
* @return array<int, array{file: string, code: string}>
* @throws ConfigException|RendererException|DiException|BaseException|ReflectionException
*/
private function composeStackTrace(Throwable $e): array
{
$trace[] = [
'file' => $e->getFile(),
'code' => $this->getSourceCode($e->getFile(), $e->getLine(), 'error-line'),
];

foreach ($e->getTrace() as $item) {
if (($item['class'] ?? null) === self::class) {
continue;
}

if (isset($item['file'])) {
$trace[] = [
'file' => $item['file'],
'code' => $this->getSourceCode($item['file'], $item['line'] ?? 1, 'switch-line'),
];
}
}

return $trace;
}

/**
* Gets the source code where the error happens
* @throws ConfigException|RendererException|DiException|BaseException|ReflectionException
*/
private function getSourceCode(string $filename, int $lineNumber, string $className): string
{
$lineNumber--;

$halfLines = intdiv(self::NUM_LINES, 2);
$start = max($lineNumber - $halfLines, 1);

$adapter = fs()->getAdapter();

if (!$adapter instanceof LocalFilesystemAdapterInterface) {
return '';
}

$lines = $adapter->getLines($filename, $start, self::NUM_LINES);

$code = '<ol start="' . key($lines) . '">';

foreach ($lines as $currentLineNumber => $line) {
$code .= $this->formatLineItem($currentLineNumber, $line, $lineNumber, $className);
}

return $code . '</ol>';
}

/**
* Formats the line item
*/
private function formatLineItem(int $currentLineNumber, string $line, int $lineNumber, string $className): string
{
$highlightClass = $currentLineNumber === $lineNumber ? " class=\"{$className}\"" : '';

$encodedLine = htmlspecialchars($line, ENT_QUOTES);

return sprintf(
'<li%s><pre>%s</pre></li>',
$highlightClass,
$encodedLine
);
}

/**
* Gets the error type based on the exception class
*/
private function getErrorType(Throwable $e): string
{
if ($e instanceof ErrorException) {
return self::ERROR_TYPES[$e->getSeverity()] ?? LogLevel::ERROR;
}

if ($e instanceof ParseError) {
return LogLevel::CRITICAL;
}

if ($e instanceof ReflectionException) {
return LogLevel::WARNING;
}

return LogLevel::ERROR;
return $this->severityResolver->resolve($e);
}
}
62 changes: 62 additions & 0 deletions src/Tracer/ExceptionSeverityResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/**
* Quantum PHP Framework
*
* An open source software development framework for PHP
*
* @package Quantum
* @author Arman Ag. <arman.ag@softberg.org>
* @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
* @link http://quantum.softberg.org/
* @since 3.0.0
*/

namespace Quantum\Tracer;

use ReflectionException;
use Psr\Log\LogLevel;
use ErrorException;
use ParseError;
use Throwable;

final class ExceptionSeverityResolver
{
/**
* @var array<int, string>
*/
private const ERROR_TYPES = [
E_ERROR => 'error',
E_WARNING => 'warning',
E_PARSE => 'error',
E_NOTICE => 'notice',
E_CORE_ERROR => 'error',
E_CORE_WARNING => 'warning',
E_COMPILE_ERROR => 'error',
E_COMPILE_WARNING => 'warning',
E_USER_ERROR => 'error',
E_USER_WARNING => 'warning',
E_USER_NOTICE => 'notice',
E_STRICT => 'notice',
E_RECOVERABLE_ERROR => 'error',
];

public function resolve(Throwable $throwable): string
{
if ($throwable instanceof ErrorException) {
return self::ERROR_TYPES[$throwable->getSeverity()] ?? LogLevel::ERROR;
}

if ($throwable instanceof ParseError) {
return LogLevel::CRITICAL;
}

if ($throwable instanceof ReflectionException) {
return LogLevel::WARNING;
}

return LogLevel::ERROR;
}
}
83 changes: 83 additions & 0 deletions src/Tracer/StackTraceFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

/**
* Quantum PHP Framework
*
* An open source software development framework for PHP
*
* @package Quantum
* @author Arman Ag. <arman.ag@softberg.org>
* @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
* @link http://quantum.softberg.org/
* @since 3.0.0
*/

namespace Quantum\Tracer;

use Quantum\Storage\Contracts\LocalFilesystemAdapterInterface;
use Throwable;

final class StackTraceFormatter
{
private const NUM_LINES = 10;

/**
* @return array<int, array{file: string, code: string}>
*/
public function compose(Throwable $throwable): array
{
$trace[] = [
'file' => $throwable->getFile(),
'code' => $this->getSourceCode($throwable->getFile(), $throwable->getLine(), 'error-line'),
];

foreach ($throwable->getTrace() as $item) {
if (($item['class'] ?? null) === ErrorHandler::class) {
continue;
}

if (isset($item['file'])) {
$trace[] = [
'file' => $item['file'],
'code' => $this->getSourceCode($item['file'], $item['line'] ?? 1, 'switch-line'),
];
}
}

return $trace;
}

public function getSourceCode(string $filename, int $lineNumber, string $className): string
{
$lineNumber--;

$halfLines = intdiv(self::NUM_LINES, 2);
$start = max($lineNumber - $halfLines, 1);

$adapter = fs()->getAdapter();

if (!$adapter instanceof LocalFilesystemAdapterInterface) {
return '';
}

$lines = $adapter->getLines($filename, $start, self::NUM_LINES);

$code = '<ol start="' . key($lines) . '">';

foreach ($lines as $currentLineNumber => $line) {
$code .= $this->formatLineItem($currentLineNumber, $line, $lineNumber, $className);
}

return $code . '</ol>';
}

public function formatLineItem(int $currentLineNumber, string $line, int $lineNumber, string $className): string
{
$highlightClass = $currentLineNumber === $lineNumber ? " class=\"{$className}\"" : '';
$encodedLine = htmlspecialchars($line, ENT_QUOTES);

return sprintf('<li%s><pre>%s</pre></li>', $highlightClass, $encodedLine);
}
}
Loading
Loading