feat: implement exception handling system with error context and policies
This commit is contained in:
37
src/Framework/ExceptionHandling/ErrorContext.php
Normal file
37
src/Framework/ExceptionHandling/ErrorContext.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
final readonly class ErrorContext
|
||||
{
|
||||
public function __construct(
|
||||
public int $severity,
|
||||
public string $message,
|
||||
public ?string $file = null,
|
||||
public ?int $line = null,
|
||||
public bool $isSuppressed = false,
|
||||
) {}
|
||||
|
||||
public static function create(
|
||||
int $severity,
|
||||
string $message,
|
||||
?string $file = null,
|
||||
?int $line = null,
|
||||
bool $isSuppressed = false,
|
||||
): self {
|
||||
return new self($severity, $message, $file, $line, $isSuppressed);
|
||||
}
|
||||
|
||||
public function isDeprecation(): bool
|
||||
{
|
||||
return $this->severity === E_DEPRECATED || $this->severity === E_USER_DEPRECATED;
|
||||
}
|
||||
|
||||
public function isFatal(): bool
|
||||
{
|
||||
return in_array($this->severity, [E_ERROR, E_RECOVERABLE_ERROR, E_USER_ERROR], true);
|
||||
}
|
||||
}
|
||||
15
src/Framework/ExceptionHandling/ErrorDecision.php
Normal file
15
src/Framework/ExceptionHandling/ErrorDecision.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
enum ErrorDecision
|
||||
{
|
||||
/** Fehler selbst behandelt (kein PHP-Standard mehr) */
|
||||
case HANDLED;
|
||||
|
||||
/** An PHP-Standard weitergeben */
|
||||
case DEFER;
|
||||
|
||||
/** Eskalieren als Exception */
|
||||
case THROW;
|
||||
}
|
||||
48
src/Framework/ExceptionHandling/ErrorHandler.php
Normal file
48
src/Framework/ExceptionHandling/ErrorHandler.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\ExceptionHandling\Strategy\StrictErrorPolicy;
|
||||
use ErrorException;
|
||||
|
||||
final readonly class ErrorHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ErrorHandlerStrategy $strategy = new StrictErrorPolicy,
|
||||
)
|
||||
{}
|
||||
|
||||
|
||||
/**
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function handle(
|
||||
int $severity,
|
||||
string $message,
|
||||
?string $file = null,
|
||||
?int $line = null,
|
||||
): bool {
|
||||
|
||||
$context = ErrorContext::create(
|
||||
severity : $severity,
|
||||
message : $message,
|
||||
file : $file,
|
||||
line : $line,
|
||||
isSuppressed: $this->isSuppressed($severity)
|
||||
);
|
||||
|
||||
$decision = $this->strategy->handle($context);
|
||||
|
||||
return match($decision) {
|
||||
ErrorDecision::HANDLED => true,
|
||||
ErrorDecision::DEFER => false,
|
||||
ErrorDecision::THROW => throw new ErrorException($message, 0, $severity, $file, $line),
|
||||
};
|
||||
}
|
||||
|
||||
private function isSuppressed($severity): bool
|
||||
{
|
||||
return !(error_reporting() & $severity);
|
||||
}
|
||||
}
|
||||
10
src/Framework/ExceptionHandling/ErrorHandlerStrategy.php
Normal file
10
src/Framework/ExceptionHandling/ErrorHandlerStrategy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
interface ErrorHandlerStrategy
|
||||
{
|
||||
public function handle(ErrorContext $context): ErrorDecision;
|
||||
}
|
||||
25
src/Framework/ExceptionHandling/ErrorKernel.php
Normal file
25
src/Framework/ExceptionHandling/ErrorKernel.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\ExceptionHandling\Reporter\LogReporter;
|
||||
use Throwable;
|
||||
|
||||
final readonly class ErrorKernel
|
||||
{
|
||||
public function __construct(
|
||||
private ErrorRendererFactory $rendererFactory = new ErrorRendererFactory,
|
||||
) {}
|
||||
public function handle(Throwable $e, array $context = []): mixed
|
||||
{
|
||||
|
||||
$log = new LogReporter();
|
||||
$log->report($e->getMessage());
|
||||
|
||||
$this->rendererFactory->getRenderer()->render();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
8
src/Framework/ExceptionHandling/ErrorRenderer.php
Normal file
8
src/Framework/ExceptionHandling/ErrorRenderer.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
interface ErrorRenderer
|
||||
{
|
||||
public function render(): void;
|
||||
}
|
||||
13
src/Framework/ExceptionHandling/ErrorRendererFactory.php
Normal file
13
src/Framework/ExceptionHandling/ErrorRendererFactory.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\ExceptionHandling\Renderer\HtmlErrorRenderer;
|
||||
final class ErrorRendererFactory
|
||||
{
|
||||
public function getRenderer():ErrorRenderer
|
||||
{
|
||||
return new HtmlErrorRenderer();
|
||||
}
|
||||
}
|
||||
53
src/Framework/ExceptionHandling/ErrorScope.php
Normal file
53
src/Framework/ExceptionHandling/ErrorScope.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\DI\Initializer;
|
||||
use Fiber;
|
||||
|
||||
final class ErrorScope
|
||||
{
|
||||
private array $stack = [];
|
||||
|
||||
#[Initializer]
|
||||
public static function initialize(): ErrorScope
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
public function enter(ErrorScopeContext $context): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$this->stack[$id] ??= [];
|
||||
$this->stack[$id][] = $context;
|
||||
return count($this->stack[$id]);
|
||||
}
|
||||
|
||||
public function current(): ?ErrorScopeContext
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$stack = $this->stack[$id] ?? [];
|
||||
return end($stack) ?? null;
|
||||
}
|
||||
|
||||
public function leave(int $token): void
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
if(!isset($this->stack[$id])) {
|
||||
return;
|
||||
}
|
||||
while(!empty($this->stack[$id]) && count($this->stack[$id]) >= $token) {
|
||||
array_pop($this->stack[$id]);
|
||||
}
|
||||
if(empty($this->stack[$id])) {
|
||||
unset($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
private function fiberId(): int
|
||||
{
|
||||
$fiber = Fiber::getCurrent();
|
||||
return $fiber ? spl_object_id($fiber) : 0;
|
||||
}
|
||||
}
|
||||
9
src/Framework/ExceptionHandling/ErrorScopeContext.php
Normal file
9
src/Framework/ExceptionHandling/ErrorScopeContext.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
final class ErrorScopeContext
|
||||
{
|
||||
|
||||
}
|
||||
10
src/Framework/ExceptionHandling/ExceptionHandler.php
Normal file
10
src/Framework/ExceptionHandling/ExceptionHandler.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionHandler
|
||||
{
|
||||
public function handle(Throwable $throwable): void;
|
||||
}
|
||||
35
src/Framework/ExceptionHandling/ExceptionHandlerManager.php
Normal file
35
src/Framework/ExceptionHandling/ExceptionHandlerManager.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\Config\EnvironmentType;
|
||||
use App\Framework\ExceptionHandling\Strategy\ErrorPolicyResolver;
|
||||
|
||||
final readonly class ExceptionHandlerManager
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$resolver = new ErrorPolicyResolver();
|
||||
$this->registerErrorHandler(new ErrorHandler($resolver->resolve(EnvironmentType::DEV)));
|
||||
|
||||
$this->registerExceptionHandler(new GlobalExceptionHandler());
|
||||
|
||||
$this->registerShutdownHandler(new ShutdownHandler());
|
||||
}
|
||||
|
||||
public function registerExceptionHandler(ExceptionHandler $handler): void
|
||||
{
|
||||
set_exception_handler($handler->handle(...));
|
||||
}
|
||||
|
||||
private function registerErrorHandler(ErrorHandler $handler):void
|
||||
{
|
||||
set_error_handler($handler->handle(...), E_ALL);
|
||||
}
|
||||
|
||||
public function registerShutdownHandler(ShutdownHandler $handler): void
|
||||
{
|
||||
register_shutdown_function($handler->handle(...));
|
||||
}
|
||||
}
|
||||
14
src/Framework/ExceptionHandling/GlobalExceptionHandler.php
Normal file
14
src/Framework/ExceptionHandling/GlobalExceptionHandler.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
final readonly class GlobalExceptionHandler implements ExceptionHandler
|
||||
{
|
||||
public function handle(\Throwable $throwable): void
|
||||
{
|
||||
$kernel = new ErrorKernel();
|
||||
|
||||
$kernel->handle($throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderer;
|
||||
|
||||
use App\Framework\ExceptionHandling\ErrorRenderer;
|
||||
|
||||
final class HtmlErrorRenderer implements ErrorRenderer
|
||||
{
|
||||
public function render(): void
|
||||
{
|
||||
echo '<html lang="en"><body><h1>500 Internal Server Error</h1></body></html>';
|
||||
}
|
||||
}
|
||||
12
src/Framework/ExceptionHandling/Reporter/LogReporter.php
Normal file
12
src/Framework/ExceptionHandling/Reporter/LogReporter.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Reporter;
|
||||
|
||||
final class LogReporter implements Reporter
|
||||
{
|
||||
public function report(string $message): void
|
||||
{
|
||||
echo ("[log] " . $message);
|
||||
}
|
||||
}
|
||||
8
src/Framework/ExceptionHandling/Reporter/Reporter.php
Normal file
8
src/Framework/ExceptionHandling/Reporter/Reporter.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Reporter;
|
||||
|
||||
interface Reporter
|
||||
{
|
||||
public function report(string $message): void;
|
||||
}
|
||||
56
src/Framework/ExceptionHandling/ShutdownHandler.php
Normal file
56
src/Framework/ExceptionHandling/ShutdownHandler.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorHandlerManager;
|
||||
use App\Framework\ExceptionHandling\Strategy\StrictErrorPolicy;
|
||||
use Error;
|
||||
|
||||
final readonly class ShutdownHandler
|
||||
{
|
||||
private const array FATAL_TYPES = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$last = error_get_last();
|
||||
|
||||
if (!$last || !$this->isFatalError($last['type'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->cleanOutputBuffer();
|
||||
|
||||
$file = (string)($last['file'] ?? 'unknown');
|
||||
$line = (int)($last['line'] ?? 0);
|
||||
|
||||
$error = new Error($last['message'] ?? 'Fatal error',0);
|
||||
|
||||
|
||||
|
||||
try {
|
||||
$ehm = new ErrorKernel();
|
||||
$ehm->handle($error, ['file' => $file, 'line' => $line]);
|
||||
} catch (\Throwable) {
|
||||
|
||||
}
|
||||
|
||||
exit(255);
|
||||
}
|
||||
|
||||
private function cleanOutputBuffer(): void
|
||||
{
|
||||
try {
|
||||
while (ob_get_level() > 0) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private function isFatalError(?int $type = null): bool
|
||||
{
|
||||
return in_array($type ?? 0, self::FATAL_TYPES, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Strategy;
|
||||
|
||||
use App\Framework\Config\EnvironmentType;
|
||||
use App\Framework\ExceptionHandling\ErrorHandlerStrategy;
|
||||
|
||||
final readonly class ErrorPolicyResolver
|
||||
{
|
||||
public function resolve(EnvironmentType $environmentType): ErrorHandlerStrategy
|
||||
{
|
||||
return match(true) {
|
||||
$environmentType->isProduction() => new StrictErrorPolicy(),
|
||||
$environmentType->isDevelopment() => new StrictErrorPolicy(),
|
||||
default => new StrictErrorPolicy(),
|
||||
};
|
||||
}
|
||||
}
|
||||
45
src/Framework/ExceptionHandling/Strategy/LenientPolicy.php
Normal file
45
src/Framework/ExceptionHandling/Strategy/LenientPolicy.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Strategy;
|
||||
|
||||
use App\Framework\ExceptionHandling\ErrorContext;
|
||||
use App\Framework\ExceptionHandling\ErrorDecision;
|
||||
use App\Framework\ExceptionHandling\ErrorHandlerStrategy;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\ValueObjects\LogContext;
|
||||
use ErrorException;
|
||||
|
||||
final readonly class LenientPolicy implements ErrorHandlerStrategy
|
||||
{
|
||||
public function __construct(private Logger $logger)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handle(ErrorContext $context): ErrorDecision
|
||||
{
|
||||
if($context->isDeprecation()) {
|
||||
$this->logger->notice("[Deprecation] {$context->message}",
|
||||
LogContext::withData(
|
||||
[
|
||||
'file' => $context->file,
|
||||
'line' => $context->line]
|
||||
));
|
||||
|
||||
return ErrorDecision::HANDLED;
|
||||
}
|
||||
|
||||
if($context->isFatal()) {
|
||||
throw new ErrorException(
|
||||
$context->message,
|
||||
0,
|
||||
$context->severity,
|
||||
$context->file,
|
||||
$context->line
|
||||
);
|
||||
}
|
||||
|
||||
return ErrorDecision::DEFER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Strategy;
|
||||
|
||||
use App\Framework\ExceptionHandling\ErrorContext;
|
||||
use App\Framework\ExceptionHandling\ErrorDecision;
|
||||
use App\Framework\ExceptionHandling\ErrorHandlerStrategy;
|
||||
|
||||
final class SilentErrorPolicy implements ErrorHandlerStrategy
|
||||
{
|
||||
public function handle(ErrorContext $context): ErrorDecision
|
||||
{
|
||||
return ErrorDecision::DEFER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Strategy;
|
||||
|
||||
use App\Framework\ExceptionHandling\ErrorContext;
|
||||
use App\Framework\ExceptionHandling\ErrorDecision;
|
||||
use App\Framework\ExceptionHandling\ErrorHandlerStrategy;
|
||||
use ErrorException;
|
||||
|
||||
final readonly class StrictErrorPolicy implements ErrorHandlerStrategy
|
||||
{
|
||||
/**
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function handle(ErrorContext $context): ErrorDecision
|
||||
{
|
||||
throw new ErrorException($context->message, 0, $context->severity, $context->file, $context->line);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user