feat: implement exception handling system with error context and policies
This commit is contained in:
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `src/` layers PHP code (`Application`, `Domain`, `Framework`, `Infrastructure`); create new classes under the matching namespace and keep adapters in Infrastructure.
|
||||
- `resources/` stores Vite-managed frontend code (`resources/js`, `resources/css`), while build artifacts belong in `public/assets` under the `public/` web root.
|
||||
- `tests/` mirrors the PHP namespaces and houses Playwright suites in `tests/e2e`; share fixtures and stubs through `tests/Support`.
|
||||
- Operational docs and tooling sit in `docs/`, `docker/`, and `deployment/`; update those paths whenever runtime behaviour changes.
|
||||
|
||||
## Build, Test & Development Commands
|
||||
- `make up` / `make down` manage the Docker stack; add `make logs` when you need container output.
|
||||
- `npm run dev` starts Vite locally, and `npm run build` produces optimized bundles into `public/assets`.
|
||||
- `make test` drives the full Pest suite; use `make test-unit`, `make test-domain`, or `make test-coverage` for focused runs.
|
||||
- JavaScript and browser checks run via `npm run test`, `npm run test:e2e`, and `npm run test:coverage`.
|
||||
- Quality gates: `make phpstan`, `make cs` (dry run), `make cs-fix` (auto-fix), plus `npm run lint:js` and `npm run format`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Follow PSR-12 with strict types; prefer constructor injection and suffix integrations with their role (`MailerAdapter`, `RedisCache`).
|
||||
- Keep PHP objects immutable unless Domain logic requires mutation; colocate interfaces with their consuming layer.
|
||||
- Use 4-space indents in PHP and Prettier defaults for TypeScript; name frontend components in PascalCase and utilities in camelCase.
|
||||
|
||||
## Testing Guidelines
|
||||
- Add Pest specs beside the feature namespace with expressive names (`it_handles_invalid_tokens`); keep builders in `tests/Support`.
|
||||
- Mock external services through `tests/__mocks__`; rely on Pest datasets for edge cases.
|
||||
- Sync Playwright specs with UX changes, reuse `tests/e2e/fixtures`, and review reports via `playwright show-report`.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Use Conventional Commits (`fix:`, `feat:`, optional scope) to match history.
|
||||
- PRs must outline the change, list executed checks, and link issues; attach screenshots for UI work or config diffs for ops updates.
|
||||
- Confirm CI covers Pest, Jest, Playwright, PHPStan, and php-cs-fixer before requesting review.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Never commit `.env` or secrets; follow `ENV_SETUP.md` and store deployment credentials in Vault.
|
||||
- Run `make security-check` ahead of releases and reflect infrastructure changes in `docs/deployment/`.
|
||||
@@ -20,6 +20,8 @@ use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\ErrorHandling\CliErrorHandler;
|
||||
use App\Framework\ErrorHandling\ErrorHandler;
|
||||
use App\Framework\ErrorHandling\ErrorHandlerManager;
|
||||
use App\Framework\ExceptionHandling\ExceptionHandlerManager;
|
||||
use App\Framework\Http\MiddlewareManager;
|
||||
use App\Framework\Http\MiddlewareManagerInterface;
|
||||
use App\Framework\Http\ResponseEmitter;
|
||||
@@ -71,7 +73,7 @@ final readonly class AppBootstrapper
|
||||
|
||||
//if ($envType->isDevelopment()) {
|
||||
|
||||
if($typedConfig->app->type->isDevelopment()) {
|
||||
/*if($typedConfig->app->type->isDevelopment()) {
|
||||
// Fehleranzeige für die Entwicklung aktivieren
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
@@ -87,7 +89,7 @@ final readonly class AppBootstrapper
|
||||
|
||||
return false;
|
||||
}, true, true);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
public function bootstrapWeb(): ApplicationInterface
|
||||
@@ -157,7 +159,11 @@ final readonly class AppBootstrapper
|
||||
|
||||
private function registerWebErrorHandler(): void
|
||||
{
|
||||
$this->container->get(ErrorHandler::class)->register();
|
||||
new ExceptionHandlerManager();
|
||||
#var_dump('registerWebErrorHandler');
|
||||
#$eh = $this->container->get(ErrorHandlerManager::class);
|
||||
#$eh->register();
|
||||
//$this->container->get(ErrorHandler::class)->register();
|
||||
}
|
||||
|
||||
private function registerCliErrorHandler(): void
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Framework\DI;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use Stringable;
|
||||
|
||||
interface Container
|
||||
{
|
||||
@@ -17,17 +18,26 @@ interface Container
|
||||
* @param class-string<T>|ClassName $class
|
||||
* @return T
|
||||
*/
|
||||
public function get(string|ClassName $class): object;
|
||||
public function get(ClassName|Stringable|string $class): object;
|
||||
|
||||
/** @param class-string $class */
|
||||
public function has(string $class): bool;
|
||||
public function has(ClassName|Stringable|string $class): bool;
|
||||
|
||||
public function bind(string $abstract, callable|string|object $concrete): void;
|
||||
public function bind(
|
||||
ClassName|Stringable|string $abstract,
|
||||
callable|string|object $concrete
|
||||
): void;
|
||||
|
||||
public function singleton(string $abstract, callable|string|object $concrete): void;
|
||||
public function singleton(
|
||||
ClassName|Stringable|string $abstract,
|
||||
callable|string|object $concrete
|
||||
): void;
|
||||
|
||||
public function instance(string $abstract, object $instance): void;
|
||||
public function instance(
|
||||
ClassName|Stringable|string $abstract,
|
||||
object $instance
|
||||
): void;
|
||||
|
||||
/** @param class-string $class */
|
||||
public function forget(string $class): void;
|
||||
public function forget(ClassName|Stringable|string $class): void;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,17 @@ declare(strict_types=1);
|
||||
namespace App\Framework\DI;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\DI\Exceptions\ClassNotInstantiable;
|
||||
use App\Framework\DI\Exceptions\ClassNotResolvableException;
|
||||
use App\Framework\DI\Exceptions\ClassResolutionException;
|
||||
use App\Framework\DI\Exceptions\ContainerException;
|
||||
use App\Framework\DI\Exceptions\CyclicDependencyException;
|
||||
use App\Framework\DI\Exceptions\LazyLoadingException;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Metrics\FrameworkMetricsCollector;
|
||||
use App\Framework\Reflection\CachedReflectionProvider;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
use Stringable;
|
||||
use Throwable;
|
||||
|
||||
final class DefaultContainer implements Container
|
||||
@@ -54,8 +60,9 @@ final class DefaultContainer implements Container
|
||||
$this->instance(ReflectionProvider::class, $this->reflectionProvider);
|
||||
}
|
||||
|
||||
public function bind(string $abstract, callable|string|object $concrete): void
|
||||
public function bind(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
|
||||
{
|
||||
$abstract = (string) $abstract;
|
||||
$this->bindings->bind($abstract, $concrete);
|
||||
|
||||
// Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
|
||||
@@ -64,23 +71,26 @@ final class DefaultContainer implements Container
|
||||
}
|
||||
}
|
||||
|
||||
public function singleton(string $abstract, callable|string|object $concrete): void
|
||||
public function singleton(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
|
||||
{
|
||||
$abstract = (string) $abstract;
|
||||
$this->bind($abstract, $concrete);
|
||||
$this->instances->markAsSingleton($abstract);
|
||||
}
|
||||
|
||||
public function instance(string $abstract, object $instance): void
|
||||
public function instance(ClassName|Stringable|string $abstract, object $instance): void
|
||||
{
|
||||
$this->instances->setInstance($abstract, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @template T of object
|
||||
* @param class-string<T>|ClassName|Stringable $class
|
||||
* @return T
|
||||
*/
|
||||
public function get(string|ClassName $class): object
|
||||
public function get(ClassName|Stringable|string $class): object
|
||||
{
|
||||
$className = $class instanceof ClassName ? $class->toString() : $class;
|
||||
$className = (string) $class;
|
||||
|
||||
// Bereits instanziierte Objekte zurückgeben
|
||||
if ($this->instances->hasSingleton($className)) {
|
||||
@@ -154,9 +164,9 @@ final class DefaultContainer implements Container
|
||||
|
||||
// For string keys without bindings, throw immediately
|
||||
if (!class_exists($class) && !interface_exists($class)) {
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve '{$class}': not a valid class and no binding exists. " .
|
||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings()))
|
||||
throw new ClassNotResolvableException(
|
||||
class: $class,
|
||||
availableBindings: array_keys($this->bindings->getAllBindings())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,27 +184,16 @@ final class DefaultContainer implements Container
|
||||
$dependencies = $this->dependencyResolver->resolveDependencies($className);
|
||||
|
||||
return $reflection->newInstance(...$dependencies->toArray());
|
||||
} catch (\RuntimeException $e) {
|
||||
// If it's already our detailed exception, just re-throw
|
||||
if (str_contains($e->getMessage(), 'Dependency resolution chain:')) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Otherwise, wrap with binding information
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve class '{$class}': {$e->getMessage()}. " .
|
||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
|
||||
". Dependency chain: " . implode(' -> ', $this->resolving),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve class '{$class}': {$e->getMessage()}. " .
|
||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
|
||||
". Dependency chain: " . implode(' -> ', $this->resolving),
|
||||
0,
|
||||
$e
|
||||
} catch (ContainerException $e) {
|
||||
// If it's already a ContainerException, just re-throw
|
||||
throw $e;
|
||||
} catch (\RuntimeException|\ReflectionException $e) {
|
||||
// Wrap with binding information
|
||||
throw ClassResolutionException::fromRuntimeException(
|
||||
class: $class,
|
||||
exception: $e,
|
||||
availableBindings: array_keys($this->bindings->getAllBindings()),
|
||||
dependencyChain: $this->resolving
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -202,38 +201,67 @@ final class DefaultContainer implements Container
|
||||
private function throwDetailedBindingException(string $class/*, $reflection*/): never
|
||||
{
|
||||
$availableBindings = array_keys($this->bindings->getAllBindings());
|
||||
$dependencyChain = implode(' -> ', $this->resolving);
|
||||
|
||||
// Look for similar interface bindings
|
||||
$similarBindings = array_filter($availableBindings, function ($binding) use ($class) {
|
||||
return str_contains($binding, basename(str_replace('\\', '/', $class)));
|
||||
});
|
||||
// Try to get DiscoveryRegistry from container and include discovered initializers
|
||||
$discoveredInitializers = [];
|
||||
if ($this->has(DiscoveryRegistry::class)) {
|
||||
try {
|
||||
$discoveryRegistry = $this->get(DiscoveryRegistry::class);
|
||||
$initializerResults = $discoveryRegistry->attributes->get(Initializer::class);
|
||||
|
||||
$message = "Cannot instantiate class '{$class}': class is not instantiable (interface, abstract class, or trait).\n" .
|
||||
"Dependency resolution chain: {$dependencyChain}\n" .
|
||||
"Total available bindings: " . count($availableBindings) . "\n";
|
||||
|
||||
if (! empty($similarBindings)) {
|
||||
$message .= "Similar bindings found: " . implode(', ', $similarBindings) . "\n";
|
||||
if (! empty($initializerResults)) {
|
||||
$discoveredInitializers = array_map(
|
||||
fn($attr) => $attr->className->getFullyQualified(),
|
||||
$initializerResults
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error
|
||||
}
|
||||
}
|
||||
|
||||
$message .= "All bindings: " . implode(', ', $availableBindings);
|
||||
|
||||
throw new \RuntimeException($message);
|
||||
throw ClassNotInstantiable::fromContainerContext(
|
||||
class: $class,
|
||||
dependencyChain: $this->resolving,
|
||||
availableBindings: $availableBindings,
|
||||
discoveredInitializers: $discoveredInitializers
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveBinding(string $class, callable|string|object $concrete): object
|
||||
{
|
||||
return match (true) {
|
||||
is_callable($concrete) => $concrete($this),
|
||||
is_string($concrete) => $this->get($concrete),
|
||||
default => $concrete
|
||||
};
|
||||
try {
|
||||
return match (true) {
|
||||
is_callable($concrete) => $concrete($this),
|
||||
is_string($concrete) => $this->get($concrete),
|
||||
/* @var object $concrete */
|
||||
default => $concrete
|
||||
};
|
||||
} catch (ContainerException $e) {
|
||||
// Re-throw ContainerExceptions as-is (they already have proper context)
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
// Determine binding type for better error messages
|
||||
$bindingType = match (true) {
|
||||
is_callable($concrete) => 'callable',
|
||||
is_string($concrete) => 'string',
|
||||
default => 'object'
|
||||
};
|
||||
|
||||
throw ClassResolutionException::fromBindingResolution(
|
||||
class: $class,
|
||||
previous: $e,
|
||||
availableBindings: array_keys($this->bindings->getAllBindings()),
|
||||
dependencyChain: $this->resolving,
|
||||
bindingType: $bindingType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param class-string $class */
|
||||
public function has(string $class): bool
|
||||
public function has(ClassName|Stringable|string $class): bool
|
||||
{
|
||||
$class = (string) $class;
|
||||
return $this->instances->hasSingleton($class)
|
||||
|| $this->instances->hasInstance($class)
|
||||
|| $this->bindings->hasBinding($class)
|
||||
@@ -241,8 +269,9 @@ final class DefaultContainer implements Container
|
||||
}
|
||||
|
||||
/** @param class-string $class */
|
||||
public function forget(string $class): void
|
||||
public function forget(ClassName|Stringable|string $class): void
|
||||
{
|
||||
$class = (string) $class;
|
||||
$this->instances->forget($class);
|
||||
$this->bindings->forget($class);
|
||||
$this->clearCaches(ClassName::create($class));
|
||||
|
||||
90
src/Framework/DI/Exceptions/ClassNotInstantiable.php
Normal file
90
src/Framework/DI/Exceptions/ClassNotInstantiable.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
|
||||
/**
|
||||
* Thrown when a class cannot be instantiated because it's an interface, abstract class, or trait
|
||||
*/
|
||||
final class ClassNotInstantiable extends ContainerException
|
||||
{
|
||||
/**
|
||||
* Factory method to create exception from container context
|
||||
* Automatically calculates similar bindings from available bindings
|
||||
*
|
||||
* @param class-string $class
|
||||
* @param string[] $dependencyChain
|
||||
* @param string[] $availableBindings
|
||||
* @param string[] $discoveredInitializers
|
||||
*/
|
||||
public static function fromContainerContext(
|
||||
string $class,
|
||||
array $dependencyChain,
|
||||
array $availableBindings,
|
||||
array $discoveredInitializers = []
|
||||
): self {
|
||||
// Calculate similar bindings based on class name
|
||||
$similarBindings = array_filter($availableBindings, function ($binding) use ($class) {
|
||||
return str_contains($binding, basename(str_replace('\\', '/', $class)));
|
||||
});
|
||||
|
||||
return new self(
|
||||
class: $class,
|
||||
dependencyChain: $dependencyChain,
|
||||
availableBindings: $availableBindings,
|
||||
similarBindings: $similarBindings,
|
||||
discoveredInitializers: $discoveredInitializers
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $class
|
||||
* @param string[] $dependencyChain
|
||||
* @param string[] $availableBindings
|
||||
* @param string[] $similarBindings
|
||||
* @param string[] $discoveredInitializers
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $class,
|
||||
public readonly array $dependencyChain,
|
||||
public readonly array $availableBindings,
|
||||
public readonly array $similarBindings = [],
|
||||
public readonly array $discoveredInitializers = [],
|
||||
)
|
||||
{
|
||||
$dependencyChainStr = implode(' -> ', $this->dependencyChain);
|
||||
|
||||
$message = "Cannot instantiate class '{$this->class}': class is not instantiable (interface, abstract class, or trait).\n" .
|
||||
"Dependency resolution chain: {$dependencyChainStr}\n" .
|
||||
'Total available bindings: ' . count($this->availableBindings) . "\n";
|
||||
|
||||
if (!empty($this->similarBindings)) {
|
||||
$message .= 'Similar bindings found: ' . implode(', ', $this->similarBindings) . "\n";
|
||||
}
|
||||
|
||||
if (!empty($this->discoveredInitializers)) {
|
||||
$message .= "Discovered initializers (" . count($this->discoveredInitializers) . "): " .
|
||||
implode(', ', $this->discoveredInitializers) . "\n";
|
||||
} else {
|
||||
$message .= "No initializers discovered in DiscoveryRegistry.\n";
|
||||
}
|
||||
|
||||
$context = ExceptionContext::forOperation('class_instantiation', 'DI Container')
|
||||
->withData([
|
||||
'class' => $this->class,
|
||||
'dependencyChain' => $this->dependencyChain,
|
||||
'availableBindings' => $this->availableBindings,
|
||||
'similarBindings' => $this->similarBindings,
|
||||
'discoveredInitializers' => $this->discoveredInitializers,
|
||||
'bindingCount' => count($this->availableBindings),
|
||||
'initializerCount' => count($this->discoveredInitializers),
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context
|
||||
);
|
||||
}
|
||||
}
|
||||
41
src/Framework/DI/Exceptions/ClassNotResolvableException.php
Normal file
41
src/Framework/DI/Exceptions/ClassNotResolvableException.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
|
||||
/**
|
||||
* Thrown when a class cannot be resolved because it's not a valid class/interface and no binding exists
|
||||
*/
|
||||
final class ClassNotResolvableException extends ContainerException
|
||||
{
|
||||
/**
|
||||
* @param string[] $availableBindings
|
||||
*/
|
||||
public function __construct(
|
||||
string $class,
|
||||
array $availableBindings,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
$message = "Cannot resolve '{$class}': not a valid class and no binding exists. " .
|
||||
"Available bindings: " . implode(', ', $availableBindings);
|
||||
|
||||
$context = ExceptionContext::forOperation('class_resolution', 'DI Container')
|
||||
->withData([
|
||||
'class' => $class,
|
||||
'availableBindings' => $availableBindings,
|
||||
'bindingCount' => count($availableBindings),
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context,
|
||||
code: $code,
|
||||
previous: $previous
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
105
src/Framework/DI/Exceptions/ClassResolutionException.php
Normal file
105
src/Framework/DI/Exceptions/ClassResolutionException.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
|
||||
/**
|
||||
* Thrown when a class cannot be resolved due to reflection or other errors
|
||||
*/
|
||||
final class ClassResolutionException extends ContainerException
|
||||
{
|
||||
/**
|
||||
* Factory method to create exception from binding resolution failure
|
||||
*
|
||||
* @param class-string $class
|
||||
* @param \Throwable $previous
|
||||
* @param string[] $availableBindings
|
||||
* @param string[] $dependencyChain
|
||||
* @param string $bindingType One of 'callable', 'string', or 'object'
|
||||
*/
|
||||
public static function fromBindingResolution(
|
||||
string $class,
|
||||
\Throwable $previous,
|
||||
array $availableBindings,
|
||||
array $dependencyChain,
|
||||
string $bindingType
|
||||
): self {
|
||||
$reason = "Binding resolution failed (binding type: {$bindingType}): " . $previous->getMessage();
|
||||
|
||||
return new self(
|
||||
class: $class,
|
||||
reason: $reason,
|
||||
availableBindings: $availableBindings,
|
||||
dependencyChain: $dependencyChain,
|
||||
previous: $previous,
|
||||
bindingType: $bindingType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create exception from RuntimeException or ReflectionException
|
||||
*
|
||||
* @param class-string $class
|
||||
* @param \RuntimeException|\ReflectionException $exception
|
||||
* @param string[] $availableBindings
|
||||
* @param string[] $dependencyChain
|
||||
*/
|
||||
public static function fromRuntimeException(
|
||||
string $class,
|
||||
\RuntimeException|\ReflectionException $exception,
|
||||
array $availableBindings,
|
||||
array $dependencyChain
|
||||
): self {
|
||||
return new self(
|
||||
class: $class,
|
||||
reason: $exception->getMessage(),
|
||||
availableBindings: $availableBindings,
|
||||
dependencyChain: $dependencyChain,
|
||||
previous: $exception
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $dependencyChain
|
||||
* @param string[] $availableBindings
|
||||
*/
|
||||
public function __construct(
|
||||
string $class,
|
||||
string $reason,
|
||||
array $availableBindings,
|
||||
array $dependencyChain,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
?string $bindingType = null
|
||||
) {
|
||||
$message = "Cannot resolve class '{$class}': {$reason}. " .
|
||||
"Available bindings: " . implode(', ', $availableBindings) .
|
||||
". Dependency chain: " . implode(' -> ', $dependencyChain);
|
||||
|
||||
$contextData = [
|
||||
'class' => $class,
|
||||
'reason' => $reason,
|
||||
'availableBindings' => $availableBindings,
|
||||
'dependencyChain' => $dependencyChain,
|
||||
'bindingCount' => count($availableBindings),
|
||||
];
|
||||
|
||||
if ($bindingType !== null) {
|
||||
$contextData['bindingType'] = $bindingType;
|
||||
}
|
||||
|
||||
$context = ExceptionContext::forOperation('class_resolution', 'DI Container')
|
||||
->withData($contextData);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context,
|
||||
code: $code,
|
||||
previous: $previous
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
28
src/Framework/DI/Exceptions/ContainerException.php
Normal file
28
src/Framework/DI/Exceptions/ContainerException.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
class ContainerException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* Simplified constructor for simple error messages
|
||||
* Automatically creates an ExceptionContext for DI operations
|
||||
*/
|
||||
public function __construct(
|
||||
string $message = '',
|
||||
?ExceptionContext $context = null,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context ?? ExceptionContext::forOperation('dependency_injection', 'DI Container'),
|
||||
code: $code,
|
||||
previous: $previous
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Framework\DI\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when DefaultImplementation attribute validation fails
|
||||
*/
|
||||
final class DefaultImplementationException extends RuntimeException
|
||||
final class DefaultImplementationException extends ContainerException
|
||||
{
|
||||
/**
|
||||
* Thrown when a class with DefaultImplementation attribute does not implement the specified interface
|
||||
@@ -21,13 +19,20 @@ final class DefaultImplementationException extends RuntimeException
|
||||
*/
|
||||
public static function doesNotImplementInterface(string $className, string $interface): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
'Class "%s" has #[DefaultImplementation] for interface "%s" but does not implement that interface',
|
||||
$className,
|
||||
$interface
|
||||
)
|
||||
$message = sprintf(
|
||||
'Class "%s" has #[DefaultImplementation] for interface "%s" but does not implement that interface',
|
||||
$className,
|
||||
$interface
|
||||
);
|
||||
|
||||
$context = ExceptionContext::forOperation('default_implementation_validation', 'DI Container')
|
||||
->withData([
|
||||
'className' => $className,
|
||||
'interface' => $interface,
|
||||
'errorType' => 'does_not_implement_interface',
|
||||
]);
|
||||
|
||||
return new self($message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,13 +42,19 @@ final class DefaultImplementationException extends RuntimeException
|
||||
*/
|
||||
public static function noInterfacesImplemented(string $className): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
'Class "%s" has #[DefaultImplementation] without explicit interface but implements no interfaces. ' .
|
||||
'Either specify an interface explicitly or ensure the class implements at least one interface.',
|
||||
$className
|
||||
)
|
||||
$message = sprintf(
|
||||
'Class "%s" has #[DefaultImplementation] without explicit interface but implements no interfaces. ' .
|
||||
'Either specify an interface explicitly or ensure the class implements at least one interface.',
|
||||
$className
|
||||
);
|
||||
|
||||
$context = ExceptionContext::forOperation('default_implementation_validation', 'DI Container')
|
||||
->withData([
|
||||
'className' => $className,
|
||||
'errorType' => 'no_interfaces_implemented',
|
||||
]);
|
||||
|
||||
return new self($message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,12 +65,19 @@ final class DefaultImplementationException extends RuntimeException
|
||||
*/
|
||||
public static function interfaceDoesNotExist(string $className, string $interface): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
'Class "%s" has #[DefaultImplementation] for interface "%s" which does not exist',
|
||||
$className,
|
||||
$interface
|
||||
)
|
||||
$message = sprintf(
|
||||
'Class "%s" has #[DefaultImplementation] for interface "%s" which does not exist',
|
||||
$className,
|
||||
$interface
|
||||
);
|
||||
|
||||
$context = ExceptionContext::forOperation('default_implementation_validation', 'DI Container')
|
||||
->withData([
|
||||
'className' => $className,
|
||||
'interface' => $interface,
|
||||
'errorType' => 'interface_does_not_exist',
|
||||
]);
|
||||
|
||||
return new self($message, $context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Framework\DI;
|
||||
|
||||
use App\Framework\Context\ContextType;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
#[\Attribute(\Attribute::TARGET_METHOD|\Attribute::IS_REPEATABLE)]
|
||||
final readonly class Initializer
|
||||
{
|
||||
/** @var ContextType[]|null */
|
||||
|
||||
@@ -29,8 +29,7 @@ final readonly class InitializerProcessor
|
||||
private Container $container,
|
||||
private ReflectionProvider $reflectionProvider,
|
||||
private ExecutionContext $executionContext,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Intelligente Initializer-Verarbeitung mit Dependency-Graph:
|
||||
@@ -41,7 +40,7 @@ final readonly class InitializerProcessor
|
||||
public function processInitializers(DiscoveryRegistry $results): void
|
||||
{
|
||||
// Safe Logger resolution - use if available, otherwise rely on error_log
|
||||
$logger = $this->container->has(Logger::class) ? $this->container->get(Logger::class) : null;
|
||||
$logger = $this->container->get(Logger::class);
|
||||
$initializerResults = $results->attributes->get(Initializer::class);
|
||||
|
||||
$logger?->debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers");
|
||||
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling;
|
||||
|
||||
use App\Framework\Attributes\Initializer;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\ErrorHandling\Handlers\DatabaseErrorHandler;
|
||||
use App\Framework\ErrorHandling\Handlers\FallbackErrorHandler;
|
||||
use App\Framework\ErrorHandling\Handlers\HttpErrorHandler;
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ErrorHandling\ValueObjects;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorSeverity;
|
||||
use App\Framework\Exception\Core\ErrorSeverity;
|
||||
|
||||
/**
|
||||
* Value Object representing error classification metadata.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace App\Framework\Http;
|
||||
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Logging\ValueObjects\LogContext;
|
||||
|
||||
final readonly class HttpMiddlewareChain implements HttpMiddlewareChainInterface
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Http\Middlewares;
|
||||
|
||||
use App\Framework\ErrorHandling\ErrorHandler;
|
||||
use App\Framework\ExceptionHandling\ErrorKernel;
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewarePriority;
|
||||
@@ -18,7 +19,7 @@ final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private Logger $logger,
|
||||
private ErrorHandler $errorHandler,
|
||||
#private ErrorHandler $errorHandler,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -27,6 +28,10 @@ final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
|
||||
try {
|
||||
return $next($context);
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
$error = new ErrorKernel();
|
||||
$error->handle($e);
|
||||
|
||||
$response = $this->errorHandler->createHttpResponse($e, $context);
|
||||
|
||||
return $context->withResponse($response);
|
||||
|
||||
Reference in New Issue
Block a user