container = $container ?? $this->buildContainer(); // Providers may contribute routes during boot; ensure dispatcher is built after container init $this->dispatcher = $dispatcher ?? $this->buildDispatcher(); } public function container(): Container { return $this->container; } public function dispatcher(): Dispatcher { return $this->dispatcher; } public function handle(ServerRequest $request): ResponseInterface { $psr17 = new Psr17Factory(); $middleware = [ new Middleware\ProblemDetailsMiddleware( \Phred\Support\Config::get('APP_DEBUG', 'false') === 'true', null, null, filter_var(\Phred\Support\Config::get('API_PROBLEM_DETAILS', 'true'), FILTER_VALIDATE_BOOLEAN) ), // Perform extension-based content negotiation hinting before standard negotiation new Middleware\UrlExtensionNegotiationMiddleware(), new Middleware\ContentNegotiationMiddleware(), new Middleware\RoutingMiddleware($this->dispatcher, $psr17), new Middleware\DispatchMiddleware($psr17), ]; $relay = new Relay($middleware); return $relay->handle($request); } private function buildContainer(): Container { $builder = new ContainerBuilder(); // Allow service providers to register definitions before defaults $configAdapter = new \Phred\Support\DefaultConfig(); $providers = new \Phred\Support\ProviderRepository($configAdapter); $providers->load(); $providers->registerAll($builder); // Add core definitions/bindings $builder->addDefinitions([ \Phred\Support\Contracts\ConfigInterface::class => \DI\autowire(\Phred\Support\DefaultConfig::class), \Phred\Http\Contracts\ErrorFormatNegotiatorInterface::class => \DI\autowire(\Phred\Http\Support\DefaultErrorFormatNegotiator::class), \Phred\Http\Contracts\RequestIdProviderInterface::class => \DI\autowire(\Phred\Http\Support\DefaultRequestIdProvider::class), \Phred\Http\Contracts\ExceptionToStatusMapperInterface::class => \DI\autowire(\Phred\Http\Support\DefaultExceptionToStatusMapper::class), \Phred\Http\Contracts\ApiResponseFactoryInterface::class => \DI\autowire(\Phred\Http\Responses\DelegatingApiResponseFactory::class), \Phred\Http\Responses\RestResponseFactory::class => \DI\autowire(\Phred\Http\Responses\RestResponseFactory::class), \Phred\Http\Responses\JsonApiResponseFactory::class => \DI\autowire(\Phred\Http\Responses\JsonApiResponseFactory::class), ]); $container = $builder->build(); // Reset provider-registered routes to avoid duplicates across multiple kernel instantiations (e.g., tests) \Phred\Http\Routing\RouteRegistry::clear(); // Boot providers after container is available $providers->bootAll($container); return $container; } private function buildDispatcher(): Dispatcher { $routesPath = dirname(__DIR__, 2) . '/routes'; $collector = static function (RouteCollector $r) use ($routesPath): void { // Load user-defined routes if present $router = new Router($r); foreach (['web.php', 'api.php'] as $file) { $path = $routesPath . '/' . $file; if (is_file($path)) { /** @noinspection PhpIncludeInspection */ (static function ($router) use ($path) { require $path; })($router); } } // Allow providers to contribute routes \Phred\Http\Routing\RouteRegistry::apply($r, $router); // Ensure default demo routes exist for acceptance/demo $r->addRoute('GET', '/_phred/health', [Controllers\HealthController::class, '__invoke']); $r->addRoute('GET', '/_phred/format', [Controllers\FormatController::class, '__invoke']); }; return simpleDispatcher($collector); } }