Skip to content

Commit

Permalink
Refactored exception/error handlers. (#322)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbankowski authored Dec 11, 2023
1 parent be9a039 commit 1369123
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 74 deletions.
6 changes: 4 additions & 2 deletions src/Ouzo/Core/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use InvalidArgumentException;
use Ouzo\Config\ConfigRepository;
use Ouzo\ExceptionHandling\DebugErrorHandler;
use Ouzo\ExceptionHandling\DebugExceptionHandler;
use Ouzo\ExceptionHandling\ErrorHandler;
use Ouzo\ExceptionHandling\ExceptionHandler;
use Ouzo\Injection\Injector;
use Ouzo\Injection\InjectorConfig;
use Ouzo\Injection\Scope;
Expand Down Expand Up @@ -109,15 +111,15 @@ public function runApplication(): FrontController
private function registerErrorHandlers(): void
{
if (Config::getValue('debug')) {
(new DebugErrorHandler())->register();
(new DebugErrorHandler(new DebugExceptionHandler()))->register();
return;
}

if (!is_null($this->errorHandler)) {
$this->errorHandler->register();
return;
}
(new ErrorHandler())->register();
(new ErrorHandler(new ExceptionHandler()))->register();
}

private function includeRoutes(): void
Expand Down
25 changes: 10 additions & 15 deletions src/Ouzo/Core/ExceptionHandling/DebugErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,22 @@

class DebugErrorHandler extends ErrorHandler
{
protected static function getRun(): Run
public function handleError(int $errorNumber, string $errorString, string $errorFile, int $errorLine): void
{
error_reporting(E_ALL);
$run = new Run();
$run->pushHandler(new PrettyPageHandler());
$run->pushHandler(new DebugErrorLogHandler());
return $run;
$this->createWhoops()->handleError($errorNumber, $errorString, $errorFile, $errorLine);
}

protected static function getExceptionHandler(): ExceptionHandler
public function handleShutdown(): void
{
return new DebugExceptionHandler();
$this->createWhoops()->handleShutdown();
}

public static function errorHandler(int $errorNumber, string $errorString, string $errorFile, int $errorLine): void
private function createWhoops(): Run
{
self::getRun()->handleError($errorNumber, $errorString, $errorFile, $errorLine);
}

public static function shutdownHandler(): void
{
self::getRun()->handleShutdown();
error_reporting(E_ALL);
$run = new Run();
$run->pushHandler(new PrettyPageHandler());
$run->pushHandler(new DebugErrorLogHandler());
return $run;
}
}
9 changes: 5 additions & 4 deletions src/Ouzo/Core/ExceptionHandling/DebugExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@

namespace Ouzo\ExceptionHandling;

use Ouzo\Http\MediaType;
use Ouzo\Response\ResponseTypeResolve;
use Ouzo\Uri;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run;

class DebugExceptionHandler extends ExceptionHandler
{
public function runDefaultHandler($exception)
public function runDefaultHandler($exception): void
{
if ($this->needPrettyHandler()) {
if ($this->isPrettyHandlerNeeded()) {
$run = new Run();
$run->pushHandler(new PrettyPageHandler());
$run->pushHandler(new DebugErrorLogHandler());
Expand All @@ -25,9 +26,9 @@ public function runDefaultHandler($exception)
}
}

private function needPrettyHandler(): bool
private function isPrettyHandlerNeeded(): bool
{
$isHtmlResponse = ResponseTypeResolve::resolve() == "text/html";
$isHtmlResponse = ResponseTypeResolve::resolve() === MediaType::TEXT_HTML;
return $isHtmlResponse && !Uri::isAjax();
}
}
33 changes: 16 additions & 17 deletions src/Ouzo/Core/ExceptionHandling/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,47 @@

class ErrorHandler
{
public function __construct(private readonly ExceptionHandler $exceptionHandler)
{
}

public function register(): void
{
set_exception_handler(fn(Throwable $exception) => static::exceptionHandler($exception));
set_error_handler(fn(...$args) => static::errorHandler(...$args), E_ALL & ~E_DEPRECATED & ~E_STRICT);
register_shutdown_function(fn() => static::shutdownHandler());
set_exception_handler(fn(Throwable $exception) => $this->handleException($exception));
set_error_handler(fn(...$args) => $this->handleError(...$args), E_ALL & ~E_DEPRECATED & ~E_STRICT);
register_shutdown_function(fn() => $this->handleShutdown());
}

public static function exceptionHandler(Throwable $exception): void
public function handleException(Throwable $exception): void
{
static::getExceptionHandler()->handleException($exception);
$this->exceptionHandler->handleException($exception);
}

public static function errorHandler(int $errorNumber, string $errorString, string $errorFile, int $errorLine): void
public function handleError(int $errorNumber, string $errorString, string $errorFile, int $errorLine): void
{
if (self::stopsExecution($errorNumber)) {
self::exceptionHandler(new ErrorException($errorString, $errorNumber, $errorNumber, $errorFile, $errorLine));
if ($this->stopsExecution($errorNumber)) {
$this->handleException(new ErrorException($errorString, $errorNumber, $errorNumber, $errorFile, $errorLine));
} else {
throw new ErrorException($errorString, $errorNumber, $errorNumber, $errorFile, $errorLine);
}
}

public static function stopsExecution($errno): bool
public function stopsExecution($errno): bool
{
return match ($errno) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => true,
default => false
};
}

protected static function getExceptionHandler(): ExceptionHandler
{
return new ExceptionHandler();
}

public static function shutdownHandler(): void
public function handleShutdown(): void
{
$error = error_get_last();

if (!ExceptionHandler::lastErrorHandled() && $error && $error['type'] & (E_ERROR | E_USER_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_RECOVERABLE_ERROR)) {
if (!$this->exceptionHandler->lastErrorHandled() && $error && $error['type'] & (E_ERROR | E_USER_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_RECOVERABLE_ERROR)) {
$stackTrace = new StackTrace($error['file'], $error['line']);
$exceptionData = new OuzoExceptionData(500, [new Error(0, $error['message'])], $stackTrace, [], null, $error['type']);
static::getExceptionHandler()->handleExceptionData($exceptionData);
$this->exceptionHandler->handleExceptionData($exceptionData);
}
}
}
7 changes: 2 additions & 5 deletions src/Ouzo/Core/ExceptionHandling/ErrorRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,23 @@ class ErrorRenderer implements Renderer
{
public function render(OuzoExceptionData $exceptionData, ?string $viewName): void
{
/** @noinspection PhpUnusedLocalVariableInspection */
$errorMessage = $exceptionData->getMessage();
/** @noinspection PhpUnusedLocalVariableInspection */
$errorTrace = $exceptionData->getStackTrace()->getTraceAsString();

$this->clearOutputBuffers();
header($exceptionData->getHeader());
$responseType = ResponseTypeResolve::resolve();
header('Content-type: ' . $responseType);
header("Content-type: {$responseType}");

$additionalHeaders = $exceptionData->getAdditionalHeaders();
array_walk($additionalHeaders, function ($header) {
header($header);
});

/** @noinspection PhpIncludeInspection */
require(ViewPathResolver::resolveViewPath($viewName, $responseType));
}

private function clearOutputBuffers()
private function clearOutputBuffers(): void
{
while (ob_get_level()) {
if (!ob_end_clean()) {
Expand Down
47 changes: 23 additions & 24 deletions src/Ouzo/Core/ExceptionHandling/ExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,74 +13,75 @@

class ExceptionHandler
{
private static bool $errorHandled = false;
private static bool $isCli = false;
public static ?Renderer $errorRenderer = null;
private bool $errorHandled = false;
private bool $isCli;
private Renderer $errorRenderer;

public static function setupErrorRenderer()
public function __construct(?Renderer $errorRenderer = null)
{
global $argv;
self::$isCli = isset($argv[0]);
self::$errorRenderer = self::$isCli ? new CliErrorRenderer() : new ErrorRenderer();
$this->isCli = isset($argv[0]);
$this->errorRenderer = is_null($errorRenderer) ? ($this->isCli ? new CliErrorRenderer() : new ErrorRenderer()) : $errorRenderer;
}

public function handleException($exception)
public function handleException($exception): void
{
if (!$this->runOuzoExceptionHandler($exception)) {
$this->runDefaultHandler($exception);
}
}

protected function runOuzoExceptionHandler($exception)
protected function runOuzoExceptionHandler($exception): bool
{
if ($exception instanceof UserException) {
$this->renderUserError(OuzoExceptionData::forException(500, $exception));
return true;
} elseif ($exception instanceof RouterException) {
}
if ($exception instanceof RouterException) {
$this->handleError(OuzoExceptionData::forException(404, $exception));
return true;
} elseif ($exception instanceof OuzoException) {
}
if ($exception instanceof OuzoException) {
$this->handleError($exception->asExceptionData());
return true;
}
return false;
}

protected function runDefaultHandler($exception)
protected function runDefaultHandler($exception): void
{
$this->handleError(OuzoExceptionData::forException(500, $exception));
}

public function handleExceptionData(OuzoExceptionData $exceptionData)
public function handleExceptionData(OuzoExceptionData $exceptionData): void
{
$this->handleError($exceptionData);
}

public static function lastErrorHandled()
public function lastErrorHandled(): bool
{
return self::$errorHandled;
return $this->errorHandled;
}

protected function handleError($exception)
protected function handleError($exception): void
{
$this->renderError($exception);
}

private function renderUserError($exception)
private function renderUserError($exception): void
{
if (!self::$isCli) {
header("Contains-Error-Message: User");
if (!$this->isCli) {
header('Contains-Error-Message: User');
}
$this->renderError($exception, 'user_exception');
}

protected function renderError(OuzoExceptionData $exceptionData, $viewName = 'exception')
protected function renderError(OuzoExceptionData $exceptionData, $viewName = 'exception'): void
{
try {
ExceptionLogger::newInstance($exceptionData)->log();
$renderer = self::$errorRenderer ?: new ErrorRenderer();
$renderer->render($exceptionData, $viewName);
self::$errorHandled = true;
$this->errorRenderer->render($exceptionData, $viewName);
$this->errorHandled = true;
} catch (Exception $e) {
echo "Framework critical error. Exception thrown in exception handler.<br>\n";
ExceptionLogger::forException($e)->log();
Expand All @@ -92,5 +93,3 @@ protected function renderError(OuzoExceptionData $exceptionData, $viewName = 'ex
}
}
}

ExceptionHandler::setupErrorRenderer();
9 changes: 6 additions & 3 deletions test/src/Ouzo/Core/ExceptionHandling/ErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use Ouzo\PageNotFoundException;
use Ouzo\Tests\Mock\Mock;
use Ouzo\Tests\Mock\MockInterface;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

Expand All @@ -18,12 +19,14 @@ public function shouldRender404OnRouterException()
{
//given
$pageNotFoundException = new PageNotFoundException();
ExceptionHandler::$errorRenderer = Mock::mock(ErrorRenderer::class);
/** @var Renderer|MockInterface $renderer */
$renderer = Mock::create(ErrorRenderer::class);
$handler = new ErrorHandler(new ExceptionHandler($renderer));

//when
ErrorHandler::exceptionHandler($pageNotFoundException);
$handler->handleException($pageNotFoundException);

//then
Mock::verify(ExceptionHandler::$errorRenderer)->render(Mock::any(), "exception");
Mock::verify($renderer)->render(Mock::any(), 'exception');
}
}
11 changes: 7 additions & 4 deletions test/src/Ouzo/Core/ExceptionHandling/ExceptionHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

namespace Ouzo\ExceptionHandling;

use Exception;
use Ouzo\Tests\CatchException;
use Ouzo\Tests\Mock\Mock;
use Ouzo\Tests\Mock\MockInterface;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

Expand All @@ -17,15 +19,16 @@ class ExceptionHandlerTest extends TestCase
public function shouldHandleException()
{
//given
$exception = new \Exception("Some exception");
ExceptionHandler::$errorRenderer = Mock::mock(ErrorRenderer::class);
$handler = new ExceptionHandler();
$exception = new Exception('Some exception');
/** @var Renderer|MockInterface $renderer */
$renderer = Mock::create(ErrorRenderer::class);
$handler = new ExceptionHandler($renderer);

//when
CatchException::when($handler)->handleException($exception);

//then
CatchException::assertThat()->notCaught();
Mock::verify(ExceptionHandler::$errorRenderer)->render(OuzoExceptionData::forException(500, $exception), "exception");
Mock::verify($renderer)->render(OuzoExceptionData::forException(500, $exception), 'exception');
}
}

0 comments on commit 1369123

Please sign in to comment.