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(); $config = $this->container->get(\Phred\Support\Contracts\ConfigInterface::class); // CORS $corsSettings = new \Neomerx\Cors\Strategies\Settings(); $corsSettings->init( parse_url((string)getenv('APP_URL'), PHP_URL_SCHEME) ?: 'http', parse_url((string)getenv('APP_URL'), PHP_URL_HOST) ?: 'localhost', (int)parse_url((string)getenv('APP_URL'), PHP_URL_PORT) ?: 80 ); $corsSettings->setAllowedOrigins($config->get('cors.origin', ['*'])); $corsSettings->setAllowedMethods($config->get('cors.methods', ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])); $corsSettings->setAllowedHeaders($config->get('cors.headers.allow', ['Content-Type', 'Accept', 'Authorization', 'X-Requested-With'])); $corsSettings->enableAllOriginsAllowed(); $corsSettings->enableAllMethodsAllowed(); $corsSettings->enableAllHeadersAllowed(); $middleware = []; if (filter_var($config->get('APP_DEBUG', false), FILTER_VALIDATE_BOOLEAN)) { $middleware[] = new class extends Middleware\Middleware { public function process(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Server\RequestHandlerInterface $handler): \Psr\Http\Message\ResponseInterface { self::$timings = []; // Reset timings for each request in debug mode $response = $handler->handle($request); $timings = self::getTimings(); if (!empty($timings)) { $encoded = json_encode($timings, JSON_UNESCAPED_SLASHES); if ($encoded) { $response = $response->withHeader('X-Phred-Timings', $encoded); } } return $response; } }; } $middleware = array_merge($middleware, [ // Security headers new Middleware\Security\SecureHeadersMiddleware($config), // CORS new \Middlewares\Cors(\Neomerx\Cors\Analyzer::instance($corsSettings)), new Middleware\ProblemDetailsMiddleware( filter_var($config->get('APP_DEBUG', 'false'), FILTER_VALIDATE_BOOLEAN), null, null, filter_var($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), \Phred\Http\Responses\XmlResponseFactory::class => \DI\autowire(\Phred\Http\Responses\XmlResponseFactory::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); } } // Load module route files under prefixes defined in routes/web.php via RouteGroups includes. // Additionally, as a convenience, auto-mount modules without explicit includes using folder name as prefix. $modulesDir = dirname(__DIR__, 2) . '/modules'; if (is_dir($modulesDir)) { $entries = array_values(array_filter(scandir($modulesDir) ?: [], static fn($e) => $e !== '.' && $e !== '..')); sort($entries, SORT_STRING); foreach ($entries as $mod) { $modRoutes = $modulesDir . '/' . $mod . '/Routes'; if (!is_dir($modRoutes)) { continue; } // Auto-mount only if the module's web.php wasn't already included via RouteGroups in root file. $autoInclude = function (string $relative, string $prefix) use ($modRoutes, $router): void { $file = $modRoutes . '/' . $relative; if (is_file($file)) { $router->group('/' . strtolower($prefix), static function (Router $r) use ($file): void { /** @noinspection PhpIncludeInspection */ (static function ($router) use ($file) { require $file; })($r); }); } }; $autoInclude('web.php', $mod); // api.php can be auto-mounted under /api/ $apiFile = $modRoutes . '/api.php'; if (is_file($apiFile)) { $router->group('/api/' . strtolower($mod), static function (Router $r) use ($apiFile): void { /** @noinspection PhpIncludeInspection */ (static function ($router) use ($apiFile) { require $apiFile; })($r); }); } } } // 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); } }