diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e80ff46b --- /dev/null +++ b/AGENTS.md @@ -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/`. diff --git a/src/Framework/Core/AppBootstrapper.php b/src/Framework/Core/AppBootstrapper.php index 56b9d751..85e745ca 100644 --- a/src/Framework/Core/AppBootstrapper.php +++ b/src/Framework/Core/AppBootstrapper.php @@ -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 diff --git a/src/Framework/DI/Container.php b/src/Framework/DI/Container.php index d9b93a5c..c24fb551 100644 --- a/src/Framework/DI/Container.php +++ b/src/Framework/DI/Container.php @@ -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|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; } diff --git a/src/Framework/DI/DefaultContainer.php b/src/Framework/DI/DefaultContainer.php index d7126c9b..bd604afe 100644 --- a/src/Framework/DI/DefaultContainer.php +++ b/src/Framework/DI/DefaultContainer.php @@ -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|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)); diff --git a/src/Framework/DI/Exceptions/ClassNotInstantiable.php b/src/Framework/DI/Exceptions/ClassNotInstantiable.php new file mode 100644 index 00000000..35ce22ed --- /dev/null +++ b/src/Framework/DI/Exceptions/ClassNotInstantiable.php @@ -0,0 +1,90 @@ + ', $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 + ); + } +} diff --git a/src/Framework/DI/Exceptions/ClassNotResolvableException.php b/src/Framework/DI/Exceptions/ClassNotResolvableException.php new file mode 100644 index 00000000..022cc7a7 --- /dev/null +++ b/src/Framework/DI/Exceptions/ClassNotResolvableException.php @@ -0,0 +1,41 @@ +withData([ + 'class' => $class, + 'availableBindings' => $availableBindings, + 'bindingCount' => count($availableBindings), + ]); + + parent::__construct( + message: $message, + context: $context, + code: $code, + previous: $previous + ); + } +} + diff --git a/src/Framework/DI/Exceptions/ClassResolutionException.php b/src/Framework/DI/Exceptions/ClassResolutionException.php new file mode 100644 index 00000000..11794949 --- /dev/null +++ b/src/Framework/DI/Exceptions/ClassResolutionException.php @@ -0,0 +1,105 @@ +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 + ); + } +} + diff --git a/src/Framework/DI/Exceptions/ContainerException.php b/src/Framework/DI/Exceptions/ContainerException.php new file mode 100644 index 00000000..908d3f3a --- /dev/null +++ b/src/Framework/DI/Exceptions/ContainerException.php @@ -0,0 +1,28 @@ +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); } } diff --git a/src/Framework/DI/Initializer.php b/src/Framework/DI/Initializer.php index 444318ba..27e27d03 100644 --- a/src/Framework/DI/Initializer.php +++ b/src/Framework/DI/Initializer.php @@ -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 */ diff --git a/src/Framework/Discovery/InitializerProcessor.php b/src/Framework/Discovery/InitializerProcessor.php index de6d3279..421c9c32 100644 --- a/src/Framework/Discovery/InitializerProcessor.php +++ b/src/Framework/Discovery/InitializerProcessor.php @@ -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"); diff --git a/src/Framework/ErrorHandling/ErrorHandlerInitializer.php b/src/Framework/ErrorHandling/ErrorHandlerInitializer.php index f5e9aff7..2ec6067c 100644 --- a/src/Framework/ErrorHandling/ErrorHandlerInitializer.php +++ b/src/Framework/ErrorHandling/ErrorHandlerInitializer.php @@ -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; diff --git a/src/Framework/ErrorHandling/ValueObjects/ErrorMetadata.php b/src/Framework/ErrorHandling/ValueObjects/ErrorMetadata.php index 169ca06a..b05ef92f 100644 --- a/src/Framework/ErrorHandling/ValueObjects/ErrorMetadata.php +++ b/src/Framework/ErrorHandling/ValueObjects/ErrorMetadata.php @@ -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. diff --git a/src/Framework/ExceptionHandling/ErrorContext.php b/src/Framework/ExceptionHandling/ErrorContext.php new file mode 100644 index 00000000..a3ebe53a --- /dev/null +++ b/src/Framework/ExceptionHandling/ErrorContext.php @@ -0,0 +1,37 @@ +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); + } +} diff --git a/src/Framework/ExceptionHandling/ErrorDecision.php b/src/Framework/ExceptionHandling/ErrorDecision.php new file mode 100644 index 00000000..b014b1b3 --- /dev/null +++ b/src/Framework/ExceptionHandling/ErrorDecision.php @@ -0,0 +1,15 @@ +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); + } +} diff --git a/src/Framework/ExceptionHandling/ErrorHandlerStrategy.php b/src/Framework/ExceptionHandling/ErrorHandlerStrategy.php new file mode 100644 index 00000000..2cbde606 --- /dev/null +++ b/src/Framework/ExceptionHandling/ErrorHandlerStrategy.php @@ -0,0 +1,10 @@ +report($e->getMessage()); + + $this->rendererFactory->getRenderer()->render(); + + return null; + } +} + diff --git a/src/Framework/ExceptionHandling/ErrorRenderer.php b/src/Framework/ExceptionHandling/ErrorRenderer.php new file mode 100644 index 00000000..87acab30 --- /dev/null +++ b/src/Framework/ExceptionHandling/ErrorRenderer.php @@ -0,0 +1,8 @@ +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; + } +} diff --git a/src/Framework/ExceptionHandling/ErrorScopeContext.php b/src/Framework/ExceptionHandling/ErrorScopeContext.php new file mode 100644 index 00000000..f8069e10 --- /dev/null +++ b/src/Framework/ExceptionHandling/ErrorScopeContext.php @@ -0,0 +1,9 @@ +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(...)); + } +} diff --git a/src/Framework/ExceptionHandling/GlobalExceptionHandler.php b/src/Framework/ExceptionHandling/GlobalExceptionHandler.php new file mode 100644 index 00000000..6ede88a7 --- /dev/null +++ b/src/Framework/ExceptionHandling/GlobalExceptionHandler.php @@ -0,0 +1,14 @@ +handle($throwable); + } +} diff --git a/src/Framework/ExceptionHandling/Renderer/HtmlErrorRenderer.php b/src/Framework/ExceptionHandling/Renderer/HtmlErrorRenderer.php new file mode 100644 index 00000000..27bfb306 --- /dev/null +++ b/src/Framework/ExceptionHandling/Renderer/HtmlErrorRenderer.php @@ -0,0 +1,14 @@ +

500 Internal Server Error

'; + } +} diff --git a/src/Framework/ExceptionHandling/Reporter/LogReporter.php b/src/Framework/ExceptionHandling/Reporter/LogReporter.php new file mode 100644 index 00000000..110a905e --- /dev/null +++ b/src/Framework/ExceptionHandling/Reporter/LogReporter.php @@ -0,0 +1,12 @@ +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); + } +} diff --git a/src/Framework/ExceptionHandling/Strategy/ErrorPolicyResolver.php b/src/Framework/ExceptionHandling/Strategy/ErrorPolicyResolver.php new file mode 100644 index 00000000..71a24df8 --- /dev/null +++ b/src/Framework/ExceptionHandling/Strategy/ErrorPolicyResolver.php @@ -0,0 +1,19 @@ +isProduction() => new StrictErrorPolicy(), + $environmentType->isDevelopment() => new StrictErrorPolicy(), + default => new StrictErrorPolicy(), + }; + } +} diff --git a/src/Framework/ExceptionHandling/Strategy/LenientPolicy.php b/src/Framework/ExceptionHandling/Strategy/LenientPolicy.php new file mode 100644 index 00000000..b91f591c --- /dev/null +++ b/src/Framework/ExceptionHandling/Strategy/LenientPolicy.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/src/Framework/ExceptionHandling/Strategy/SilentErrorPolicy.php b/src/Framework/ExceptionHandling/Strategy/SilentErrorPolicy.php new file mode 100644 index 00000000..b559609a --- /dev/null +++ b/src/Framework/ExceptionHandling/Strategy/SilentErrorPolicy.php @@ -0,0 +1,16 @@ +message, 0, $context->severity, $context->file, $context->line); + } +} diff --git a/src/Framework/Http/HttpMiddlewareChain.php b/src/Framework/Http/HttpMiddlewareChain.php index 062577c5..376c755b 100644 --- a/src/Framework/Http/HttpMiddlewareChain.php +++ b/src/Framework/Http/HttpMiddlewareChain.php @@ -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 { diff --git a/src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php b/src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php index c2464a0d..836e2dff 100644 --- a/src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php +++ b/src/Framework/Http/Middlewares/ExceptionHandlingMiddleware.php @@ -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);