Lines 75.82% 69 / 91
Methods 58.33% 7 / 12
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 boot 0.00% 0 / 9 0.00% 0 / 1 6
 handle 85.71% 6 / 7 0.00% 0 / 1 5.07
 getContainer 80.00% 4 / 5 0.00% 0 / 1 3.07
 getRoutingContext 100.00% 1 / 1 100.00% 1 / 1 1
 isDebug 100.00% 1 / 1 100.00% 1 / 1 1
 buildContainer 100.00% 3 / 3 100.00% 1 / 1 1
 resolveContainer 44.44% 4 / 9 0.00% 0 / 1 15.40
 createContainerBuilder 50.00% 6 / 12 0.00% 0 / 1 2.50
 loadRoutes 100.00% 12 / 12 100.00% 1 / 1 5
 createHttpKernel 100.00% 8 / 8 100.00% 1 / 1 2
 registerEventListeners 100.00% 23 / 23 100.00% 1 / 1 8
52class Kernel implements HttpKernelInterface
53{
54    private ?ContainerInterface $container = null;
55
56    private ?HttpKernel $httpKernel = null;
57
58    private bool $booted = false;
59
60    private ?RouteCollection $routes = null;
61
62    public function __construct(
63        private readonly string $routingContext = 'public',
64        private readonly bool $debug = false,
65    ) {
66    }
67
68    /**
69     * Boots the Kernel: builds the DI container, loads routes, registers listeners, and creates the HttpKernel.
70     */
71    public function boot(): void
72    {
73        if ($this->booted) {
74            return;
75        }
76
77        $container = $this->buildContainer();
78        $this->container = $container;
79        ContainerRegistry::set($container);
80
81        $routes = $this->loadRoutes($container);
82        $this->routes = $routes;
83
84        $this->httpKernel = $this->createHttpKernel($container, $routes);
85        $this->booted = true;
86    }
87
88    public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response
89    {
90        if (!$this->booted) {
91            $this->boot();
92        }
93
94        // Mark API context on the request for exception listeners
95        if ($this->routingContext === 'api' || $this->routingContext === 'admin-api') {
96            $request->attributes->set('_api_context', true);
97        }
98
99        if ($this->httpKernel === null) {
100            throw new \LogicException('The Kernel booted without creating an HttpKernel.');
101        }
102
103        return $this->httpKernel->handle($request, $type, $catch);
104    }
105
106    public function getContainer(): ContainerInterface
107    {
108        if (!$this->booted) {
109            $this->boot();
110        }
111
112        if ($this->container === null) {
113            throw new \LogicException('The Kernel booted without building a container.');
114        }
115
116        return $this->container;
117    }
118
119    public function getRoutingContext(): string
120    {
121        return $this->routingContext;
122    }
123
124    public function isDebug(): bool
125    {
126        return $this->debug;
127    }
128
129    private function buildContainer(): ContainerInterface
130    {
131        $container = $this->resolveContainer();
132
133        // Register kernel-level services ('kernel' is declared synthetic in the builder)
134        $container->set('kernel', $this);
135
136        return $container;
137    }
138
139    private function resolveContainer(): ContainerInterface
140    {
141        $cacheEnabled = filter_var(Environment::get('CONTAINER_CACHE_ENABLED', 'true'), FILTER_VALIDATE_BOOLEAN);
142        $cacheDisabled = $this->debug || Environment::isDebugMode() || System::isDevelopmentVersion();
143
144        if (!$cacheEnabled || $cacheDisabled) {
145            return $this->createContainerBuilder();
146        }
147
148        /** @var mixed $configuredCacheDir */
149        $configuredCacheDir = Environment::get('CONTAINER_CACHE_DIR');
150        $cacheDir = is_string($configuredCacheDir) && trim($configuredCacheDir) !== ''
151            ? $configuredCacheDir
152            : (string) PMF_ROOT_DIR . '/cache/container';
153
154        return new ContainerCacheManager($cacheDir)->getContainer($this->createContainerBuilder(...));
155    }
156
157    private function createContainerBuilder(): ContainerBuilder
158    {
159        $containerBuilder = new ContainerBuilder();
160        $phpFileLoader = new PhpFileLoader($containerBuilder, new FileLocator(PMF_SRC_DIR));
161
162        try {
163            $phpFileLoader->load(resource: 'services.php');
164        } catch (\Throwable $exception) {
165            throw new \RuntimeException(
166                'Kernel boot failed while loading "services.php"; cannot resolve "phpmyfaq.event_dispatcher".',
167                0,
168                $exception,
169            );
170        }
171
172        // Register Forms services
173        FormsServiceProvider::register($containerBuilder);
174
175        // The Kernel instance itself is injected after the container is built or loaded
176        // from the compiled cache, so it must survive compilation as a synthetic service.
177        $containerBuilder->register('kernel')->setSynthetic(true)->setPublic(true);
178
179        return $containerBuilder;
180    }
181
182    private function loadRoutes(ContainerInterface $container): RouteCollection
183    {
184        $configurationService = $container->get(id: 'phpmyfaq.configuration');
185        $configuration = $configurationService instanceof Configuration ? $configurationService : null;
186
187        $cacheEnabled = filter_var(Environment::get('ROUTING_CACHE_ENABLED', 'true'), FILTER_VALIDATE_BOOLEAN);
188        $cacheDir = (string) Environment::get('ROUTING_CACHE_DIR', (string) PMF_ROOT_DIR . '/cache/routes');
189
190        if ($cacheEnabled && !$this->debug && !Environment::isDebugMode()) {
191            $cacheManager = new RouteCacheManager($cacheDir, Environment::isDebugMode());
192            return $cacheManager->getRoutes($this->routingContext, function () use ($configuration) {
193                $builder = new RouteCollectionBuilder($configuration);
194                return $builder->build($this->routingContext);
195            });
196        }
197
198        $builder = new RouteCollectionBuilder($configuration);
199        return $builder->build($this->routingContext);
200    }
201
202    private function createHttpKernel(ContainerInterface $container, RouteCollection $routes): HttpKernel
203    {
204        $dispatcher = $container->get('phpmyfaq.event_dispatcher');
205
206        if (!$dispatcher instanceof EventDispatcher) {
207            $dispatcher = new EventDispatcher();
208        }
209
210        $this->registerEventListeners($dispatcher, $container, $routes);
211
212        $controllerResolver = new ContainerControllerResolver($container);
213        $requestStack = new RequestStack();
214        $argumentResolver = new ArgumentResolver();
215
216        return new HttpKernel($dispatcher, $controllerResolver, $requestStack, $argumentResolver);
217    }
218
219    private function registerEventListeners(
220        EventDispatcher $dispatcher,
221        ContainerInterface $container,
222        RouteCollection $routes,
223    ): void {
224        // Language listener — initializes Strings and translations (priority 300, runs before router
225        // so that the 404/error pages rendered from an exception still have translations available)
226        $languageListener = new LanguageListener($container);
227        $dispatcher->addListener(KernelEvents::REQUEST, [$languageListener, 'onKernelRequest'], 300);
228
229        // Router listener — matches request to route (priority 256)
230        $routerListener = new RouterListener($routes);
231        $dispatcher->addListener(KernelEvents::REQUEST, [$routerListener, 'onKernelRequest'], 256);
232
233        if (
234            $this->routingContext === 'api'
235            && $container->has('phpmyfaq.configuration')
236            && $container->has('phpmyfaq.http.rate-limiter')
237        ) {
238            $rateLimiterConfiguration = $container->get('phpmyfaq.configuration');
239            $rateLimiter = $container->get('phpmyfaq.http.rate-limiter');
240            if ($rateLimiterConfiguration instanceof Configuration && $rateLimiter instanceof RateLimiter) {
241                $apiRateLimiterListener = new ApiRateLimiterListener($rateLimiterConfiguration, $rateLimiter);
242                $dispatcher->addListener(KernelEvents::REQUEST, [$apiRateLimiterListener, 'onKernelRequest'], 150);
243            }
244        }
245
246        // API exception listener — converts exceptions to RFC 7807 JSON (priority 0)
247        $configurationService = $container->has('phpmyfaq.configuration')
248            ? $container->get('phpmyfaq.configuration')
249            : null;
250        $apiExceptionListener = new ApiExceptionListener(
251            $configurationService instanceof Configuration ? $configurationService : null,
252        );
253        $dispatcher->addListener(KernelEvents::EXCEPTION, [$apiExceptionListener, 'onKernelException'], 0);
254
255        // Web exception listener — handles web (non-API) exceptions (priority -10, after API listener)
256        $webExceptionListener = new WebExceptionListener($container);
257        $dispatcher->addListener(KernelEvents::EXCEPTION, [$webExceptionListener, 'onKernelException'], -10);
258
259        // Controller container listener — injects shared container into controllers
260        $controllerContainerListener = new ControllerContainerListener($container);
261        $dispatcher->addListener(KernelEvents::CONTROLLER, [$controllerContainerListener, 'onKernelController'], 0);
262    }
263}