Lines 79.08% 344 / 435
Methods 48.78% 20 / 41
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 6 / 6 100.00% 1 / 1 1
 container 100.00% 1 / 1 100.00% 1 / 1 1
 register 100.00% 5 / 5 100.00% 1 / 1 2
 registerProviders 100.00% 2 / 2 100.00% 1 / 1 1
 bootProviders 100.00% 2 / 2 100.00% 1 / 1 2
 make 0.00% 0 / 1 0.00% 0 / 1 2
 call 0.00% 0 / 1 0.00% 0 / 1 2
 exceptions 100.00% 1 / 1 100.00% 1 / 1 1
 listen 100.00% 1 / 1 100.00% 1 / 1 1
 dispatch 100.00% 1 / 1 100.00% 1 / 1 1
 cache 100.00% 9 / 9 100.00% 1 / 1 4
 setCache 100.00% 2 / 2 100.00% 1 / 1 1
 addLoggingChannel 0.00% 0 / 1 0.00% 0 / 1 2
 getLoggingChannel 66.66% 2 / 3 0.00% 0 / 1 2.15
 boot 88.88% 16 / 18 0.00% 0 / 1 10.14
 getEnv 100.00% 1 / 1 100.00% 1 / 1 1
 getInstance 66.66% 2 / 3 0.00% 0 / 1 2.15
 loadRoutes 100.00% 4 / 4 100.00% 1 / 1 2
 executeHttpRequest 0.00% 0 / 14 0.00% 0 / 1 20
 handleHttpRequest 92.59% 25 / 27 0.00% 0 / 1 8.03
 dispatchRoute 100.00% 28 / 28 100.00% 1 / 1 2
 resolveMiddleware 66.66% 4 / 6 0.00% 0 / 1 3.33
 dispatchController 77.77% 21 / 27 0.00% 0 / 1 9.89
 requiresPsr7Request 87.50% 7 / 8 0.00% 0 / 1 5.05
 resolveModelBinding 88.46% 23 / 26 0.00% 0 / 1 9.12
 resolveBindKeys 56.25% 9 / 16 0.00% 0 / 1 13.36
 invokeBindCallable 66.66% 10 / 15 0.00% 0 / 1 12.00
 loadEnv 26.31% 5 / 19 0.00% 0 / 1 33.60
 setEnv 100.00% 5 / 5 100.00% 1 / 1 3
 configureDatabase 100.00% 21 / 21 100.00% 1 / 1 10
 resolveSqlitePath 100.00% 4 / 4 100.00% 1 / 1 4
 executeConsoleCommand 81.44% 79 / 97 0.00% 0 / 1 44.28
 loadCommands 0.00% 0 / 4 0.00% 0 / 1 6
 reset 100.00% 4 / 4 100.00% 1 / 1 2
 processArguments 100.00% 15 / 15 100.00% 1 / 1 4
 registerFallback 0.00% 0 / 1 0.00% 0 / 1 2
 requiresOptions 0.00% 0 / 1 0.00% 0 / 1 2
 setHeaders 0.00% 0 / 5 0.00% 0 / 1 12
 responseWithError 96.42% 27 / 28 0.00% 0 / 1 7
 registerErrorTemplate 100.00% 1 / 1 100.00% 1 / 1 1
 registerGlobalMiddleware 100.00% 1 / 1 100.00% 1 / 1 1
60class Application
61{
62    /**
63     * HTTP router instance for handling web requests
64     *
65     * @var HttpRouter
66     */
67    public private(set) HttpRouter $httpRouter;
68
69    /**
70     * CLI router instance for handling console commands
71     *
72     * @var CliRouter
73     */
74    public private(set) CliRouter $consoleRouter;
75
76    /**
77     * Array of registered route files
78     *
79     * @var array
80     */
81    private array $routes = [];
82
83    /**
84     * Array of registered command files
85     *
86     * @var array
87     */
88    private array $commands = [];
89
90    /**
91     * Whether the application has been booted.
92     *
93     * @var bool
94     */
95    private bool $booted = false;
96
97    /**
98     * Singleton instance of the Application
99     *
100     * @var Application|null
101     */
102    private static ?Application $instance = null;
103
104    /**
105     * The dependency injection container for this application.
106     *
107     * @var Container
108     */
109    private Container $container;
110
111    /**
112     * The application's cache store.
113     *
114     * Lazily built from the `CACHE_DRIVER` environment variable on first
115     * access, or replaced explicitly via {@see setCache()}.
116     *
117     * @var CacheInterface|null
118     */
119    private ?CacheInterface $cache = null;
120
121    /**
122     * Environment variables loaded from .env file
123     *
124     * @var array
125     */
126    private array $env;
127
128    /**
129     * Registered logging channels
130     *
131     * @var array<string, Channel>
132     */
133    public private(set) array $loggers = [];
134
135    /**
136     * Registered error pages
137     *
138     * @var array<string, ResponseInterface>
139     */
140    public private(set) array $errorPageResponses;
141
142    /**
143     * Fallback route when no error page or route is set.
144     */
145    public private(set) ?ResponseInterface $fallbackResponse;
146
147    /**
148     * An array of globally applicable middleware thats ran for all requests.
149     */
150    private array $globalMiddlewares = [];
151
152    /**
153     * Registered service providers.
154     *
155     * @var array<int, ServiceProvider>
156     */
157    private array $providers = [];
158
159    /**
160     * Initialize a new Application instance
161     *
162     * Sets up HTTP and CLI routers, ensures .env file exists,
163     * loads environment variables, registers core service providers,
164     * and initializes a null logger.
165     */
166    public function __construct()
167    {
168        //Create our router instance
169        $this->httpRouter = new HttpRouter();
170        $this->consoleRouter = new CliRouter();
171
172        //Load the env file if it exists. The .env file is expected to be
173        //created by the project (e.g. via create-project template), not by
174        //the framework itself.
175        $this->loadEnv();
176
177        $this->container = new Container();
178        $this->loggers["blank"] = new NullChannel();
179
180        // Register core service providers (clock, event dispatcher,
181        // exceptions). Their register() methods bind the shared services
182        // onto the container.
183        $this->registerProviders();
184    }
185
186    /**
187     * Get the application's dependency injection container.
188     *
189     * The container is app-scoped (created in the constructor), so it is
190     * naturally reset whenever the application singleton is replaced.
191     *
192     * @return Container The PSR-11 service container
193     */
194    public function container(): Container
195    {
196        return $this->container;
197    }
198
199    /**
200     * Register a service provider.
201     *
202     * The provider's {@see ServiceProvider::register()} is invoked immediately
203     * so its bindings are available before anything is resolved. Deferred
204     * providers are registered as usual, but their boot() is delayed until
205     * {@see bootProviders()}.
206     *
207     * @param ServiceProvider|class-string<ServiceProvider> $provider Provider instance or class name
208     * @return ServiceProvider The registered provider
209     */
210    public function register(ServiceProvider|string $provider): ServiceProvider
211    {
212        if (is_string($provider)) {
213            $provider = new $provider($this->container);
214        }
215
216        $this->providers[] = $provider;
217        $provider->register();
218
219        return $provider;
220    }
221
222    /**
223     * Register the application's core service providers.
224     *
225     * Called from the constructor after the container is created. Each core
226     * provider registers its subsystem's services on the container.
227     *
228     * @return void
229     */
230    private function registerProviders(): void
231    {
232        $this->register(EventDispatcherServiceProvider::class);
233        $this->register(ExceptionsServiceProvider::class);
234    }
235
236    /**
237     * Boot all registered service providers.
238     *
239     * Called at the start of {@see boot()}, after every provider has been
240     * registered, so providers can safely resolve services in boot().
241     *
242     * @return void
243     */
244    private function bootProviders(): void
245    {
246        foreach ($this->providers as $provider) {
247            $provider->boot();
248        }
249    }
250
251    /**
252     * Resolve a service from the container, autowiring its dependencies.
253     *
254     * Passthrough to {@see Container::make()}.
255     *
256     * @param string $abstract Identifier (class name or alias) to resolve
257     * @param array $parameters Explicit values keyed by constructor parameter name
258     * @return mixed The resolved entry
259     */
260    public function make(string $abstract, array $parameters = []): mixed
261    {
262        return $this->container->make($abstract, $parameters);
263    }
264
265    /**
266     * Invoke a callable, resolving its parameters from the container.
267     *
268     * Passthrough to {@see Container::call()}.
269     *
270     * @param callable|string|array $callback The callable to invoke
271     * @param array $parameters Explicit values keyed by parameter name
272     * @param string|null $defaultMethod Method to invoke when $callback is an invokable class string
273     * @return mixed The callable's return value
274     */
275    public function call(callable|string|array $callback, array $parameters = [], ?string $defaultMethod = null): mixed
276    {
277        return $this->container->call($callback, $parameters, $defaultMethod);
278    }
279
280    /**
281     * Resolve the shared exception manager from the container.
282     *
283     * The manager is registered as a lazy singleton, so it is instantiated on
284     * first use and the same instance is returned on every call.
285     *
286     * @return Exceptions The shared exception manager
287     */
288    public function exceptions(): Exceptions
289    {
290        return $this->container->get(Exceptions::class);
291    }
292
293    /**
294     * Register a listener for an event.
295     *
296     * Convenience passthrough to the container-registered listener provider.
297     *
298     * @param class-string $eventClass Event class (or parent class / interface) to listen for
299     * @param callable|string $listener Callable, or class-string of an invokable listener
300     * @param int $priority Higher priorities run first; defaults to 0
301     * @return void
302     */
303    public function listen(string $eventClass, callable|string $listener, int $priority = 0): void
304    {
305        $this->container->get(ListenerProvider::class)->listen($eventClass, $listener, $priority);
306    }
307
308    /**
309     * Dispatch an event to its registered listeners.
310     *
311     * Convenience passthrough to the container-registered event dispatcher.
312     *
313     * @param object $event The event to dispatch
314     * @return object The event, possibly modified by listeners
315     */
316    public function dispatch(object $event): object
317    {
318        return $this->container->get(EventDispatcherInterface::class)->dispatch($event);
319    }
320
321    /**
322     * Get the application's cache store.
323     *
324     * Builds the store lazily on first access from the `CACHE_DRIVER`
325     * environment variable (defaulting to `file`), then registers it on the
326     * container under {@see CacheInterface::class} so it can be resolved via
327     * dependency injection. The same instance is returned on subsequent calls.
328     *
329     * @return CacheInterface The cache store
330     */
331    public function cache(): CacheInterface
332    {
333        if ($this->cache === null) {
334            $driver = $this->env['CACHE_DRIVER'] ?? 'file';
335            $path = $this->env['CACHE_PATH'] ?? 'storage/cache';
336
337            $this->cache = CacheFactory::create($driver, $this->container, $path);
338
339            if ($this->cache instanceof Cache) {
340                $defaultTtl = $this->env['CACHE_DEFAULT_TTL'] ?? null;
341                $this->cache->setDefaultTtl($defaultTtl === null ? null : (int) $defaultTtl);
342            }
343
344            $this->container->instance(CacheInterface::class, $this->cache);
345        }
346
347        return $this->cache;
348    }
349
350    /**
351     * Replace the application's cache store.
352     *
353     * This is the injection point for third-party cache implementations: any
354     * object implementing {@see CacheInterface} can be supplied here. The
355     * replacement is also registered on the container under
356     * {@see CacheInterface::class}, so dependency-injected consumers resolve
357     * the new store.
358     *
359     * @param CacheInterface $cache The cache store to use
360     * @return void
361     */
362    public function setCache(CacheInterface $cache): void
363    {
364        $this->cache = $cache;
365        $this->container->instance(CacheInterface::class, $cache);
366    }
367
368    /**
369     * Register a new logging channel.
370     *
371     * By default the channel is registered under its own name (see
372     * Channel::getName()), which is also the key used by getLoggingChannel().
373     * Pass $name to override the registry key.
374     *
375     * @param Channel $log Logger instance
376     * @param string|null $name Optional override for the registry key
377     * @return void
378     */
379    public function addLoggingChannel(Channel $log, ?string $name = null): void
380    {
381        $this->loggers[$name ?? $log->getName()] = $log;
382    }
383
384    /**
385     * Get a logging channel by key
386     *
387     * Returns the null logger if the requested channel doesn't exist
388     *
389     * @param string $key Channel identifier
390     * @return Channel Logger instance
391     */
392    public function getLoggingChannel(string $key): Channel
393    {
394        if (!array_key_exists($key, $this->loggers)) {
395            return $this->loggers["blank"];
396        }
397        return $this->loggers[$key];
398    }
399
400    /**
401     * Boot the application.
402     *
403     * Loads all registered routes and commands, then sets up the database
404     * logger. When $autoLoadRoutes / $autoLoadCommands are true (the
405     * default), the framework auto-discovers files in the project's
406     * `routes/` and `commands/` directories (top-level, non-recursive).
407     *
408     * Pass false to either param to opt out of auto-discovery and manage
409     * loading explicitly via loadRoutes() / CommandLine::register().
410     *
411     * Idempotent: a second call is a no-op (see $booted guard).
412     *
413     * @param bool $autoLoadRoutes   Auto-scan RUNNING_LOCATION/routes/*.php
414     * @param bool $autoLoadCommands Auto-scan RUNNING_LOCATION/commands/*.php
415     * @return void
416     */
417    public function boot(bool $autoLoadRoutes = true, bool $autoLoadCommands = true): void
418    {
419        if ($this->booted) {
420            return;
421        }
422        $this->booted = true;
423
424        // Boot registered service providers now that every provider has been
425        // registered, so they can safely resolve services.
426        $this->bootProviders();
427
428        // Auto-discover route files from the project's routes/ directory.
429        if ($autoLoadRoutes) {
430            $routesDir = FileSystem::rootPath() . DIRECTORY_SEPARATOR . 'routes' . DIRECTORY_SEPARATOR;
431            if (is_dir($routesDir)) {
432                foreach (glob($routesDir . '*.php') as $routeFile) {
433                    $this->httpRouter->loadRoutes($routeFile);
434                }
435            }
436        }
437
438        // Load explicitly registered route files.
439        foreach ($this->routes as $route) {
440            $this->httpRouter->loadRoutes($route["file"]);
441        }
442
443        // Auto-discover command files from the project's commands/ directory.
444        if ($autoLoadCommands) {
445            $commandsDir = FileSystem::rootPath() . DIRECTORY_SEPARATOR . 'commands' . DIRECTORY_SEPARATOR;
446            if (is_dir($commandsDir)) {
447                foreach (glob($commandsDir . '*.php') as $commandFile) {
448                    require_once $commandFile;
449                }
450            }
451        }
452
453        // Load explicitly registered command files.
454        foreach ($this->commands as $command) {
455            require_once $command;
456        }
457    }
458
459    /**
460     * Get environment variables
461     *
462     * @return array Environment variables
463     */
464    public function getEnv(): array
465    {
466        return $this->env;
467    }
468
469    /**
470     * Get or create the singleton Application instance
471     *
472     * @return Application The singleton instance
473     */
474    public static function getInstance(): Application
475    {
476        if (Application::$instance == null) {
477            Application::$instance = new Application();
478        }
479
480        return Application::$instance;
481    }
482
483    /**
484     * Register a route file
485     *
486     * @param string $route Path to route file
487     * @return void
488     */
489    public function loadRoutes(string $route): void
490    {
491        // Resolve against the project root so boot() always passes a real
492        // absolute filesystem path to the router.  This handles both
493        // bare relative paths ("routes/web.php") and paths that look
494        // absolute but are really project-relative ("/routes/web.php").
495        if (!str_starts_with($route, FileSystem::rootPath())) {
496            $route = FileSystem::rootPath() . DIRECTORY_SEPARATOR
497                . ltrim($route, DIRECTORY_SEPARATOR);
498        }
499        $this->routes[] = ["file" => $route];
500    }
501
502    /**
503     * Execute an HTTP request
504     *
505     * Process incoming HTTP request and emit the response using streaming-aware
506     * emission (reads body stream in a loop with flush, handling both regular
507     * and streaming/SSE responses uniformly).
508     *
509     * @return string html body
510     */
511    public function executeHttpRequest(): string
512    {
513        $request = ServerRequest::capture();
514        $response = $this->handleHttpRequest($request);
515
516        http_response_code($response->getStatusCode());
517        $this->setHeaders($response->getHeaders());
518
519        // Streaming-aware emission
520        $body = $response->getBody();
521        if ($body->isSeekable()) {
522            $body->rewind();
523        }
524
525        $chunkSize = 8192;
526        while (! $body->eof()) {
527            echo $body->read($chunkSize);
528            if (ob_get_level() > 0) {
529                ob_flush();
530            }
531            flush();
532        }
533
534        return '';
535    }
536
537
538    /**
539     * Handle an HTTP request and return a PSR-7 ResponseInterface.
540     *
541     * Runs the PSR-15 middleware pipeline and dispatches the controller.
542     * The request is provided by the caller (see executeHttpRequest(), which
543     * builds one from globals, or the MakeRequest test trait).
544     *
545     * @param ServerRequestInterface $request The PSR-7 request to handle
546     * @return ResponseInterface
547     */
548    public function handleHttpRequest(ServerRequestInterface $request): ResponseInterface
549    {
550        try {
551            $this->boot(true, true);
552
553            // Global middleware runs for EVERY request, including routing
554            // failures (404/403) and dispatch errors (500). It wraps the
555            // fallback handler below, so it sees every response the app
556            // produces and may short-circuit with its own response.
557            $middlewareList = [];
558            foreach ($this->globalMiddlewares as $middleware) {
559                $middlewareList[] = $this->resolveMiddleware($middleware);
560            }
561
562            // Fallback handler: route lookup + dispatch + error conversion all
563            // happen INSIDE the pipeline so global middleware wraps the errors too.
564            $fallback = new CallbackRequestHandler(function (ServerRequestInterface $req): ResponseInterface {
565                try {
566                    // dispatchRoute() attaches route attributes (routeInfo,
567                    // urlVars) to the request passed down the pipeline.
568                    return $this->dispatchRoute($req);
569                } catch (Throwable $throwable) {
570                    $this->exceptions()->reportException($throwable, $req);
571
572                    $response = $this->exceptions()->renderException($throwable, $req);
573                    if ($response instanceof ResponseInterface) {
574                        return $response;
575                    }
576
577                    // Preserve the 4xx status for HttpException, else 500.
578                    $status = $throwable instanceof HttpException
579                        ? $throwable->getStatus()
580                        : HttpStatus::SERVER_ERROR;
581
582                    return $this->responseWithError($status, $throwable);
583                }
584            });
585
586            $pipeline = new MiddlewarePipeline($middlewareList, $fallback);
587
588            return $pipeline->handle($request);
589        } catch (Throwable $throwable) {
590            // Exceptions thrown by global middleware itself still produce a
591            // 500 response rather than escaping the request handler.
592            $this->exceptions()->reportException($throwable, $request);
593
594            $response = $this->exceptions()->renderException($throwable, $request);
595            if ($response instanceof ResponseInterface) {
596                return $response;
597            }
598
599            // Preserve the 4xx status for HttpException, else 500.
600            $status = $throwable instanceof HttpException
601                ? $throwable->getStatus()
602                : HttpStatus::SERVER_ERROR;
603
604            return $this->responseWithError($status, $throwable);
605        }
606    }
607
608    /**
609     * Look up a route and dispatch it to its controller.
610     *
611     * Runs inside the global middleware pipeline's fallback handler, so route
612     * lookup failures (404/403) and dispatch errors are converted to error
613     * responses that global middleware still wraps.
614     *
615     * Route info and URL vars are attached as PSR-7 attributes here, so they
616     * are visible to route-scoped middleware and the controller, but NOT to
617     * global middleware (which runs before routing).
618     *
619     * @param ServerRequestInterface $request The PSR-7 request
620     * @return ResponseInterface
621     */
622    private function dispatchRoute(ServerRequestInterface $request): ResponseInterface
623    {
624        $routeData = $this->httpRouter->AnalyseRouteAndLookup(
625            $this->httpRouter->GetUriAsArray($request->getUri()->getPath()),
626            $request->getMethod()
627        );
628
629        $controller = $this->container->make($routeData["controller"]);
630
631        // Store route info and URL vars as PSR-7 attributes
632        $routeInfo = new RouteInfo(
633            $routeData["controller"],
634            $routeData["method"],
635            $routeData["route"],
636            $request->getMethod(),
637            $routeData["variables"]
638        );
639        $request = $request
640            ->withAttribute('routeInfo', $routeInfo)
641            ->withAttribute('urlVars', $routeData["variables"]);
642
643        $method = new ReflectionMethod($routeData["controller"], $routeData["method"]);
644
645        // Build route-scoped middleware pipeline
646        $middlewareList = [];
647        foreach ($routeData["middleware"] as $middleware) {
648            $middlewareList[] = $this->resolveMiddleware($middleware);
649        }
650
651        // Build controller dispatch callback
652        $dispatchCallback = function (ServerRequestInterface $request) use (
653            $method,
654            $routeData,
655            $controller
656        ): ResponseInterface {
657            return $this->dispatchController($request,  $method, $routeData, $controller);
658        };
659
660        // Wrap dispatch callback as a PSR-15 RequestHandlerInterface
661        $pipeline = new MiddlewarePipeline($middlewareList, new CallbackRequestHandler($dispatchCallback));
662
663        return $pipeline->handle($request);
664    }
665
666    /**
667     * Resolve a middleware entry (instance or class-string) to a MiddlewareInterface.
668     *
669     * @param MiddlewareInterface|string $middleware Middleware instance or class name
670     * @return MiddlewareInterface
671     */
672    private function resolveMiddleware(MiddlewareInterface|string $middleware): MiddlewareInterface
673    {
674        if ($middleware instanceof MiddlewareInterface) {
675            return $middleware;
676        }
677
678        $object = new $middleware();
679        if ($object instanceof MiddlewareInterface) {
680            return $object;
681        }
682
683        throw new \RuntimeException('Unknown middleware type: ' . get_class($object));
684    }
685
686    /**
687     * Dispatch a matched route to its controller method.
688     *
689     * The controller is resolved from the container (constructor injection),
690     * the PSR-7 request and services are injected, model binding is applied
691     * for route parameters, the method is invoked via the container's
692     * {@see call()}, and the returned ResponseInterface is validated.
693     *
694     * @param ServerRequestInterface $request The PSR-7 request
695     * @param ReflectionMethod $method Controller method to invoke
696     * @param array $routeData Matched route data
697     * @param object $controller The container-resolved controller instance
698     * @return ResponseInterface
699     */
700    private function dispatchController(
701        ServerRequestInterface $request,
702        ReflectionMethod $method,
703        array $routeData,
704        object $controller
705    ): ResponseInterface {
706        // Check if method requires a PSR-7 request parameter
707        $psr7Injection = $this->requiresPsr7Request($method);
708
709        $variables = $routeData["variables"];
710
711        if ($psr7Injection !== null) {
712            $variables[$psr7Injection] = $request;
713        }
714
715        // Apply #[Bind] route model binding for parameters that opt in.
716        // A Model type-hint WITHOUT #[Bind] is never auto-resolved from the
717        // URL â€” the container resolves it (or the controller fetches it).
718        foreach ($method->getParameters() as $parameter) {
719            $type = $parameter->getType();
720
721            if ($type === null || !($type instanceof ReflectionNamedType)) {
722                continue;
723            }
724
725            $typeName = $type->getName();
726
727            if (!is_subclass_of($typeName, Model::class)) {
728                continue;
729            }
730
731            $instance = $this->resolveModelBinding(
732                $parameter,
733                $typeName,
734                $routeData["variables"],
735                $request,
736            );
737            if ($instance !== null) {
738                $variables[$parameter->getName()] = $instance;
739            }
740        }
741
742        $result = $this->container->call([$controller, $method->getName()], $variables);
743
744        if ($result instanceof ResponseInterface) {
745            return $result;
746        }
747
748        throw new \RuntimeException(sprintf(
749            'Controller must return a %s, got %s.',
750            ResponseInterface::class,
751            is_object($result) ? get_class($result) : gettype($result)
752        ));
753    }
754
755    /**
756     * Check if a method requires a PSR-7 ServerRequestInterface parameter.
757     *
758     * @param ReflectionMethod $method Method to check
759     * @return string|null Parameter name that should receive the ServerRequest, or null if none
760     */
761    private function requiresPsr7Request(ReflectionMethod $method): ?string
762    {
763        foreach ($method->getParameters() as $parameter) {
764            $type = $parameter->getType();
765            if ($type === null) {
766                continue;
767            }
768            $name = $type->getName();
769            if ($name === ServerRequestInterface::class || $name === ServerRequest::class) {
770                return $parameter->getName();
771            }
772        }
773        return null;
774    }
775
776    /**
777     * Resolve a #[Bind] route model binding for a controller parameter.
778     *
779     * Returns null when the parameter carries no #[Bind] attribute â€” the
780     * Model parameter is then left for the container to resolve (or the
781     * controller to fetch). A parameter WITH #[Bind] that cannot be resolved
782     * throws HttpException(NOT_FOUND).
783     *
784     * @param \ReflectionParameter $parameter The controller parameter
785     * @param class-string<Model> $type The parameter's model class
786     * @param array<string, mixed> $vars The matched route variables
787     * @param ServerRequestInterface $request The current request â€” passed to
788     *        resolve/scope/connection callables as their context argument
789     * @return Model|null The bound model, or null when no #[Bind] is present
790     * @throws ModelBindingException When the lookup finds no row
791     */
792    private function resolveModelBinding(
793        \ReflectionParameter $parameter,
794        string $type,
795        array $vars,
796        ServerRequestInterface $request,
797    ): ?Model {
798        $attr = $parameter->getAttributes(Bind::class)[0] ?? null;
799        if ($attr === null) {
800            return null; // no #[Bind] â†’ no auto-binding
801        }
802
803        $bind = $attr->newInstance();
804        $name = $parameter->getName(); // route variable is always the parameter name
805
806        if (!array_key_exists($name, $vars)) {
807            throw new \InvalidArgumentException("Route variable '{$name}' not found for #[Bind]");
808        }
809
810        $value = $vars[$name];
811        $keys = $this->resolveBindKeys($bind, $type, $vars, $value, $request);
812
813        $resolve = function () use ($type, $keys, $bind, $vars, $value, $request) {
814            $query = $type::newQuery();
815
816            foreach ($keys as $column => $columnValue) {
817                $query = $query->where($column, '=', $columnValue);
818            }
819
820            if ($bind->scope !== null) {
821                $query = self::invokeBindCallable($bind->scope, [$query, $value, $vars, $request]);
822            }
823
824            return $query->first();
825        };
826
827        // Connection splitting: run the lookup on the named connection and
828        // restore the previous active connection afterwards (no bleed).
829        $conn = $bind->connection;
830        if ($conn !== null && !is_string($conn)) {
831            $conn = self::invokeBindCallable($conn, [$vars, $request]);
832        }
833
834        $instance = $conn !== null
835            ? Database::usingConnection($conn, $resolve)
836            : $resolve();
837
838        if ($instance === null) {
839            throw new ModelBindingException($type, $keys);
840        }
841
842        return $instance;
843    }
844
845    /**
846     * Resolve the [column => value] map for a #[Bind] binding.
847     *
848     * @param Bind $bind The attribute instance
849     * @param class-string<Model> $type The model class
850     * @param array<string, mixed> $vars The matched route variables
851     * @param mixed $value The route variable's value
852     * @param ServerRequestInterface $request The current request â€” passed to
853     *        resolve callables as their context argument
854     * @return array<string, mixed>
855     * @throws \InvalidArgumentException On a composite PK without an explicit resolve,
856     *         a missing route variable, or an empty resolve-callback map
857     */
858    private function resolveBindKeys(Bind $bind, string $type, array $vars, mixed $value, ServerRequestInterface $request): array
859    {
860        // Explicit callable: the developer declares every key part's source.
861        if ($bind->resolve !== null && !is_string($bind->resolve)) {
862            $map = self::invokeBindCallable($bind->resolve, [$vars, $request]);
863            if (!is_array($map) || $map === []) {
864                throw new \InvalidArgumentException('#[Bind] resolve callback must return a non-empty [column => value] array');
865            }
866            return $map;
867        }
868
869        // Explicit column name: bind that column to the route variable.
870        if (is_string($bind->resolve)) {
871            return [$bind->resolve => $value];
872        }
873
874        // Default: single primary key bound to the route variable.
875        $pks = array_map(
876            fn($pk) => $pk->name,
877            MetadataFactory::for($type)->primaryKeys,
878        );
879
880        if (count($pks) !== 1 || $pks[0] === null) {
881            throw new \InvalidArgumentException(
882                "Model {$type} has a composite primary key; provide #[Bind(resolve: SomeResolver::class)]"
883            );
884        }
885
886        return [$pks[0] => $value];
887    }
888
889    /**
890     * Invoke a #[Bind] callable argument.
891     *
892     * Attribute arguments must be constant expressions, so callables arrive
893     * in one of these forms:
894     *
895     * - an invokable class-string (`SomeScope::class`) â€” instantiated and
896     *   invoked;
897     * - a `[Class::class, 'method']` array of constants â€” instantiated and
898     *   the named method invoked;
899     * - a Closure (only when the Bind is constructed programmatically â€”
900     *   closures are a compile error inside attribute arguments).
901     *
902     * @param mixed $callable Invokable class-string, [class, method] array, or Closure
903     * @param array<int, mixed> $args Arguments to invoke with
904     * @return mixed The callable's return value
905     * @throws \InvalidArgumentException When the value is not callable
906     */
907    private static function invokeBindCallable(mixed $callable, array $args): mixed
908    {
909        if (is_string($callable) && class_exists($callable)) {
910            $callable = new $callable();
911        } elseif (is_array($callable)
912            && array_keys($callable) === [0, 1]
913            && is_string($callable[0])
914            && is_string($callable[1])
915            && class_exists($callable[0])
916        ) {
917            $callable = [new $callable[0](), $callable[1]];
918        }
919
920        if (!is_callable($callable)) {
921            throw new \InvalidArgumentException(
922                '#[Bind] callable arguments must be an invokable class-string, a '
923                . "[Class::class, 'method'] array, or a Closure; got "
924                . get_debug_type($callable) . '.'
925            );
926        }
927
928        return $callable(...$args);
929    }
930
931    /**
932     * Load environment variables from a .env file
933     *
934     * Parses the .env file and populates the env property with key-value pairs.
935     * Handles empty lines, comments, and quoted values.
936     *
937     * The loaded values replace the current in-memory environment (the file is
938     * the source of truth). Keys are normalised to upper-case, matching
939     * {@see setEnv()}. To overlay individual keys on top of the existing
940     * environment instead, use {@see setEnv()}.
941     *
942     * @param string|null $path Optional path to the .env file. Defaults to
943     *                          FileSystem::rootPath()/.env.
944     * @return void
945     */
946    public function loadEnv(?string $path = null): void
947    {
948
949        $envPath = $path ?? FileSystem::rootPath() . DIRECTORY_SEPARATOR . ".env";
950        $output = [];
951
952        if (file_exists($envPath)) {
953            $file = fopen($envPath, "r");
954
955            if ($file) {
956                while (($line = fgets($file)) !== false) {
957                    // Skip comments and empty lines
958                    $line = trim($line);
959                    if (empty($line) || str_starts_with($line, '#')) {
960                        continue;
961                    }
962
963                    // Find position of first equals sign
964                    $pos = strpos($line, '=');
965                    if ($pos !== false) {
966                        $key = trim(substr($line, 0, $pos));
967                        $value = trim(substr($line, $pos + 1));
968
969                        // Remove quotes if present
970                        $value = trim($value, '"\'');
971
972                        if (!empty($key)) {
973                            $output[strtoupper($key)] = $value;
974                        }
975                    }
976                }
977                fclose($file);
978            }
979        }
980
981        $this->env = $output;
982        $this->configureDatabase();
983    }
984
985    /**
986     * Set environment variables in memory.
987     *
988     * Populates the in-memory environment without touching any .env file on
989     * disk. Keys are normalised to upper-case and values cast to string.
990     *
991     * By default the given values are merged into the existing environment. Pass
992     * $merge = false to replace the entire environment with $values instead.
993     *
994     * Re-configures the database layer with the resulting environment, so it can
995     * be used to switch database drivers at runtime (e.g. in tests) without
996     * writing a .env file.
997     *
998     * @param array $values Key-value pairs to set.
999     * @param bool  $merge  Whether to merge into the existing environment
1000     *                      (true) or replace it entirely (false).
1001     * @return void
1002     */
1003    public function setEnv(array $values, bool $merge = true): void
1004    {
1005        $normalised = [];
1006        foreach ($values as $key => $value) {
1007            $normalised[strtoupper($key)] = (string) $value;
1008        }
1009
1010        $this->env = $merge ? array_merge($this->env, $normalised) : $normalised;
1011        $this->configureDatabase();
1012    }
1013
1014    /**
1015     * Build the Radiant DatabaseManager from the environment and inject it
1016     * into the Radiant Database facade.
1017     *
1018     * Reads the DB_* environment variables (DB_DRIVER, DB_HOST, DB_PORT,
1019     * DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_CHARSET) into the Radiant
1020     * connection config shape. For the sqlite driver, DB_DATABASE is
1021     * resolved to an absolute filesystem path (non-absolute paths are
1022     * resolved against {@see FileSystem::rootPath()}, `:memory:` passes
1023     * through untouched) â€” Radiant's connector consumes the value as a
1024     * DSN path verbatim. Called from loadEnv() and setEnv(), so the
1025     * database layer re-configures whenever the environment changes (e.g.
1026     * switching drivers at runtime in tests).
1027     *
1028     * An existing manager is NEVER replaced â€” runtime customizations (extra
1029     * named connections, extended connectors) survive an env reload:
1030     *
1031     * - manager with a `default` connection: the config is ALTERED via
1032     *   {@see DatabaseManager::setConnectionConfig()} â€” the evicted
1033     *   connection is rebuilt lazily on next use, so a changed config
1034     *   (e.g. a new sqlite path) takes full effect;
1035     * - manager WITHOUT a `default` connection (not built by Lucent): the
1036     *   config is registered via addConnection() and made active via
1037     *   {@see DatabaseManager::useConnection()} â€” switching the active
1038     *   connection is what makes the env config the default.
1039     *
1040     * Note: when DB_DRIVER is absent from the environment, the existing
1041     * manager is left untouched â€” an env reload cannot un-configure the
1042     * database layer.
1043     *
1044     * @return void
1045     */
1046    private function configureDatabase(): void
1047    {
1048        $driver = $this->env['DB_DRIVER'] ?? null;
1049
1050        if ($driver === null || $driver === '') {
1051            return; // no database configured â€” nothing to wire
1052        }
1053
1054        $config = ['driver' => $driver];
1055
1056        foreach (['host', 'port', 'database', 'username', 'password', 'charset'] as $key) {
1057            $envKey = 'DB_' . strtoupper($key);
1058            if (isset($this->env[$envKey]) && $this->env[$envKey] !== '') {
1059                $config[$key] = $key === 'port' ? (int) $this->env[$envKey] : $this->env[$envKey];
1060            }
1061        }
1062
1063        if ($driver === 'sqlite') {
1064            $this->resolveSqlitePath($config);
1065        }
1066
1067        if (!\BlueprintAU\Radiant\Database::hasManager()) {
1068            // No manager yet â€” build one with Lucent's default connection.
1069            \BlueprintAU\Radiant\Database::setManager(
1070                new \BlueprintAU\Radiant\Database\DatabaseManager(['default' => $config], 'default'),
1071            );
1072            return;
1073        }
1074
1075        $manager = \BlueprintAU\Radiant\Database::manager();
1076
1077        if ($manager->hasConnection('default')) {
1078            // Evicts the resolved connection (rolling back any open
1079            // transaction first); the next use rebuilds from the new config.
1080            $manager->setConnectionConfig('default', $config);
1081            return;
1082        }
1083
1084        // A manager exists but was not built by Lucent (no `default`
1085        // connection) â€” register the env config and make it the active
1086        // connection. addConnection() alone would leave the manager using
1087        // ITS OWN active connection; useConnection() is the switch.
1088        $manager->addConnection('default', $config);
1089        $manager->useConnection('default');
1090    }
1091
1092    /**
1093     * Resolve the sqlite "database" config value against the project root.
1094     *
1095     * Radiant's SqliteConnector consumes the `database` value verbatim as a
1096     * PDO DSN path â€” it has no knowledge of the application's root
1097     * directory. This restores the pre-Radiant behaviour (and matches
1098     * Laravel's SQLiteConnector, which resolves via base_path()): relative
1099     * paths such as "storage/database.sqlite" are resolved against
1100     * {@see FileSystem::rootPath()}, absolute paths pass through, and the
1101     * `:memory:` in-memory sentinel is handed to the connector untouched.
1102     * `..` segments are collapsed lexically (no filesystem access, so the
1103     * path may not exist yet).
1104     *
1105     * @param  array<string, mixed>  $config  The default connection config;
1106     *         `database` is replaced in place when it is a relative path.
1107     * @return void
1108     */
1109    private function resolveSqlitePath(array &$config): void
1110    {
1111        $path = $config['database'] ?? null;
1112
1113        if (!is_string($path) || $path === '' || $path === ':memory:') {
1114            return; // absent/invalid lets the connector's fail-fast validation handle it
1115        }
1116
1117        $config['database'] = FileSystem::normalizePath(FileSystem::absolutePath($path));
1118    }
1119
1120    /**
1121     * Execute a console command
1122     *
1123     * Registers built-in commands, analyzes the command input,
1124     * validates the controller and method, and executes the command.
1125     *
1126     * @param array $args Command line arguments
1127     * @return string Command output
1128     * @throws ReflectionException
1129     */
1130    public function executeConsoleCommand(array $args = []): string
1131    {
1132        $this->boot(true, true);
1133
1134        if (!CommandLine::isCaptured()) {
1135            ob_implicit_flush(true);
1136            if (ob_get_level() > 0) {
1137                ob_end_flush();
1138            }
1139        }
1140
1141        CommandLine::register(GenerateDocumentationCommand::$command, "generateApi", GenerateDocumentationCommand::class, "Generates API documentation based on your controller attributes");
1142        CommandLine::register(SyncCommand::$command, "run", SyncCommand::class, "Sync the discovered models' schema to the database (diff-based, destructive prompts). Options: --filter= --exclude-filter= --dir= --force --dry-run --no-transactional --no-drop-tables");
1143        CommandLine::register(SyncLegacyCommand::$command, "run", SyncLegacyCommand::class, "DEPRECATED one-time migration: rename legacy table names to the Radiant naming. Options: --filter= --exclude-filter= --dir= --dry-run");
1144        CommandLine::register(StartDevServerCommand::$command, "start", StartDevServerCommand::class, "Start the built-in PHP development server");
1145        CommandLine::register(DeploymentController::$command_latest,   "latest",   DeploymentController::class, "Downloads and deploys the latest project release");
1146        CommandLine::register(DeploymentController::$command_rollback, "rollback", DeploymentController::class, "Rolls back to the most recent backup");
1147        CommandLine::register(ClearCacheCommand::$command, "clear", ClearCacheCommand::class, "Clears the application cache");
1148        if ($args === []) {
1149            $args = array_slice($_SERVER["argv"], 1);
1150            $args = str_replace("\n", "", $args);
1151        }
1152
1153        // Split colons in the COMMAND NAME (first argument) to support
1154        // "namespace:command" style invocation (e.g. "make:migration").
1155        // Other arguments (options, parameter values) are left untouched so
1156        // values like "--file=/path:with:colons" are not corrupted.
1157        $expandedArgs = [];
1158        foreach ($args as $index => $arg) {
1159            if ($index === 0 && str_contains($arg, ':')) {
1160                $parts = explode(':', $arg);
1161                foreach ($parts as $part) {
1162                    if ($part !== '') {
1163                        $expandedArgs[] = $part;
1164                    }
1165                }
1166            } else {
1167                $expandedArgs[] = $arg;
1168            }
1169        }
1170
1171        $args = $expandedArgs;
1172
1173        if ((count($args) === 1 && $args[0] === "") || count($args) === 0 || (count($args) === 1 && $args[0] === "help")) {
1174            $commands = $this->consoleRouter->getRoutes()["CLI"];
1175            $output = "\nAvailable commands:\n\n";
1176
1177            $maxLength = 0;
1178            foreach ($commands as $route => $command) {
1179                $maxLength = max($maxLength, strlen($route));
1180            }
1181
1182            foreach ($commands as $route => $command) {
1183                $description = $command["description"] ?? '';
1184                $output .= "  \033[1m" . str_pad($route, $maxLength + 4) . "\033[0m";
1185                if ($description) {
1186                    $output .= $description;
1187                }
1188                $output .= "\n";
1189            }
1190
1191            $output .= "\n";
1192            return $output;
1193        }
1194
1195        $processedArgs = $this->processArguments($args);
1196        $commandArgs = $processedArgs['args'];
1197        $options = $processedArgs['options'];
1198
1199        try {
1200            $response = $this->consoleRouter->analyseRouteAndLookup($commandArgs, CliRouter::$ROUTE_CLI);
1201
1202            $reflect = new ReflectionClass($response["controller"]);
1203            $method = $reflect->getMethod($response["method"]);
1204            $controller = $reflect->newInstance();
1205
1206            $varCount = count($response["variables"]);
1207            $filteredVariables = [];
1208            $variables = "";
1209
1210            foreach ($method->getParameters() as $param) {
1211                if ($param->getName() == "options") {
1212                    $filteredVariables["options"] = $options;
1213                    continue;
1214                }
1215
1216                $variables .= " [" . $param->getName() . "]";
1217
1218                if (array_key_exists($param->getName(), $response["variables"])) {
1219                    $filteredVariables[$param->getName()] = $response["variables"][$param->getName()];
1220                    continue;
1221                }
1222
1223                if (!$param->isDefaultValueAvailable()) {
1224                    return "Argument missing: The '" . $param->getName() . "' argument is required for this command.\nExpected format: [command] [argument_name]\nExample usage: " . $response["route"] . $variables;
1225                }
1226            }
1227
1228            if ($varCount < $method->getNumberOfRequiredParameters() || count($method->getParameters()) < $varCount) {
1229                return "Insufficient arguments! The command requires at least " . $varCount . " parameters.\nUsage: " . $response["route"] . " " . $variables;
1230            }
1231
1232            if (CommandLine::isCaptured()) {
1233                ob_start();
1234                $result = $method->invokeArgs($controller, $filteredVariables);
1235                $output = ob_get_clean();
1236
1237                if (is_string($result) && $result !== '') {
1238                    $output .= $result;
1239                }
1240
1241                return $output;
1242            }
1243
1244            $result = $method->invokeArgs($controller, $filteredVariables);
1245
1246            if (is_string($result) && $result !== '') {
1247                echo $result;
1248            }
1249
1250            return '';
1251        } catch (HttpException $e) {
1252            $commands = $this->consoleRouter->getRoutes()["CLI"] ?? [];
1253            $output = "Unrecognized command. Type '\033[1mphp cli\033[0m' to see available commands.\n";
1254
1255            $suggestions = [];
1256            $fullInput = strtolower(implode(' ', $args));
1257
1258            foreach ($commands as $route => $command) {
1259                $routeBase = preg_replace('/\s+/', ' ', trim(preg_replace('/\{[^}]+\}/', '', $route)));
1260                $routeBase = strtolower($routeBase);
1261
1262                if (str_starts_with($routeBase, $fullInput)) {
1263                    $suggestions[$route] = 0;
1264                } else if (preg_match('/\b' . preg_quote($fullInput) . '/i', $routeBase)) {
1265                    $suggestions[$route] = 1;
1266                } else {
1267                    $distance = levenshtein($fullInput, $routeBase);
1268                    $maxDistance = max(2, strlen($fullInput) / 2);
1269                    if ($distance <= $maxDistance) {
1270                        $suggestions[$route] = $distance + 10;
1271                    }
1272                }
1273            }
1274
1275            asort($suggestions);
1276            $suggestions = array_slice(array_keys($suggestions), 0, 3);
1277
1278            if (!empty($suggestions)) {
1279                $output .= "Did you mean something similar?\n\n";
1280                foreach ($suggestions as $suggestion) {
1281                    $output .= "  \033[1m" . $suggestion . "\033[0m\n";
1282                }
1283                $output .= "\n";
1284            }
1285
1286            return $output;
1287        } catch (\Throwable $e) {
1288            return ExceptionChain::render($e, "Command failed: ") . "\n";
1289        }
1290    }
1291    /**
1292     * Register a command file
1293     *
1294     * @param string $commandFile Path to command file
1295     * @return void
1296     */
1297    public function loadCommands(string $commandFile): void
1298    {
1299        if (!str_starts_with($commandFile, FileSystem::rootPath())) {
1300            $commandFile = FileSystem::rootPath() . DIRECTORY_SEPARATOR
1301                . ltrim($commandFile, DIRECTORY_SEPARATOR);
1302        }
1303        $this->commands[] = $commandFile;
1304    }
1305
1306    /**
1307     * Resets the application instance.
1308     *
1309     * Replaces the singleton with a fresh Application, which naturally
1310     * resets $booted (and all other state) to its default. Used by tests
1311     * to obtain a clean application between cases.
1312     *
1313     * @return void
1314     */
1315    public static function reset(): void
1316    {
1317        $loggers = self::$instance?->loggers ?? [];
1318        Application::$instance = new Application();
1319
1320        if (!empty($loggers)) {
1321            Application::$instance->loggers = $loggers;
1322        }
1323    }
1324
1325    /**
1326     * Processes command line arguments, separating regular arguments from options
1327     * Options are arguments that start with '--'
1328     * Options can also have values like --file=/test.php
1329     *
1330     * @param array $argv Command line arguments array
1331     * @return array Associative array with 'args' and 'options' keys
1332     */
1333    function processArguments(array $argv): array
1334    {
1335        $args = [];
1336        $options = [];
1337
1338        // Skip the script name (first argument)
1339        for ($i = 0; $i < count($argv); $i++) {
1340            $arg = $argv[$i];
1341
1342            // Check if it's an option (starts with --)
1343            if (str_starts_with($arg, '--')) {
1344                $option = substr($arg, 2); // Remove the '--'
1345
1346                // Check if it has a value with '='
1347                if (str_contains($option, '=')) {
1348                    list($key, $value) = explode('=', $option, 2);
1349                    $options[$key] = $value;
1350                } else {
1351                    // Option without value
1352                    $options[$option] = true;
1353                }
1354            } else {
1355                // Regular argument
1356                $args[] = $arg;
1357            }
1358        }
1359
1360        return [
1361            'args' => $args,
1362            'options' => $options
1363        ];
1364    }
1365
1366    public function registerFallback(ResponseInterface $response): void
1367    {
1368        $this->fallbackResponse = $response;
1369    }
1370
1371    private function requiresOptions(ReflectionMethod $method): bool
1372    {
1373        return array_any($method->getParameters(), fn($parameter) => $parameter->getName() === "options");
1374    }
1375
1376    /**
1377     * Set multiple HTTP headers from a headers array (string[][]).
1378     *
1379     * @param array<string, string[]> $headers Headers where each value is an array of strings
1380     * @param bool $replace Whether to replace previous headers with the same name (default: true)
1381     * @return void
1382     */
1383    public function setHeaders(array $headers, bool $replace = true): void
1384    {
1385        foreach ($headers as $name => $values) {
1386            $safeName = str_replace(["\r", "\n"], '', $name);
1387            foreach ($values as $value) {
1388                $safeValue = str_replace(["\r", "\n"], '', $value);
1389                header("$safeName: $safeValue", $replace);
1390            }
1391        }
1392    }
1393
1394    private function responseWithError(HttpStatus $status, ?\Throwable $throwable = null): ResponseInterface
1395    {
1396        // Check for registered error pages first
1397        if (isset($this->errorPageResponses[$status->value])) {
1398            return $this->errorPageResponses[$status->value];
1399        }
1400
1401        if ($status === HttpStatus::NOT_FOUND) {
1402            $fallback = $this->fallbackResponse ?? null;
1403            if ($fallback) {
1404                return $fallback;
1405            }
1406        }
1407
1408        $response = new Response();
1409        $response = $response->withJsonEnvelope([], $status->message(), false, $status->value);
1410
1411        // Normalise boolean env strings: only "1", "true", "on" and "yes"
1412        // (case-insensitive) are truthy; everything else is falsy.
1413        $is_debug = filter_var(App::env("DEBUG", false), FILTER_VALIDATE_BOOL);
1414
1415        if (!$is_debug) {
1416            return $response;
1417        }
1418
1419        $cause = $throwable instanceof HttpException ? $throwable->getPrevious() : $throwable;
1420
1421        if ($cause !== null) {
1422            $debugPayload = [
1423                "message" => $cause->getMessage(),
1424                "code"    => $cause->getCode(),
1425                "file"    => $cause->getFile(),
1426                "line"    => $cause->getLine(),
1427                "trace"   => $cause->getTrace(),
1428            ];
1429            $response = $response->withJsonEnvelope(
1430                [],
1431                $status->message(),
1432                false,
1433                $status->value,
1434                ['exception' => $debugPayload]
1435            );
1436        }
1437
1438        return $response;
1439    }
1440
1441    public function registerErrorTemplate(int $code, ResponseInterface $response): void
1442    {
1443        $this->errorPageResponses[$code] = $response;
1444    }
1445
1446    public function registerGlobalMiddleware(MiddlewareInterface|string $middleware): void
1447    {
1448        $this->globalMiddlewares[] = $middleware;
1449    }
1450}