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\Encryption\EncryptionFactory;
|
||||||
use App\Framework\ErrorHandling\CliErrorHandler;
|
use App\Framework\ErrorHandling\CliErrorHandler;
|
||||||
use App\Framework\ErrorHandling\ErrorHandler;
|
use App\Framework\ErrorHandling\ErrorHandler;
|
||||||
|
use App\Framework\ErrorHandling\ErrorHandlerManager;
|
||||||
|
use App\Framework\ExceptionHandling\ExceptionHandlerManager;
|
||||||
use App\Framework\Http\MiddlewareManager;
|
use App\Framework\Http\MiddlewareManager;
|
||||||
use App\Framework\Http\MiddlewareManagerInterface;
|
use App\Framework\Http\MiddlewareManagerInterface;
|
||||||
use App\Framework\Http\ResponseEmitter;
|
use App\Framework\Http\ResponseEmitter;
|
||||||
@@ -71,7 +73,7 @@ final readonly class AppBootstrapper
|
|||||||
|
|
||||||
//if ($envType->isDevelopment()) {
|
//if ($envType->isDevelopment()) {
|
||||||
|
|
||||||
if($typedConfig->app->type->isDevelopment()) {
|
/*if($typedConfig->app->type->isDevelopment()) {
|
||||||
// Fehleranzeige für die Entwicklung aktivieren
|
// Fehleranzeige für die Entwicklung aktivieren
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
ini_set('display_startup_errors', 1);
|
ini_set('display_startup_errors', 1);
|
||||||
@@ -87,7 +89,7 @@ final readonly class AppBootstrapper
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, true, true);
|
}, true, true);
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public function bootstrapWeb(): ApplicationInterface
|
public function bootstrapWeb(): ApplicationInterface
|
||||||
@@ -157,7 +159,11 @@ final readonly class AppBootstrapper
|
|||||||
|
|
||||||
private function registerWebErrorHandler(): void
|
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
|
private function registerCliErrorHandler(): void
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\DI;
|
namespace App\Framework\DI;
|
||||||
|
|
||||||
use App\Framework\Core\ValueObjects\ClassName;
|
use App\Framework\Core\ValueObjects\ClassName;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
interface Container
|
interface Container
|
||||||
{
|
{
|
||||||
@@ -17,17 +18,26 @@ interface Container
|
|||||||
* @param class-string<T>|ClassName $class
|
* @param class-string<T>|ClassName $class
|
||||||
* @return T
|
* @return T
|
||||||
*/
|
*/
|
||||||
public function get(string|ClassName $class): object;
|
public function get(ClassName|Stringable|string $class): object;
|
||||||
|
|
||||||
/** @param class-string $class */
|
/** @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 */
|
/** @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;
|
namespace App\Framework\DI;
|
||||||
|
|
||||||
use App\Framework\Core\ValueObjects\ClassName;
|
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\CyclicDependencyException;
|
||||||
use App\Framework\DI\Exceptions\LazyLoadingException;
|
use App\Framework\DI\Exceptions\LazyLoadingException;
|
||||||
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||||
use App\Framework\Metrics\FrameworkMetricsCollector;
|
use App\Framework\Metrics\FrameworkMetricsCollector;
|
||||||
use App\Framework\Reflection\CachedReflectionProvider;
|
use App\Framework\Reflection\CachedReflectionProvider;
|
||||||
use App\Framework\Reflection\ReflectionProvider;
|
use App\Framework\Reflection\ReflectionProvider;
|
||||||
|
use Stringable;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class DefaultContainer implements Container
|
final class DefaultContainer implements Container
|
||||||
@@ -54,8 +60,9 @@ final class DefaultContainer implements Container
|
|||||||
$this->instance(ReflectionProvider::class, $this->reflectionProvider);
|
$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);
|
$this->bindings->bind($abstract, $concrete);
|
||||||
|
|
||||||
// Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
|
// 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->bind($abstract, $concrete);
|
||||||
$this->instances->markAsSingleton($abstract);
|
$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);
|
$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
|
// Bereits instanziierte Objekte zurückgeben
|
||||||
if ($this->instances->hasSingleton($className)) {
|
if ($this->instances->hasSingleton($className)) {
|
||||||
@@ -154,9 +164,9 @@ final class DefaultContainer implements Container
|
|||||||
|
|
||||||
// For string keys without bindings, throw immediately
|
// For string keys without bindings, throw immediately
|
||||||
if (!class_exists($class) && !interface_exists($class)) {
|
if (!class_exists($class) && !interface_exists($class)) {
|
||||||
throw new \RuntimeException(
|
throw new ClassNotResolvableException(
|
||||||
"Cannot resolve '{$class}': not a valid class and no binding exists. " .
|
class: $class,
|
||||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings()))
|
availableBindings: array_keys($this->bindings->getAllBindings())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,27 +184,16 @@ final class DefaultContainer implements Container
|
|||||||
$dependencies = $this->dependencyResolver->resolveDependencies($className);
|
$dependencies = $this->dependencyResolver->resolveDependencies($className);
|
||||||
|
|
||||||
return $reflection->newInstance(...$dependencies->toArray());
|
return $reflection->newInstance(...$dependencies->toArray());
|
||||||
} catch (\RuntimeException $e) {
|
} catch (ContainerException $e) {
|
||||||
// If it's already our detailed exception, just re-throw
|
// If it's already a ContainerException, just re-throw
|
||||||
if (str_contains($e->getMessage(), 'Dependency resolution chain:')) {
|
throw $e;
|
||||||
throw $e;
|
} catch (\RuntimeException|\ReflectionException $e) {
|
||||||
}
|
// Wrap with binding information
|
||||||
|
throw ClassResolutionException::fromRuntimeException(
|
||||||
// Otherwise, wrap with binding information
|
class: $class,
|
||||||
throw new \RuntimeException(
|
exception: $e,
|
||||||
"Cannot resolve class '{$class}': {$e->getMessage()}. " .
|
availableBindings: array_keys($this->bindings->getAllBindings()),
|
||||||
"Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
|
dependencyChain: $this->resolving
|
||||||
". 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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,38 +201,67 @@ final class DefaultContainer implements Container
|
|||||||
private function throwDetailedBindingException(string $class/*, $reflection*/): never
|
private function throwDetailedBindingException(string $class/*, $reflection*/): never
|
||||||
{
|
{
|
||||||
$availableBindings = array_keys($this->bindings->getAllBindings());
|
$availableBindings = array_keys($this->bindings->getAllBindings());
|
||||||
$dependencyChain = implode(' -> ', $this->resolving);
|
|
||||||
|
|
||||||
// Look for similar interface bindings
|
// Try to get DiscoveryRegistry from container and include discovered initializers
|
||||||
$similarBindings = array_filter($availableBindings, function ($binding) use ($class) {
|
$discoveredInitializers = [];
|
||||||
return str_contains($binding, basename(str_replace('\\', '/', $class)));
|
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" .
|
if (! empty($initializerResults)) {
|
||||||
"Dependency resolution chain: {$dependencyChain}\n" .
|
$discoveredInitializers = array_map(
|
||||||
"Total available bindings: " . count($availableBindings) . "\n";
|
fn($attr) => $attr->className->getFullyQualified(),
|
||||||
|
$initializerResults
|
||||||
if (! empty($similarBindings)) {
|
);
|
||||||
$message .= "Similar bindings found: " . implode(', ', $similarBindings) . "\n";
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$message .= "All bindings: " . implode(', ', $availableBindings);
|
throw ClassNotInstantiable::fromContainerContext(
|
||||||
|
class: $class,
|
||||||
throw new \RuntimeException($message);
|
dependencyChain: $this->resolving,
|
||||||
|
availableBindings: $availableBindings,
|
||||||
|
discoveredInitializers: $discoveredInitializers
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveBinding(string $class, callable|string|object $concrete): object
|
private function resolveBinding(string $class, callable|string|object $concrete): object
|
||||||
{
|
{
|
||||||
return match (true) {
|
try {
|
||||||
is_callable($concrete) => $concrete($this),
|
return match (true) {
|
||||||
is_string($concrete) => $this->get($concrete),
|
is_callable($concrete) => $concrete($this),
|
||||||
default => $concrete
|
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 */
|
/** @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)
|
return $this->instances->hasSingleton($class)
|
||||||
|| $this->instances->hasInstance($class)
|
|| $this->instances->hasInstance($class)
|
||||||
|| $this->bindings->hasBinding($class)
|
|| $this->bindings->hasBinding($class)
|
||||||
@@ -241,8 +269,9 @@ final class DefaultContainer implements Container
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @param class-string $class */
|
/** @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->instances->forget($class);
|
||||||
$this->bindings->forget($class);
|
$this->bindings->forget($class);
|
||||||
$this->clearCaches(ClassName::create($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;
|
namespace App\Framework\DI\Exceptions;
|
||||||
|
|
||||||
use App\Framework\Exception\ExceptionContext;
|
use App\Framework\Exception\ExceptionContext;
|
||||||
use App\Framework\Exception\FrameworkException;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception thrown when DefaultImplementation attribute validation fails
|
* 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
|
* 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
|
public static function doesNotImplementInterface(string $className, string $interface): self
|
||||||
{
|
{
|
||||||
return new self(
|
$message = sprintf(
|
||||||
sprintf(
|
'Class "%s" has #[DefaultImplementation] for interface "%s" but does not implement that interface',
|
||||||
'Class "%s" has #[DefaultImplementation] for interface "%s" but does not implement that interface',
|
$className,
|
||||||
$className,
|
$interface
|
||||||
$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
|
public static function noInterfacesImplemented(string $className): self
|
||||||
{
|
{
|
||||||
return new self(
|
$message = sprintf(
|
||||||
sprintf(
|
'Class "%s" has #[DefaultImplementation] without explicit interface but implements no interfaces. ' .
|
||||||
'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.',
|
||||||
'Either specify an interface explicitly or ensure the class implements at least one interface.',
|
$className
|
||||||
$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
|
public static function interfaceDoesNotExist(string $className, string $interface): self
|
||||||
{
|
{
|
||||||
return new self(
|
$message = sprintf(
|
||||||
sprintf(
|
'Class "%s" has #[DefaultImplementation] for interface "%s" which does not exist',
|
||||||
'Class "%s" has #[DefaultImplementation] for interface "%s" which does not exist',
|
$className,
|
||||||
$className,
|
$interface
|
||||||
$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;
|
use App\Framework\Context\ContextType;
|
||||||
|
|
||||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
#[\Attribute(\Attribute::TARGET_METHOD|\Attribute::IS_REPEATABLE)]
|
||||||
final readonly class Initializer
|
final readonly class Initializer
|
||||||
{
|
{
|
||||||
/** @var ContextType[]|null */
|
/** @var ContextType[]|null */
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ final readonly class InitializerProcessor
|
|||||||
private Container $container,
|
private Container $container,
|
||||||
private ReflectionProvider $reflectionProvider,
|
private ReflectionProvider $reflectionProvider,
|
||||||
private ExecutionContext $executionContext,
|
private ExecutionContext $executionContext,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intelligente Initializer-Verarbeitung mit Dependency-Graph:
|
* Intelligente Initializer-Verarbeitung mit Dependency-Graph:
|
||||||
@@ -41,7 +40,7 @@ final readonly class InitializerProcessor
|
|||||||
public function processInitializers(DiscoveryRegistry $results): void
|
public function processInitializers(DiscoveryRegistry $results): void
|
||||||
{
|
{
|
||||||
// Safe Logger resolution - use if available, otherwise rely on error_log
|
// 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);
|
$initializerResults = $results->attributes->get(Initializer::class);
|
||||||
|
|
||||||
$logger?->debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers");
|
$logger?->debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers");
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\ErrorHandling;
|
namespace App\Framework\ErrorHandling;
|
||||||
|
|
||||||
use App\Framework\Attributes\Initializer;
|
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
|
use App\Framework\DI\Initializer;
|
||||||
use App\Framework\ErrorHandling\Handlers\DatabaseErrorHandler;
|
use App\Framework\ErrorHandling\Handlers\DatabaseErrorHandler;
|
||||||
use App\Framework\ErrorHandling\Handlers\FallbackErrorHandler;
|
use App\Framework\ErrorHandling\Handlers\FallbackErrorHandler;
|
||||||
use App\Framework\ErrorHandling\Handlers\HttpErrorHandler;
|
use App\Framework\ErrorHandling\Handlers\HttpErrorHandler;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\ErrorHandling\ValueObjects;
|
namespace App\Framework\ErrorHandling\ValueObjects;
|
||||||
|
|
||||||
use App\Framework\ErrorHandling\ErrorSeverity;
|
use App\Framework\Exception\Core\ErrorSeverity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value Object representing error classification metadata.
|
* 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\DI\Container;
|
||||||
use App\Framework\Logging\Logger;
|
use App\Framework\Logging\Logger;
|
||||||
|
use App\Framework\Logging\ValueObjects\LogContext;
|
||||||
|
|
||||||
final readonly class HttpMiddlewareChain implements HttpMiddlewareChainInterface
|
final readonly class HttpMiddlewareChain implements HttpMiddlewareChainInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Http\Middlewares;
|
namespace App\Framework\Http\Middlewares;
|
||||||
|
|
||||||
use App\Framework\ErrorHandling\ErrorHandler;
|
use App\Framework\ErrorHandling\ErrorHandler;
|
||||||
|
use App\Framework\ExceptionHandling\ErrorKernel;
|
||||||
use App\Framework\Http\HttpMiddleware;
|
use App\Framework\Http\HttpMiddleware;
|
||||||
use App\Framework\Http\MiddlewareContext;
|
use App\Framework\Http\MiddlewareContext;
|
||||||
use App\Framework\Http\MiddlewarePriority;
|
use App\Framework\Http\MiddlewarePriority;
|
||||||
@@ -18,7 +19,7 @@ final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Logger $logger,
|
private Logger $logger,
|
||||||
private ErrorHandler $errorHandler,
|
#private ErrorHandler $errorHandler,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +28,10 @@ final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
|
|||||||
try {
|
try {
|
||||||
return $next($context);
|
return $next($context);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|
||||||
|
$error = new ErrorKernel();
|
||||||
|
$error->handle($e);
|
||||||
|
|
||||||
$response = $this->errorHandler->createHttpResponse($e, $context);
|
$response = $this->errorHandler->createHttpResponse($e, $context);
|
||||||
|
|
||||||
return $context->withResponse($response);
|
return $context->withResponse($response);
|
||||||
|
|||||||
Reference in New Issue
Block a user