Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.58% covered (success)
95.58%
324 / 339
95.06% covered (success)
95.06%
77 / 81
CRAP
0.00% covered (danger)
0.00%
0 / 1
Application
95.58% covered (success)
95.58%
324 / 339
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.14% covered (success)
97.14%
34 / 35
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     *
820     * @param  string $name
821     * @param  mixed  $action
822     * @param  int    $priority
823     * @return static
824     */
825    public function on(string $name, mixed $action, int $priority = 0): static
826    {
827        $this->events->on($name, $action, $priority);
828        return $this;
829    }
830
831    /**
832     * Detach an event. Default hook-points are:
833     *
834     *   app.init
835     *   app.route.pre
836     *   app.dispatch.pre
837     *   app.dispatch.post
838     *   app.error
839     *
840     * @param  string $name
841     * @param  mixed  $action
842     * @return static
843     */
844    public function off(string $name, mixed $action): static
845    {
846        $this->events->off($name, $action);
847        return $this;
848    }
849
850    /**
851     * Trigger an event
852     *
853     * @param  string $name
854     * @param  array $args
855     * @return static
856     */
857    public function trigger(string $name, array $args = []): static
858    {
859        if (count($args) == 0) {
860            $args = ['application' => $this];
861        } else if (!in_array($this, $args, true)) {
862            $args['application'] = $this;
863        }
864        $this->events->trigger($name, $args);
865        return $this;
866    }
867
868    /**
869     * Add a middleware handler
870     *
871     * @param  mixed $handler
872     * @param  mixed $name
873     * @return static
874     */
875    public function addMiddleware(mixed $handler, mixed $name = null): static
876    {
877        $this->middleware->addHandler($handler, $name);
878        return $this;
879    }
880
881    /**
882     * Get middleware
883     *
884     * @param  mixed $name
885     * @return mixed
886     */
887    public function getMiddleware(mixed $name): mixed
888    {
889        return $this->middleware->getHandler($name);
890    }
891
892    /**
893     * Remove middleware
894     *
895     * @param  mixed $name
896     * @return static
897     */
898    public function removeMiddleware(mixed $name): static
899    {
900        $this->middleware->removeHandler($name);
901        return $this;
902    }
903
904    /**
905     * Get environment value
906     *
907     * @param  string $key
908     * @param  mixed  $default
909     * @return mixed
910     */
911    public function env(string $key, mixed $default = null): mixed
912    {
913        return App::env($key, $default);
914    }
915
916    /**
917     * Get application environment
918     *
919     * @param  mixed $env
920     * @return string|null|bool
921     */
922    public function environment(mixed $env = null): string|null|bool
923    {
924        return App::environment($env);
925    }
926
927    /**
928     * Get application name (alias method)
929     *
930     * @return ?string
931     */
932    public function name(): ?string
933    {
934        return $this->name;
935    }
936
937    /**
938     * Get application URL
939     *
940     * @return ?string
941     */
942    public function url(): ?string
943    {
944        return App::url();
945    }
946
947    /**
948     * Check if application environment is local
949     *
950     * @return bool
951     */
952    public function isLocal(): bool
953    {
954        return App::isLocal();
955    }
956
957    /**
958     * Check if application environment is dev
959     *
960     * @return bool
961     */
962    public function isDev(): bool
963    {
964        return App::isDev();
965    }
966
967    /**
968     * Check if application environment is testing
969     *
970     * @return bool
971     */
972    public function isTesting(): bool
973    {
974        return App::isTesting();
975    }
976
977    /**
978     * Check if application environment is staging
979     *
980     * @return bool
981     */
982    public function isStaging(): bool
983    {
984        return App::isStaging();
985    }
986
987    /**
988     * Check if application environment is production
989     *
990     * @return bool
991     */
992    public function isProduction(): bool
993    {
994        return App::isProduction();
995    }
996
997    /**
998     * Check if application is in maintenance mode
999     *
1000     * @return bool
1001     */
1002    public function isDown(): bool
1003    {
1004        return App::isDown();
1005    }
1006
1007    /**
1008     * Check if application is in not maintenance mode
1009     *
1010     * @return bool
1011     */
1012    public function isUp(): bool
1013    {
1014        return App::isUp();
1015    }
1016
1017    /**
1018     * Run the application
1019     *
1020     * @param  bool              $exit
1021     * @param  string|array|null $forceRoute
1022     * @throws \Throwable
1023     * @return void
1024     */
1025    public function run(bool $exit = true, string|array|null $forceRoute = null): void
1026    {
1027        try {
1028            $this->init();
1029
1030            // Fire any app.route.pre listeners
1031            $this->events->dispatch(new Event\RoutePreEvent($this));
1032
1033            if (($this->router !== null)) {
1034                $this->router->route($forceRoute);
1035
1036                // Fire any app.dispatch.pre listeners
1037                $this->events->dispatch(new Event\DispatchPreEvent($this));
1038
1039                // Dispatch
1040                if ($this->router->hasDispatchable()) {
1041                    $dispatchable = $this->router->getDispatchable();
1042
1043                    // Handle maintenance mode uniformly, regardless of route target shape
1044                    if (App::isDown() && !App::isSecretRequest() &&
1045                        !(($dispatchable instanceof Dispatch\MaintenanceInterface) && $dispatchable->bypassMaintenance())) {
1046                        if ($dispatchable instanceof Dispatch\MaintenanceInterface) {
1047                            $dispatchable->dispatchMaintenance();
1048                        } else {
1049                            $this->renderMaintenanceResponse($exit);
1050                        }
1051                    // Process middleware
1052                    } else if (($this->middleware !== null) && ($this->middleware->hasHandlers())) {
1053                        if ($this->hasPsr15Middleware() &&
1054                            is_subclass_of((string)$this->router->getDispatchableClass(), Dispatch\AbstractDispatcher::class)) {
1055                            throw new Middleware\Exception(
1056                                'Error: A PSR-15 middleware adapter is registered, but the matched route target ' .
1057                                'is a controller class whose dispatch() method never produces a PSR-7 response. ' .
1058                                'Use a closure route that returns a Psr\Http\Message\ResponseInterface instead.'
1059                            );
1060                        }
1061
1062                        [$dispatch, $dispatchParams] = $this->buildDispatch($dispatchable);
1063                        $request = $this->resolveMiddlewareRequest($dispatchable);
1064
1065                        if ($request === null) {
1066                            throw new Exception('Error: Unable to retrieve the request object for the middleware.');
1067                        }
1068
1069                        $this->middleware->process($request, $dispatch, $dispatchParams);
1070                    // Skip middleware or process as normal
1071                    } else {
1072                        $this->invokeDispatchable($dispatchable);
1073                    }
1074                // Else, no route found
1075                } else {
1076                    if ($this->router->isHttp() && $this->router->hasMethodMismatch()) {
1077                        $this->router->methodNotAllowed($this->router->getAllowedMethods(), $exit);
1078                    } else {
1079                        $this->router->noRouteFound($exit);
1080                    }
1081                }
1082
1083                // Fire any app.dispatch.post listeners
1084                $this->events->dispatch(new Event\DispatchPostEvent($this));
1085            }
1086        } catch (Event\AbortException) {
1087            return;
1088        } catch (\Throwable $exception) {
1089            // Fire any app.error listeners
1090            $this->events->dispatch(new Event\ErrorEvent($this, $exception));
1091            throw $exception;
1092        }
1093    }
1094
1095    /**
1096     * Invoke the dispatchable directly, bypassing the middleware pipeline
1097     *
1098     * @param  mixed $dispatchable
1099     * @return void
1100     */
1101    protected function invokeDispatchable(mixed $dispatchable): void
1102    {
1103        [$dispatch, $dispatchParams] = $this->buildDispatch($dispatchable);
1104
1105        if ($dispatchParams !== null) {
1106            call_user_func_array($dispatch, $dispatchParams);
1107        } else {
1108            $dispatch();
1109        }
1110    }
1111
1112    /**
1113     * Build the dispatch closure and its params for the given dispatchable -
1114     * used both as the deferred callable the middleware pipeline invokes once
1115     * its handler chain completes, and by invokeDispatchable() to call the
1116     * same logic immediately when there's no middleware to defer to
1117     *
1118     * @param  mixed $dispatchable
1119     * @return array
1120     */
1121    protected function buildDispatch(mixed $dispatchable): array
1122    {
1123        if ($this->router->getDispatchableClass() == 'Closure') {
1124            $dispatch       = $dispatchable;
1125            $dispatchParams = ($this->router->hasRouteParams()) ? array_values($this->router->getRouteParams()) : null;
1126        } else if ($this->router->getDispatchableClass() == 'Pop\Utils\CallableObject') {
1127            $params         = ($this->router->hasRouteParams()) ? $this->router->getRouteParams() : null;
1128            $dispatch       = function() use ($dispatchable, $params) {
1129                $callableObject = new \Pop\Utils\CallableObject($dispatchable, $params);
1130                return $callableObject->call();
1131            };
1132            $dispatchParams = null;
1133        } else {
1134            $params         = ($this->router->hasRouteParams()) ? $this->router->getRouteParams() : null;
1135            $dispatch       = function() use ($dispatchable, $params) {
1136                $dispatchable->dispatch($this->router->getAction(), $params);
1137            };
1138            $dispatchParams = null;
1139        }
1140
1141        return [$dispatch, $dispatchParams];
1142    }
1143
1144    /**
1145     * Resolve the request object to pass into the middleware pipeline
1146     *
1147     * @param  mixed $dispatchable
1148     * @return mixed
1149     */
1150    protected function resolveMiddlewareRequest(mixed $dispatchable): mixed
1151    {
1152        if (is_object($dispatchable) && in_array('Pop\Dispatch\HttpTrait', class_uses($dispatchable))) {
1153            return $dispatchable->request();
1154        } else if (is_object($dispatchable) && in_array('Pop\Dispatch\ConsoleTrait', class_uses($dispatchable))) {
1155            return $dispatchable->console();
1156        } else if ($this->router->isHttp()) {
1157            return new Request(new Uri());
1158        } else if ($this->router->isCli()) {
1159            return new Console(120);
1160        }
1161
1162        return null;
1163    }
1164
1165    /**
1166     * Determine whether a PSR-15 middleware adapter is registered in the middleware stack
1167     *
1168     * @return bool
1169     */
1170    protected function hasPsr15Middleware(): bool
1171    {
1172        if ($this->middleware === null) {
1173            return false;
1174        }
1175
1176        foreach ($this->middleware->getHandlers() as $handler) {
1177            if ($handler instanceof Middleware\Psr15\MiddlewareAdapter) {
1178                return true;
1179            }
1180        }
1181
1182        return false;
1183    }
1184
1185    /**
1186     * Render a default maintenance-mode response for route targets that
1187     * aren't a Dispatch\MaintenanceInterface (closures, callables) and so
1188     * have no custom maintenance action of their own to run
1189     *
1190     * @param  bool $exit
1191     * @return void
1192     */
1193    protected function renderMaintenanceResponse(bool $exit): void
1194    {
1195        if ($this->router->isHttp() && $this->router->acceptsHtml()) {
1196            if (!headers_sent()) {
1197                header('HTTP/1.1 503 Service Unavailable');
1198            }
1199            echo '<!DOCTYPE html>' . PHP_EOL;
1200            echo '<html>' . PHP_EOL;
1201            echo '    <head>' . PHP_EOL;
1202            echo '        <title>Service Unavailable</title>' . PHP_EOL;
1203            echo '    </head>' . PHP_EOL;
1204            echo '<body>' . PHP_EOL;
1205            echo '    <h1>Service Unavailable</h1>' . PHP_EOL;
1206            echo '</body>' . PHP_EOL;
1207            echo '</html>' . PHP_EOL;
1208        } else if ($this->router->isHttp()) {
1209            if (!headers_sent()) {
1210                header('HTTP/1.1 503 Service Unavailable');
1211                header('Content-Type: application/json');
1212            }
1213            echo json_encode(['error' => 'Service Unavailable'], JSON_PRETTY_PRINT) . PHP_EOL;
1214        } else {
1215            echo PHP_EOL . 'Service Unavailable.' . PHP_EOL . PHP_EOL;
1216        }
1217
1218        if ($exit) {
1219            exit();
1220        }
1221    }
1222
1223    /**
1224     * Set a pre-designated value in the application object
1225     *
1226     * @param  string $name
1227     * @param  mixed $value
1228     * @throws Exception
1229     * @return void
1230     */
1231    public function __set(string $name, mixed $value): void
1232    {
1233        switch ($name) {
1234            case 'config':
1235                $this->registerConfig($value);
1236                break;
1237            case 'router':
1238                $this->registerRouter($value);
1239                break;
1240            case 'services':
1241                $this->registerServices($value);
1242                break;
1243            case 'events':
1244                $this->registerEvents($value);
1245                break;
1246            case 'middleware':
1247                $this->registerMiddleware($value);
1248                break;
1249            case 'modules':
1250                $this->registerModules($value);
1251                break;
1252            case 'autoloader':
1253                $this->registerAutoloader($value);
1254                break;
1255        }
1256    }
1257
1258    /**
1259     * Get a pre-designated value from the application object
1260     *
1261     * @param  string $name
1262     * @return mixed
1263     */
1264    public function __get(string $name): mixed
1265    {
1266        return match ($name) {
1267            'config'     => $this->config,
1268            'router'     => $this->router,
1269            'services'   => $this->services,
1270            'events'     => $this->events,
1271            'middleware' => $this->middleware,
1272            'modules'    => $this->modules,
1273            'autoloader' => $this->autoloader,
1274            default      => null,
1275        };
1276    }
1277
1278    /**
1279     * Determine if a pre-designated value in the application object exists
1280     *
1281     * @param  string $name
1282     * @return bool
1283     */
1284    public function __isset(string $name): bool
1285    {
1286        return match ($name) {
1287            'config'     => ($this->config !== null),
1288            'router'     => ($this->router !== null),
1289            'services'   => ($this->services !== null),
1290            'events'     => ($this->events !== null),
1291            'middleware' => ($this->middleware !== null),
1292            'modules'    => ($this->modules !== null),
1293            'autoloader' => ($this->autoloader !== null),
1294            default      => false,
1295        };
1296    }
1297
1298    /**
1299     * Unset a pre-designated value in the application object
1300     *
1301     * @param  string $name
1302     * @return void
1303     */
1304    public function __unset(string $name): void
1305    {
1306        switch ($name) {
1307            case 'config':
1308                $this->config = null;
1309                break;
1310            case 'router':
1311                $this->router = null;
1312                break;
1313            case 'services':
1314                $this->services = null;
1315                break;
1316            case 'events':
1317                $this->events = null;
1318                break;
1319            case 'middleware':
1320                $this->middleware = null;
1321                break;
1322            case 'modules':
1323                $this->modules = null;
1324                break;
1325            case 'autoloader':
1326                $this->autoloader = null;
1327                break;
1328        }
1329    }
1330
1331    /**
1332     * Set a pre-designated value in the application object
1333     *
1334     * @param  mixed $offset
1335     * @param  mixed $value
1336     * @throws Exception
1337     * @return void
1338     */
1339    public function offsetSet(mixed $offset, mixed $value): void
1340    {
1341        $this->__set($offset, $value);
1342    }
1343
1344    /**
1345     * Get a pre-designated value from the application object
1346     *
1347     * @param  mixed $offset
1348     * @return mixed
1349     */
1350    public function offsetGet(mixed $offset): mixed
1351    {
1352        return $this->__get($offset);
1353    }
1354
1355    /**
1356     * Determine if a pre-designated value in the application object exists
1357     *
1358     * @param  mixed $offset
1359     * @return bool
1360     */
1361    public function offsetExists(mixed $offset): bool
1362    {
1363        return $this->__isset($offset);
1364    }
1365
1366    /**
1367     * Unset a pre-designated value in the application object
1368     *
1369     * @param  mixed $offset
1370     * @return void
1371     */
1372    public function offsetUnset(mixed $offset): void
1373    {
1374        $this->__unset($offset);
1375    }
1376
1377}