Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.59% covered (success)
95.59%
325 / 340
95.06% covered (success)
95.06%
77 / 81
CRAP
0.00% covered (danger)
0.00%
0 / 1
Application
95.59% covered (success)
95.59%
325 / 340
95.06% covered (success)
95.06%
77 / 81
203
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
11
 bootstrap
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 initializeDefaultManagers
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 registerConfiguredAutoloaderPrefix
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
7
 applyConfigMetadata
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 loadHelperFunctions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 applyConfigRoutes
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 applyConfigServices
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 applyConfigEvents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
6
 applyConfigMiddleware
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 init
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 autoloader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 router
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 services
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 events
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 middleware
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 modules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerRouter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 registerServices
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 registerEvents
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 registerMiddleware
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 registerModules
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 registerAutoloader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mergeServices
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mergeMiddleware
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mergeEvents
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 mergeApplication
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 module
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 unregister
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isRegistered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addRoute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addRoutes
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 httpRouter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 head
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 post
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 put
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 trace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 options
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 connect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 patch
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addCustomMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addCustomMethods
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasCustomMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __call
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setService
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeService
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 on
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 off
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 trigger
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addMiddleware
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMiddleware
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeMiddleware
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 env
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 environment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLocal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDev
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isTesting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStaging
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isProduction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isUp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
17
 invokeDispatchable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 buildDispatch
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 resolveMiddlewareRequest
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
7.07
 hasPsr15Middleware
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 renderMaintenanceResponse
40.00% covered (warning)
40.00%
8 / 20
0.00% covered (danger)
0.00%
0 / 1
17.58
 __set
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 __get
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
9
 __isset
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
9
 __unset
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop;
16
17use Pop\Console\Console;
18use Pop\Http\Server\Request;
19use Pop\Http\Uri;
20use Pop\Utils\Arr;
21use Pop\Utils\Helper;
22
23/**
24 * Application class
25 *
26 * @category   Pop
27 * @package    Pop
28 * @author     Nick Sagona, III <nick@popphp.org>
29 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
30 * @license    https://www.popphp.org/license     New BSD License
31 * @version    5.0.0
32 * @property   mixed                              $config
33 * @property   ?Router\Router                     $router
34 * @property   ?Service\Locator                   $services
35 * @property   ?Event\Manager                     $events
36 * @property   ?Middleware\Manager                $middleware
37 * @property   ?Module\Manager                    $modules
38 * @property   ?\Composer\Autoload\ClassLoader    $autoloader
39 */
40class Application extends AbstractApplication implements \ArrayAccess
41{
42
43    /**
44     * Application router
45     * @var ?Router\Router
46     */
47    protected ?Router\Router $router = null;
48
49    /**
50     * Service locator
51     * @var ?Service\Locator
52     */
53    protected ?Service\Locator $services = null;
54
55    /**
56     * Event manager
57     * @var ?Event\Manager
58     */
59    protected ?Event\Manager $events = null;
60
61    /**
62     * Middleware manager
63     * @var ?Middleware\Manager
64     */
65    protected ?Middleware\Manager $middleware = null;
66
67    /**
68     * Module manager
69     * @var ?Module\Manager
70     */
71    protected ?Module\Manager $modules = null;
72
73    /**
74     * Autoloader
75     * @var ?\Composer\Autoload\ClassLoader
76     */
77    protected ?\Composer\Autoload\ClassLoader $autoloader = null;
78
79    /**
80     * Constructor
81     *
82     * Instantiate an application object
83     *
84     * Optional parameters are a service locator instance, a router instance,
85     * an event manager instance or a configuration object or array
86     */
87    public function __construct()
88    {
89        $args       = func_get_args();
90        $autoloader = null;
91        $config     = null;
92
93        foreach ($args as $arg) {
94            if ($arg instanceof \Composer\Autoload\ClassLoader) {
95                $autoloader = $arg;
96            } else if ($arg instanceof Router\Router) {
97                $this->registerRouter($arg);
98            } else if ($arg instanceof Service\Locator) {
99                $this->registerServices($arg);
100            } else if ($arg instanceof Event\Manager) {
101                $this->registerEvents($arg);
102            } else if ($arg instanceof Middleware\Manager) {
103                $this->registerMiddleware($arg);
104            } else if ($arg instanceof Module\Manager) {
105                $this->registerModules($arg);
106            } else if (is_array($arg) || ($arg instanceof \ArrayAccess)) {
107                $config = $arg;
108            }
109        }
110
111        if ($config !== null) {
112            $this->registerConfig($config);
113        }
114
115        $this->bootstrap($autoloader);
116    }
117
118    /**
119     * Bootstrap the application, creating the required objects if they haven't been created yet
120     * and registering with the autoloader, adding routes, services and events
121     *
122     * @param  ?\Composer\Autoload\ClassLoader $autoloader
123     * @throws Exception|Module\Exception|Service\Exception
124     * @return static
125     */
126    public function bootstrap(?\Composer\Autoload\ClassLoader $autoloader = null): static
127    {
128        if ($autoloader !== null) {
129            $this->registerAutoloader($autoloader);
130        }
131
132        $this->initializeDefaultManagers();
133        $this->registerConfiguredAutoloaderPrefix();
134        $this->applyConfigMetadata();
135        $this->loadHelperFunctions();
136        $this->applyConfigRoutes();
137        $this->applyConfigServices();
138        $this->applyConfigEvents();
139        $this->applyConfigMiddleware();
140
141        // Register application object with App helper class
142        App::set($this);
143
144        return $this;
145    }
146
147    /**
148     * Instantiate and register any manager objects not already set
149     *
150     * @return void
151     */
152    protected function initializeDefaultManagers(): void
153    {
154        if ($this->router === null) {
155            $this->registerRouter(new Router\Router());
156        }
157        if ($this->services === null) {
158            $this->registerServices(new Service\Locator());
159        }
160        if ($this->events === null) {
161            $this->registerEvents(new Event\Manager());
162        }
163        if ($this->middleware === null) {
164            $this->registerMiddleware(new Middleware\Manager());
165        }
166        if ($this->modules === null) {
167            $this->registerModules(new Module\Manager());
168        }
169    }
170
171    /**
172     * If the autoloader is set and the application config has a defined
173     * prefix and src, register with the autoloader
174     *
175     * @return void
176     */
177    protected function registerConfiguredAutoloaderPrefix(): void
178    {
179        if (($this->autoloader !== null) && isset($this->config['prefix']) &&
180            isset($this->config['src']) && file_exists($this->config['src'])) {
181            // Register as PSR-0
182            if (isset($this->config['psr-0']) && ($this->config['psr-0'])) {
183                $this->autoloader->add($this->config['prefix'], $this->config['src']);
184            // Else, default to PSR-4
185            } else {
186                $this->autoloader->addPsr4($this->config['prefix'], $this->config['src']);
187            }
188        }
189    }
190
191    /**
192     * Set the app name and version from config, if present
193     *
194     * @return void
195     */
196    protected function applyConfigMetadata(): void
197    {
198        // Set the app name
199        if (!empty($this->config['name'])) {
200            $this->setName($this->config['name']);
201        } else if (!empty(App::name())) {
202            $this->setName(App::name());
203        }
204
205        // Set the app version
206        if (!empty($this->config['version'])) {
207            $this->setVersion($this->config['version']);
208        }
209    }
210
211    /**
212     * Load helper functions, unless disabled by config
213     *
214     * @return void
215     */
216    protected function loadHelperFunctions(): void
217    {
218        if ((!isset($this->config['helper_functions']) || ($this->config['helper_functions'] === true)) && (!Helper::isLoaded())) {
219            Helper::loadFunctions();
220        }
221    }
222
223    /**
224     * If routes are set in the app config, register them with the application
225     *
226     * @return void
227     */
228    protected function applyConfigRoutes(): void
229    {
230        if (isset($this->config['routes']) && ($this->router !== null)) {
231            $this->router->addRoutes($this->config['routes']);
232        }
233    }
234
235    /**
236     * If services are set in the app config, register them with the application
237     *
238     * @return void
239     */
240    protected function applyConfigServices(): void
241    {
242        if (isset($this->config['services']) && ($this->services !== null)) {
243            foreach ($this->config['services'] as $name => $service) {
244                $this->setService($name, $service);
245            }
246        }
247    }
248
249    /**
250     * If events are set in the app config, register them with the application
251     *
252     * @return void
253     */
254    protected function applyConfigEvents(): void
255    {
256        if (isset($this->config['events']) && ($this->events !== null)) {
257            foreach ($this->config['events'] as $event) {
258                if (isset($event['name']) && isset($event['action'])) {
259                    $this->on($event['name'], $event['action'], ((int)($event['priority'] ?? 0)));
260                }
261            }
262        }
263    }
264
265    /**
266     * If middleware is defined in the app config, register them with the application
267     *
268     * @return void
269     */
270    protected function applyConfigMiddleware(): void
271    {
272        $middlewareDisabled = App::middlewareDisabled();
273
274        if (isset($this->config['middleware']) && ($this->middleware !== null) &&
275            (empty($middlewareDisabled) || ($middlewareDisabled == 'route'))) {
276            $this->middleware->addItems(Arr::make($this->config['middleware']));
277        }
278    }
279
280    /**
281     * Initialize the application
282     *
283     * @return static
284     */
285    public function init(): static
286    {
287        $this->events->dispatch(new Event\InitEvent($this));
288        return $this;
289    }
290
291    /**
292     * Get the autoloader object
293     *
294     * @return ?\Composer\Autoload\ClassLoader
295     */
296    public function autoloader(): ?\Composer\Autoload\ClassLoader
297    {
298        return $this->autoloader;
299    }
300
301    /**
302     * Access the application router
303     *
304     * @return ?Router\Router
305     */
306    public function router(): ?Router\Router
307    {
308        return $this->router;
309    }
310
311    /**
312     * Get the service locator
313     *
314     * @return ?Service\Locator
315     */
316    public function services(): ?Service\Locator
317    {
318        return $this->services;
319    }
320
321    /**
322     * Get the event manager
323     *
324     * @return ?Event\Manager
325     */
326    public function events(): ?Event\Manager
327    {
328        return $this->events;
329    }
330
331    /**
332     * Get the middleware manager
333     *
334     * @return ?Middleware\Manager
335     */
336    public function middleware(): ?Middleware\Manager
337    {
338        return $this->middleware;
339    }
340
341    /**
342     * Access all application module configs
343     *
344     * @return ?Module\Manager
345     */
346    public function modules(): ?Module\Manager
347    {
348        return $this->modules;
349    }
350
351    /**
352     * Register a new router object with the application
353     *
354     * @param  Router\Router $router
355     * @return static
356     */
357    public function registerRouter(Router\Router $router): static
358    {
359        $this->router = $router;
360        Router\Route::setRouter($router);
361        return $this;
362    }
363
364    /**
365     * Register a new service locator object with the application
366     *
367     * @param  Service\Locator $services
368     * @return static
369     */
370    public function registerServices(Service\Locator $services): static
371    {
372        $this->services = $services;
373        return $this;
374    }
375
376    /**
377     * Register a new event manager object with the application
378     *
379     * @param  Event\Manager $events
380     * @return static
381     */
382    public function registerEvents(Event\Manager $events): static
383    {
384        $this->events = $events;
385        return $this;
386    }
387
388    /**
389     * Register a new middleware manager object with the application
390     *
391     * @param  Middleware\Manager $middleware
392     * @return static
393     */
394    public function registerMiddleware(Middleware\Manager $middleware): static
395    {
396        $this->middleware = $middleware;
397        return $this;
398    }
399
400    /**
401     * Register a new module manager object with the application
402     *
403     * @param  Module\Manager $modules
404     * @return static
405     */
406    public function registerModules(Module\Manager $modules): static
407    {
408        $this->modules = $modules;
409        return $this;
410    }
411
412    /**
413     * Register the autoloader object with the application
414     *
415     * @param  \Composer\Autoload\ClassLoader $autoloader
416     * @return static
417     */
418    public function registerAutoloader(\Composer\Autoload\ClassLoader $autoloader): static
419    {
420        $this->autoloader = $autoloader;
421        return $this;
422    }
423
424    /**
425     * Merge another service locator's items into this application's service locator
426     *
427     * @param  Service\Locator $services
428     * @return static
429     */
430    public function mergeServices(Service\Locator $services): static
431    {
432        $this->services()?->addItems($services->getItems());
433        return $this;
434    }
435
436    /**
437     * Merge another middleware manager's items into this application's middleware manager
438     *
439     * @param  Middleware\Manager $middleware
440     * @return static
441     */
442    public function mergeMiddleware(Middleware\Manager $middleware): static
443    {
444        $this->middleware()?->addItems($middleware->getItems());
445        return $this;
446    }
447
448    /**
449     * Merge another event manager's listeners into this application's event manager,
450     * combining listeners for shared event names instead of replacing them
451     *
452     * @param  Event\Manager $events
453     * @return static
454     */
455    public function mergeEvents(Event\Manager $events): static
456    {
457        foreach ($events->getItems() as $name => $queue) {
458            $clone = clone $queue;
459            $clone->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
460            foreach ($clone as $entry) {
461                $this->events()?->on($name, $entry['data'], $entry['priority']);
462            }
463        }
464        return $this;
465    }
466
467    /**
468     * Merge another application's services, middleware, events and config into this application
469     *
470     * @param  Application $application
471     * @param  bool        $preserveConfig
472     * @param  array       $configExclude
473     * @return static
474     */
475    public function mergeApplication(Application $application, bool $preserveConfig = false, array $configExclude = []): static
476    {
477        if ($application->services() !== null) {
478            $this->mergeServices($application->services());
479        }
480        if ($application->middleware() !== null) {
481            $this->mergeMiddleware($application->middleware());
482        }
483        if ($application->events() !== null) {
484            $this->mergeEvents($application->events());
485        }
486        if ($application->config() !== null) {
487            $this->mergeConfig($application->config(), $preserveConfig, $configExclude);
488        }
489
490        return $this;
491    }
492
493    /**
494     * Access a module object
495     *
496     * @param  string $name
497     * @return ?Module\ModuleInterface
498     */
499    public function module(string $name): ?Module\ModuleInterface
500    {
501        return $this->modules[$name] ?? null;
502    }
503
504    /**
505     * Register a module with the module manager object
506     *
507     * @param  mixed   $module
508     * @param  ?string $name
509     * @throws Module\Exception|Service\Exception
510     * @return static
511     */
512    public function register(mixed $module, ?string $name = null): static
513    {
514        if (!($module instanceof Module\ModuleInterface)) {
515            // Deliberately not passed $this - Module's constructor
516            // self-registers immediately when given an application, which
517            // would lock in its default/config name before setName() below
518            // gets a chance to override it.
519            $module = new Module\Module($module);
520        }
521
522        if ($name !== null) {
523            $module->setName($name);
524        }
525
526        if (!$module->isRegistered()) {
527            $module->register($this);
528        }
529
530        return $this;
531    }
532
533    /**
534     * Unregister a module with the module manager object
535     *
536     * @param  string $name
537     * @return static
538     */
539    public function unregister(string $name): static
540    {
541        unset($this->modules[$name]);
542        return $this;
543    }
544
545    /**
546     * Determine whether a module is registered with the application object
547     *
548     * @param  string $name
549     * @return bool
550     */
551    public function isRegistered(string $name): bool
552    {
553        return $this->modules->isRegistered($name);
554    }
555
556    /**
557     * Add a route
558     *
559     * @param  string $route
560     * @param  mixed  $controller
561     * @return static
562     */
563    public function addRoute(string $route, mixed $controller): static
564    {
565        $this->router->addRoute($route, $controller);
566        return $this;
567    }
568
569    /**
570     * Add routes
571     *
572     * @param  array $routes
573     * @return static
574     */
575    public function addRoutes(array $routes): static
576    {
577        $this->router->addRoutes($routes);
578        return $this;
579    }
580
581    /**
582     * Get the active HTTP router, guarding that the application is routed for HTTP
583     *
584     * @throws Exception
585     * @return Router\Router
586     */
587    protected function httpRouter(): Router\Router
588    {
589        if (($this->router === null) || (!$this->router->isHttp())) {
590            throw new Exception('Error: The application is not routed for HTTP.');
591        }
592        return $this->router;
593    }
594
595    /**
596     * Add a GET route
597     *
598     * @param  string $route
599     * @param  mixed  $controller
600     * @throws Exception
601     * @return static
602     */
603    public function get(string $route, mixed $controller): static
604    {
605        $this->httpRouter()->get($route, $controller);
606        return $this;
607    }
608
609    /**
610     * Add a HEAD route
611     *
612     * @param  string $route
613     * @param  mixed  $controller
614     * @throws Exception
615     * @return static
616     */
617    public function head(string $route, mixed $controller): static
618    {
619        $this->httpRouter()->head($route, $controller);
620        return $this;
621    }
622
623    /**
624     * Add a POST route
625     *
626     * @param  string $route
627     * @param  mixed  $controller
628     * @throws Exception
629     * @return static
630     */
631    public function post(string $route, mixed $controller): static
632    {
633        $this->httpRouter()->post($route, $controller);
634        return $this;
635    }
636
637    /**
638     * Add a PUT route
639     *
640     * @param  string $route
641     * @param  mixed  $controller
642     * @throws Exception
643     * @return static
644     */
645    public function put(string $route, mixed $controller): static
646    {
647        $this->httpRouter()->put($route, $controller);
648        return $this;
649    }
650
651    /**
652     * Add a DELETE route
653     *
654     * @param  string $route
655     * @param  mixed  $controller
656     * @throws Exception
657     * @return static
658     */
659    public function delete(string $route, mixed $controller): static
660    {
661        $this->httpRouter()->delete($route, $controller);
662        return $this;
663    }
664
665    /**
666     * Add a TRACE route
667     *
668     * @param  string $route
669     * @param  mixed  $controller
670     * @throws Exception
671     * @return static
672     */
673    public function trace(string $route, mixed $controller): static
674    {
675        $this->httpRouter()->trace($route, $controller);
676        return $this;
677    }
678
679    /**
680     * Add an OPTIONS route
681     *
682     * @param  string $route
683     * @param  mixed  $controller
684     * @throws Exception
685     * @return static
686     */
687    public function options(string $route, mixed $controller): static
688    {
689        $this->httpRouter()->options($route, $controller);
690        return $this;
691    }
692
693    /**
694     * Add a CONNECT route
695     *
696     * @param  string $route
697     * @param  mixed  $controller
698     * @throws Exception
699     * @return static
700     */
701    public function connect(string $route, mixed $controller): static
702    {
703        $this->httpRouter()->connect($route, $controller);
704        return $this;
705    }
706
707    /**
708     * Add a PATCH route
709     *
710     * @param  string $route
711     * @param  mixed  $controller
712     * @throws Exception
713     * @return static
714     */
715    public function patch(string $route, mixed $controller): static
716    {
717        $this->httpRouter()->patch($route, $controller);
718        return $this;
719    }
720
721    /**
722     * Add a custom HTTP method to the whitelist
723     *
724     * @param  string $method
725     * @throws Exception
726     * @return static
727     */
728    public function addCustomMethod(string $method): static
729    {
730        $this->httpRouter()->addCustomMethod($method);
731        return $this;
732    }
733
734    /**
735     * Add multiple custom HTTP methods to the whitelist
736     *
737     * @param  array $methods
738     * @throws Exception
739     * @return static
740     */
741    public function addCustomMethods(array $methods): static
742    {
743        $this->httpRouter()->addCustomMethods($methods);
744        return $this;
745    }
746
747    /**
748     * Determine if a custom HTTP method has been whitelisted
749     *
750     * @param  string $method
751     * @throws Exception
752     * @return bool
753     */
754    public function hasCustomMethod(string $method): bool
755    {
756        return $this->httpRouter()->hasCustomMethod($method);
757    }
758
759    /**
760     * Magic method to register a route for a whitelisted custom HTTP method
761     *
762     * @param  string $name
763     * @param  array  $arguments
764     * @throws Exception
765     * @return static
766     */
767    public function __call(string $name, array $arguments): static
768    {
769        $this->httpRouter()->{$name}(...$arguments);
770        return $this;
771    }
772
773    /**
774     * Set a service
775     *
776     * @param  string $name
777     * @param  mixed  $service
778     * @throws Service\Exception
779     * @return static
780     */
781    public function setService(string $name, mixed $service): static
782    {
783        $this->services->set($name, $service);
784        return $this;
785    }
786
787    /**
788     * Get a service
789     *
790     * @param  string $name
791     * @throws Service\Exception
792     * @return mixed
793     */
794    public function getService(string $name): mixed
795    {
796        return $this->services->get($name);
797    }
798
799    /**
800     * Remove a service
801     *
802     * @param  string $name
803     * @return static
804     */
805    public function removeService(string $name): static
806    {
807        $this->services->remove($name);
808        return $this;
809    }
810
811    /**
812     * Attach an event. Default hook-points are:
813     *
814     *   app.init
815     *   app.route.pre
816     *   app.dispatch.pre
817     *   app.dispatch.post
818     *   app.error
819     *   app.shutdown
820     *
821     * @param  string $name
822     * @param  mixed  $action
823     * @param  int    $priority
824     * @return static
825     */
826    public function on(string $name, mixed $action, int $priority = 0): static
827    {
828        $this->events->on($name, $action, $priority);
829        return $this;
830    }
831
832    /**
833     * Detach an event. Default hook-points are:
834     *
835     *   app.init
836     *   app.route.pre
837     *   app.dispatch.pre
838     *   app.dispatch.post
839     *   app.error
840     *   app.shutdown
841     *
842     * @param  string $name
843     * @param  mixed  $action
844     * @return static
845     */
846    public function off(string $name, mixed $action): static
847    {
848        $this->events->off($name, $action);
849        return $this;
850    }
851
852    /**
853     * Trigger an event
854     *
855     * @param  string $name
856     * @param  array $args
857     * @return static
858     */
859    public function trigger(string $name, array $args = []): static
860    {
861        if (count($args) == 0) {
862            $args = ['application' => $this];
863        } else if (!in_array($this, $args, true)) {
864            $args['application'] = $this;
865        }
866        $this->events->trigger($name, $args);
867        return $this;
868    }
869
870    /**
871     * Add a middleware handler
872     *
873     * @param  mixed $handler
874     * @param  mixed $name
875     * @return static
876     */
877    public function addMiddleware(mixed $handler, mixed $name = null): static
878    {
879        $this->middleware->addHandler($handler, $name);
880        return $this;
881    }
882
883    /**
884     * Get middleware
885     *
886     * @param  mixed $name
887     * @return mixed
888     */
889    public function getMiddleware(mixed $name): mixed
890    {
891        return $this->middleware->getHandler($name);
892    }
893
894    /**
895     * Remove middleware
896     *
897     * @param  mixed $name
898     * @return static
899     */
900    public function removeMiddleware(mixed $name): static
901    {
902        $this->middleware->removeHandler($name);
903        return $this;
904    }
905
906    /**
907     * Get environment value
908     *
909     * @param  string $key
910     * @param  mixed  $default
911     * @return mixed
912     */
913    public function env(string $key, mixed $default = null): mixed
914    {
915        return App::env($key, $default);
916    }
917
918    /**
919     * Get application environment
920     *
921     * @param  mixed $env
922     * @return string|null|bool
923     */
924    public function environment(mixed $env = null): string|null|bool
925    {
926        return App::environment($env);
927    }
928
929    /**
930     * Get application name (alias method)
931     *
932     * @return ?string
933     */
934    public function name(): ?string
935    {
936        return $this->name;
937    }
938
939    /**
940     * Get application URL
941     *
942     * @return ?string
943     */
944    public function url(): ?string
945    {
946        return App::url();
947    }
948
949    /**
950     * Check if application environment is local
951     *
952     * @return bool
953     */
954    public function isLocal(): bool
955    {
956        return App::isLocal();
957    }
958
959    /**
960     * Check if application environment is dev
961     *
962     * @return bool
963     */
964    public function isDev(): bool
965    {
966        return App::isDev();
967    }
968
969    /**
970     * Check if application environment is testing
971     *
972     * @return bool
973     */
974    public function isTesting(): bool
975    {
976        return App::isTesting();
977    }
978
979    /**
980     * Check if application environment is staging
981     *
982     * @return bool
983     */
984    public function isStaging(): bool
985    {
986        return App::isStaging();
987    }
988
989    /**
990     * Check if application environment is production
991     *
992     * @return bool
993     */
994    public function isProduction(): bool
995    {
996        return App::isProduction();
997    }
998
999    /**
1000     * Check if application is in maintenance mode
1001     *
1002     * @return bool
1003     */
1004    public function isDown(): bool
1005    {
1006        return App::isDown();
1007    }
1008
1009    /**
1010     * Check if application is in not maintenance mode
1011     *
1012     * @return bool
1013     */
1014    public function isUp(): bool
1015    {
1016        return App::isUp();
1017    }
1018
1019    /**
1020     * Run the application
1021     *
1022     * @param  bool              $exit
1023     * @param  string|array|null $forceRoute
1024     * @throws \Throwable
1025     * @return void
1026     */
1027    public function run(bool $exit = true, string|array|null $forceRoute = null): void
1028    {
1029        try {
1030            $this->init();
1031
1032            // Fire any app.route.pre listeners
1033            $this->events->dispatch(new Event\RoutePreEvent($this));
1034
1035            if (($this->router !== null)) {
1036                $this->router->route($forceRoute);
1037
1038                // Fire any app.dispatch.pre listeners
1039                $this->events->dispatch(new Event\DispatchPreEvent($this));
1040
1041                // Dispatch
1042                if ($this->router->hasDispatchable()) {
1043                    $dispatchable = $this->router->getDispatchable();
1044
1045                    // Handle maintenance mode uniformly, regardless of route target shape
1046                    if (App::isDown() && !App::isSecretRequest() &&
1047                        !(($dispatchable instanceof Dispatch\MaintenanceInterface) && $dispatchable->bypassMaintenance())) {
1048                        if ($dispatchable instanceof Dispatch\MaintenanceInterface) {
1049                            $dispatchable->dispatchMaintenance();
1050                        } else {
1051                            $this->renderMaintenanceResponse($exit);
1052                        }
1053                    // Process middleware
1054                    } else if (($this->middleware !== null) && ($this->middleware->hasHandlers())) {
1055                        if ($this->hasPsr15Middleware() &&
1056                            is_subclass_of((string)$this->router->getDispatchableClass(), Dispatch\AbstractDispatcher::class)) {
1057                            throw new Middleware\Exception(
1058                                'Error: A PSR-15 middleware adapter is registered, but the matched route target ' .
1059                                'is a controller class whose dispatch() method never produces a PSR-7 response. ' .
1060                                'Use a closure route that returns a Psr\Http\Message\ResponseInterface instead.'
1061                            );
1062                        }
1063
1064                        [$dispatch, $dispatchParams] = $this->buildDispatch($dispatchable);
1065                        $request = $this->resolveMiddlewareRequest($dispatchable);
1066
1067                        if ($request === null) {
1068                            throw new Exception('Error: Unable to retrieve the request object for the middleware.');
1069                        }
1070
1071                        $this->middleware->process($request, $dispatch, $dispatchParams);
1072                    // Skip middleware or process as normal
1073                    } else {
1074                        $this->invokeDispatchable($dispatchable);
1075                    }
1076                // Else, no route found
1077                } else {
1078                    if ($this->router->isHttp() && $this->router->hasMethodMismatch()) {
1079                        $this->router->methodNotAllowed($this->router->getAllowedMethods(), $exit);
1080                    } else {
1081                        $this->router->noRouteFound($exit);
1082                    }
1083                }
1084
1085                // Fire any app.dispatch.post listeners
1086                $this->events->dispatch(new Event\DispatchPostEvent($this));
1087            }
1088        } catch (Event\AbortException) {
1089            return;
1090        } catch (\Throwable $exception) {
1091            // Fire any app.error listeners
1092            $this->events->dispatch(new Event\ErrorEvent($this, $exception));
1093            throw $exception;
1094        } finally {
1095            // Fire any app.shutdown listeners, guaranteed to run regardless of
1096            // how the request above ended (normal completion, an aborted
1097            // listener, or a rethrown error)
1098            $this->events->dispatch(new Event\ShutdownEvent($this));
1099        }
1100    }
1101
1102    /**
1103     * Invoke the dispatchable directly, bypassing the middleware pipeline
1104     *
1105     * @param  mixed $dispatchable
1106     * @return void
1107     */
1108    protected function invokeDispatchable(mixed $dispatchable): void
1109    {
1110        [$dispatch, $dispatchParams] = $this->buildDispatch($dispatchable);
1111
1112        if ($dispatchParams !== null) {
1113            call_user_func_array($dispatch, $dispatchParams);
1114        } else {
1115            $dispatch();
1116        }
1117    }
1118
1119    /**
1120     * Build the dispatch closure and its params for the given dispatchable -
1121     * used both as the deferred callable the middleware pipeline invokes once
1122     * its handler chain completes, and by invokeDispatchable() to call the
1123     * same logic immediately when there's no middleware to defer to
1124     *
1125     * @param  mixed $dispatchable
1126     * @return array
1127     */
1128    protected function buildDispatch(mixed $dispatchable): array
1129    {
1130        if ($this->router->getDispatchableClass() == 'Closure') {
1131            $dispatch       = $dispatchable;
1132            $dispatchParams = ($this->router->hasRouteParams()) ? array_values($this->router->getRouteParams()) : null;
1133        } else if ($this->router->getDispatchableClass() == 'Pop\Utils\CallableObject') {
1134            $params         = ($this->router->hasRouteParams()) ? $this->router->getRouteParams() : null;
1135            $dispatch       = function() use ($dispatchable, $params) {
1136                $callableObject = new \Pop\Utils\CallableObject($dispatchable, $params);
1137                return $callableObject->call();
1138            };
1139            $dispatchParams = null;
1140        } else {
1141            $params         = ($this->router->hasRouteParams()) ? $this->router->getRouteParams() : null;
1142            $dispatch       = function() use ($dispatchable, $params) {
1143                $dispatchable->dispatch($this->router->getAction(), $params);
1144            };
1145            $dispatchParams = null;
1146        }
1147
1148        return [$dispatch, $dispatchParams];
1149    }
1150
1151    /**
1152     * Resolve the request object to pass into the middleware pipeline
1153     *
1154     * @param  mixed $dispatchable
1155     * @return mixed
1156     */
1157    protected function resolveMiddlewareRequest(mixed $dispatchable): mixed
1158    {
1159        if (is_object($dispatchable) && in_array('Pop\Dispatch\HttpTrait', class_uses($dispatchable))) {
1160            return $dispatchable->request();
1161        } else if (is_object($dispatchable) && in_array('Pop\Dispatch\ConsoleTrait', class_uses($dispatchable))) {
1162            return $dispatchable->console();
1163        } else if ($this->router->isHttp()) {
1164            return new Request(new Uri());
1165        } else if ($this->router->isCli()) {
1166            return new Console(120);
1167        }
1168
1169        return null;
1170    }
1171
1172    /**
1173     * Determine whether a PSR-15 middleware adapter is registered in the middleware stack
1174     *
1175     * @return bool
1176     */
1177    protected function hasPsr15Middleware(): bool
1178    {
1179        if ($this->middleware === null) {
1180            return false;
1181        }
1182
1183        foreach ($this->middleware->getHandlers() as $handler) {
1184            if ($handler instanceof Middleware\Psr15\MiddlewareAdapter) {
1185                return true;
1186            }
1187        }
1188
1189        return false;
1190    }
1191
1192    /**
1193     * Render a default maintenance-mode response for route targets that
1194     * aren't a Dispatch\MaintenanceInterface (closures, callables) and so
1195     * have no custom maintenance action of their own to run
1196     *
1197     * @param  bool $exit
1198     * @return void
1199     */
1200    protected function renderMaintenanceResponse(bool $exit): void
1201    {
1202        if ($this->router->isHttp() && $this->router->acceptsHtml()) {
1203            if (!headers_sent()) {
1204                header('HTTP/1.1 503 Service Unavailable');
1205            }
1206            echo '<!DOCTYPE html>' . PHP_EOL;
1207            echo '<html>' . PHP_EOL;
1208            echo '    <head>' . PHP_EOL;
1209            echo '        <title>Service Unavailable</title>' . PHP_EOL;
1210            echo '    </head>' . PHP_EOL;
1211            echo '<body>' . PHP_EOL;
1212            echo '    <h1>Service Unavailable</h1>' . PHP_EOL;
1213            echo '</body>' . PHP_EOL;
1214            echo '</html>' . PHP_EOL;
1215        } else if ($this->router->isHttp()) {
1216            if (!headers_sent()) {
1217                header('HTTP/1.1 503 Service Unavailable');
1218                header('Content-Type: application/json');
1219            }
1220            echo json_encode(['error' => 'Service Unavailable'], JSON_PRETTY_PRINT) . PHP_EOL;
1221        } else {
1222            echo PHP_EOL . 'Service Unavailable.' . PHP_EOL . PHP_EOL;
1223        }
1224
1225        if ($exit) {
1226            exit();
1227        }
1228    }
1229
1230    /**
1231     * Set a pre-designated value in the application object
1232     *
1233     * @param  string $name
1234     * @param  mixed $value
1235     * @throws Exception
1236     * @return void
1237     */
1238    public function __set(string $name, mixed $value): void
1239    {
1240        switch ($name) {
1241            case 'config':
1242                $this->registerConfig($value);
1243                break;
1244            case 'router':
1245                $this->registerRouter($value);
1246                break;
1247            case 'services':
1248                $this->registerServices($value);
1249                break;
1250            case 'events':
1251                $this->registerEvents($value);
1252                break;
1253            case 'middleware':
1254                $this->registerMiddleware($value);
1255                break;
1256            case 'modules':
1257                $this->registerModules($value);
1258                break;
1259            case 'autoloader':
1260                $this->registerAutoloader($value);
1261                break;
1262        }
1263    }
1264
1265    /**
1266     * Get a pre-designated value from the application object
1267     *
1268     * @param  string $name
1269     * @return mixed
1270     */
1271    public function __get(string $name): mixed
1272    {
1273        return match ($name) {
1274            'config'     => $this->config,
1275            'router'     => $this->router,
1276            'services'   => $this->services,
1277            'events'     => $this->events,
1278            'middleware' => $this->middleware,
1279            'modules'    => $this->modules,
1280            'autoloader' => $this->autoloader,
1281            default      => null,
1282        };
1283    }
1284
1285    /**
1286     * Determine if a pre-designated value in the application object exists
1287     *
1288     * @param  string $name
1289     * @return bool
1290     */
1291    public function __isset(string $name): bool
1292    {
1293        return match ($name) {
1294            'config'     => ($this->config !== null),
1295            'router'     => ($this->router !== null),
1296            'services'   => ($this->services !== null),
1297            'events'     => ($this->events !== null),
1298            'middleware' => ($this->middleware !== null),
1299            'modules'    => ($this->modules !== null),
1300            'autoloader' => ($this->autoloader !== null),
1301            default      => false,
1302        };
1303    }
1304
1305    /**
1306     * Unset a pre-designated value in the application object
1307     *
1308     * @param  string $name
1309     * @return void
1310     */
1311    public function __unset(string $name): void
1312    {
1313        switch ($name) {
1314            case 'config':
1315                $this->config = null;
1316                break;
1317            case 'router':
1318                $this->router = null;
1319                break;
1320            case 'services':
1321                $this->services = null;
1322                break;
1323            case 'events':
1324                $this->events = null;
1325                break;
1326            case 'middleware':
1327                $this->middleware = null;
1328                break;
1329            case 'modules':
1330                $this->modules = null;
1331                break;
1332            case 'autoloader':
1333                $this->autoloader = null;
1334                break;
1335        }
1336    }
1337
1338    /**
1339     * Set a pre-designated value in the application object
1340     *
1341     * @param  mixed $offset
1342     * @param  mixed $value
1343     * @throws Exception
1344     * @return void
1345     */
1346    public function offsetSet(mixed $offset, mixed $value): void
1347    {
1348        $this->__set($offset, $value);
1349    }
1350
1351    /**
1352     * Get a pre-designated value from the application object
1353     *
1354     * @param  mixed $offset
1355     * @return mixed
1356     */
1357    public function offsetGet(mixed $offset): mixed
1358    {
1359        return $this->__get($offset);
1360    }
1361
1362    /**
1363     * Determine if a pre-designated value in the application object exists
1364     *
1365     * @param  mixed $offset
1366     * @return bool
1367     */
1368    public function offsetExists(mixed $offset): bool
1369    {
1370        return $this->__isset($offset);
1371    }
1372
1373    /**
1374     * Unset a pre-designated value in the application object
1375     *
1376     * @param  mixed $offset
1377     * @return void
1378     */
1379    public function offsetUnset(mixed $offset): void
1380    {
1381        $this->__unset($offset);
1382    }
1383
1384}