Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
193 / 193
100.00% covered (success)
100.00%
45 / 45
CRAP
100.00% covered (success)
100.00%
1 / 1
Application
100.00% covered (success)
100.00%
193 / 193
100.00% covered (success)
100.00%
45 / 45
122
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
12
 bootstrap
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
28
 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
 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
 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%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 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
 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
 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
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 __set
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 __get
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 __isset
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 __unset
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 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
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop;
15
16use Pop\Utils\Helper;
17use ReflectionException;
18
19/**
20 * Application class
21 *
22 * @category   Pop
23 * @package    Pop
24 * @author     Nick Sagona, III <dev@noladev.com>
25 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    4.3.5
28 * @property   $config mixed
29 */
30class Application extends AbstractApplication implements \ArrayAccess
31{
32
33    /**
34     * Application router
35     * @var ?Router\Router
36     */
37    protected ?Router\Router $router = null;
38
39    /**
40     * Service locator
41     * @var ?Service\Locator
42     */
43    protected ?Service\Locator $services = null;
44
45    /**
46     * Event manager
47     * @var ?Event\Manager
48     */
49    protected ?Event\Manager $events = null;
50
51    /**
52     * Module manager
53     * @var ?Module\Manager
54     */
55    protected ?Module\Manager $modules = null;
56
57    /**
58     * Autoloader
59     * @var mixed
60     */
61    protected mixed $autoloader = null;
62
63    /**
64     * Constructor
65     *
66     * Instantiate an application object
67     *
68     * Optional parameters are a service locator instance, a router instance,
69     * an event manager instance or a configuration object or array
70     */
71    public function __construct()
72    {
73        $args       = func_get_args();
74        $autoloader = null;
75        $config     = null;
76
77        foreach ($args as $arg) {
78            $class = (is_object($arg)) ? get_class($arg) : '';
79            if ((stripos($class, 'classload') !== false) || (stripos($class, 'autoload') !== false)) {
80                $autoloader = $arg;
81            } else if ($arg instanceof Router\Router) {
82                $this->registerRouter($arg);
83            } else if ($arg instanceof Service\Locator) {
84                $this->registerServices($arg);
85            } else if ($arg instanceof Event\Manager) {
86                $this->registerEvents($arg);
87            } else if ($arg instanceof Module\Manager) {
88                $this->registerModules($arg);
89            } else if (is_array($arg) || ($arg instanceof \ArrayAccess)) {
90                $config = $arg;
91            }
92        }
93
94        if ($config !== null) {
95            $this->registerConfig($config);
96        }
97
98        $this->bootstrap($autoloader);
99    }
100
101    /**
102     * Bootstrap the application, creating the required objects if they haven't been created yet
103     * and registering with the autoloader, adding routes, services and events
104     *
105     * @param  mixed $autoloader
106     * @throws Exception|Module\Exception|Service\Exception
107     * @return static
108     */
109    public function bootstrap(mixed $autoloader = null): static
110    {
111        if ($autoloader !== null) {
112            $this->registerAutoloader($autoloader);
113        }
114        if ($this->router === null) {
115            $this->registerRouter(new Router\Router());
116        }
117        if ($this->services === null) {
118            $this->registerServices(new Service\Locator());
119        }
120        if ($this->events === null) {
121            $this->registerEvents(new Event\Manager());
122        }
123        if ($this->modules === null) {
124            $this->registerModules(new Module\Manager());
125        }
126
127        // If the autoloader is set and the application config has a
128        // defined prefix and src, register with the autoloader
129        if (($this->autoloader !== null) && isset($this->config['prefix']) &&
130            isset($this->config['src']) && file_exists($this->config['src'])) {
131            // Register as PSR-0
132            if (isset($this->config['psr-0']) && ($this->config['psr-0'])) {
133                $this->autoloader->add($this->config['prefix'], $this->config['src']);
134            // Else, default to PSR-4
135            } else {
136                $this->autoloader->addPsr4($this->config['prefix'], $this->config['src']);
137            }
138        }
139
140        // Set the app name
141        if (!empty($this->config['name'])) {
142            $this->setName($this->config['name']);
143        } else if (!empty(App::name())) {
144            $this->setName(App::name());
145        }
146
147        // Set the app version
148        if (!empty($this->config['version'])) {
149            $this->setVersion($this->config['version']);
150        }
151
152        // Load helper functions
153        if ((!isset($this->config['helper_functions']) || ($this->config['helper_functions'] === true)) && (!Helper::isLoaded())) {
154            Helper::loadFunctions();
155        }
156
157        // If routes are set in the app config, register them with the application
158        if (isset($this->config['routes']) && ($this->router !== null)) {
159            $this->router->addRoutes($this->config['routes']);
160        }
161
162        // If services are set in the app config, register them with the application
163        if (isset($this->config['services']) && ($this->services !== null)) {
164            foreach ($this->config['services'] as $name => $service) {
165                $this->setService($name, $service);
166            }
167        }
168
169        // If events are set in the app config, register them with the application
170        if (isset($this->config['events']) && ($this->events !== null)) {
171            foreach ($this->config['events'] as $event) {
172                if (isset($event['name']) && isset($event['action'])) {
173                    $this->on($event['name'], $event['action'], ((int)$event['priority'] ?? 0));
174                }
175            }
176        }
177
178        // Register application object with App helper class
179        App::set($this);
180
181        return $this;
182    }
183
184    /**
185     * Initialize the application
186     *
187     * @return static
188     */
189    public function init(): static
190    {
191        $this->trigger('app.init');
192        return $this;
193    }
194
195    /**
196     * Get the autoloader object
197     *
198     * @return mixed
199     */
200    public function autoloader(): mixed
201    {
202        return $this->autoloader;
203    }
204
205    /**
206     * Access the application router
207     *
208     * @return Router\Router
209     */
210    public function router(): Router\Router
211    {
212        return $this->router;
213    }
214
215    /**
216     * Get the service locator
217     *
218     * @return Service\Locator
219     */
220    public function services(): Service\Locator
221    {
222        return $this->services;
223    }
224
225    /**
226     * Get the event manager
227     *
228     * @return Event\Manager
229     */
230    public function events(): Event\Manager
231    {
232        return $this->events;
233    }
234
235    /**
236     * Access all application module configs
237     *
238     * @return Module\Manager
239     */
240    public function modules(): Module\Manager
241    {
242        return $this->modules;
243    }
244
245    /**
246     * Register a new router object with the application
247     *
248     * @param  Router\Router $router
249     * @return static
250     */
251    public function registerRouter(Router\Router $router): static
252    {
253        $this->router = $router;
254        Router\Route::setRouter($router);
255        return $this;
256    }
257
258    /**
259     * Register a new service locator object with the application
260     *
261     * @param  Service\Locator $services
262     * @return static
263     */
264    public function registerServices(Service\Locator $services): static
265    {
266        $this->services = $services;
267        return $this;
268    }
269
270    /**
271     * Register a new event manager object with the application
272     *
273     * @param  Event\Manager $events
274     * @return static
275     */
276    public function registerEvents(Event\Manager $events): static
277    {
278        $this->events = $events;
279        return $this;
280    }
281
282    /**
283     * Register a new module manager object with the application
284     *
285     * @param  Module\Manager $modules
286     * @return static
287     */
288    public function registerModules(Module\Manager $modules): static
289    {
290        $this->modules = $modules;
291        return $this;
292    }
293
294    /**
295     * Register the autoloader object with the application
296     *
297     * @param  mixed $autoloader
298     * @throws Exception
299     * @return static
300     */
301    public function registerAutoloader(mixed $autoloader): static
302    {
303        if (!method_exists($autoloader, 'add') || !method_exists($autoloader, 'addPsr4')) {
304            throw new Exception(
305                'Error: The autoloader instance must contain the methods \'add\' and \'addPsr4\', ' .
306                'as with Composer\Autoload\ClassLoader.'
307            );
308        }
309        $this->autoloader = $autoloader;
310        return $this;
311    }
312
313    /**
314     * Access a module object
315     *
316     * @param  string $name
317     * @return ?Module\ModuleInterface
318     */
319    public function module(string $name): ?Module\ModuleInterface
320    {
321        return $this->modules[$name] ?? null;
322    }
323
324    /**
325     * Register a module with the module manager object
326     *
327     * @param  mixed   $module
328     * @param  ?string $name
329     * @throws Module\Exception|Service\Exception
330     * @return static
331     */
332    public function register(mixed $module, ?string $name = null): static
333    {
334        if (!($module instanceof Module\ModuleInterface)) {
335            $module = new Module\Module($module, $this);
336        }
337
338        if ($name !== null) {
339            $module->setName($name);
340        }
341
342        if (!$module->isRegistered()) {
343            $module->register($this);
344        }
345
346        return $this;
347    }
348
349    /**
350     * Unregister a module with the module manager object
351     *
352     * @param  string $name
353     * @return static
354     */
355    public function unregister(string $name): static
356    {
357        unset($this->modules[$name]);
358        return $this;
359    }
360
361    /**
362     * Determine whether a module is registered with the application object
363     *
364     * @param  string $name
365     * @return bool
366     */
367    public function isRegistered(string $name): bool
368    {
369        return $this->modules->isRegistered($name);
370    }
371
372    /**
373     * Add a route
374     *
375     * @param  string $route
376     * @param  mixed  $controller
377     * @return static
378     */
379    public function addRoute(string $route, mixed $controller): static
380    {
381        $this->router->addRoute($route, $controller);
382        return $this;
383    }
384
385    /**
386     * Add routes
387     *
388     * @param  array $routes
389     * @return static
390     */
391    public function addRoutes(array $routes): static
392    {
393        $this->router->addRoutes($routes);
394        return $this;
395    }
396
397    /**
398     * Set a service
399     *
400     * @param  string $name
401     * @param  mixed  $service
402     * @throws Service\Exception
403     * @return static
404     */
405    public function setService(string $name, mixed $service): static
406    {
407        $this->services->set($name, $service);
408        return $this;
409    }
410
411    /**
412     * Get a service
413     *
414     * @param  string $name
415     * @throws Service\Exception
416     * @return mixed
417     */
418    public function getService(string $name): mixed
419    {
420        return $this->services->get($name);
421    }
422
423    /**
424     * Remove a service
425     *
426     * @param  string $name
427     * @return static
428     */
429    public function removeService(string $name): static
430    {
431        $this->services->remove($name);
432        return $this;
433    }
434
435    /**
436     * Attach an event. Default hook-points are:
437     *
438     *   app.init
439     *   app.route.pre
440     *   app.dispatch.pre
441     *   app.dispatch.post
442     *   app.error
443     *
444     * @param  string $name
445     * @param  mixed  $action
446     * @param  int    $priority
447     * @return static
448     */
449    public function on(string $name, mixed $action, int $priority = 0): static
450    {
451        $this->events->on($name, $action, $priority);
452        return $this;
453    }
454
455    /**
456     * Detach an event. Default hook-points are:
457     *
458     *   app.init
459     *   app.route.pre
460     *   app.dispatch.pre
461     *   app.dispatch.post
462     *   app.error
463     *
464     * @param  string $name
465     * @param  mixed  $action
466     * @return static
467     */
468    public function off(string $name, mixed $action): static
469    {
470        $this->events->off($name, $action);
471        return $this;
472    }
473
474    /**
475     * Trigger an event
476     *
477     * @param  string $name
478     * @param  array $args
479     * @return static
480     */
481    public function trigger(string $name, array $args = []): static
482    {
483        if (count($args) == 0) {
484            $args = ['application' => $this];
485        } else if (!in_array($this, $args, true)) {
486            $args['application'] = $this;
487        }
488        $this->events->trigger($name, $args);
489        return $this;
490    }
491
492    /**
493     * Get environment value
494     *
495     * @param  string $key
496     * @param  mixed  $default
497     * @return mixed
498     */
499    public function env(string $key, mixed $default = null): mixed
500    {
501        return App::env($key, $default);
502    }
503
504    /**
505     * Get application environment
506     *
507     * @param  mixed $env
508     * @return string|null|bool
509     */
510    public function environment(mixed $env = null): string|null|bool
511    {
512        return App::environment($env);
513    }
514
515    /**
516     * Get application name (alias method)
517     *
518     * @return ?string
519     */
520    public function name(): ?string
521    {
522        return $this->name;
523    }
524
525    /**
526     * Get application URL
527     *
528     * @return ?string
529     */
530    public function url(): ?string
531    {
532        return App::url();
533    }
534
535    /**
536     * Check if application environment is local
537     *
538     * @return bool
539     */
540    public function isLocal(): bool
541    {
542        return App::isLocal();
543    }
544
545    /**
546     * Check if application environment is dev
547     *
548     * @return bool
549     */
550    public function isDev(): bool
551    {
552        return App::isDev();
553    }
554
555    /**
556     * Check if application environment is testing
557     *
558     * @return bool
559     */
560    public function isTesting(): bool
561    {
562        return App::isTesting();
563    }
564
565    /**
566     * Check if application environment is staging
567     *
568     * @return bool
569     */
570    public function isStaging(): bool
571    {
572        return App::isStaging();
573    }
574
575    /**
576     * Check if application environment is production
577     *
578     * @return bool
579     */
580    public function isProduction(): bool
581    {
582        return App::isProduction();
583    }
584
585    /**
586     * Check if application is in maintenance mode
587     *
588     * @return bool
589     */
590    public function isDown(): bool
591    {
592        return App::isDown();
593    }
594
595    /**
596     * Check if application is in not maintenance mode
597     *
598     * @return bool
599     */
600    public function isUp(): bool
601    {
602        return App::isUp();
603    }
604
605    /**
606     * Run the application
607     *
608     * @param  bool    $exit
609     * @param  ?string $forceRoute
610     * @throws Event\Exception|Router\Exception|ReflectionException
611     * @return void
612     */
613    public function run(bool $exit = true, ?string $forceRoute = null): void
614    {
615        try {
616            $this->init();
617
618            // Trigger any app.route.pre events
619            $this->trigger('app.route.pre');
620
621            if (($this->router !== null)) {
622                $this->router->route($forceRoute);
623
624                // Trigger any app.dispatch.post events
625                $this->trigger('app.dispatch.pre');
626
627                if ($this->router->hasController()) {
628                    $controller = $this->router->getController();
629                    if ($this->router->getControllerClass() == 'Closure') {
630                        if ($this->router->hasRouteParams()) {
631                            call_user_func_array($controller, array_values($this->router->getRouteParams()));
632                        } else {
633                            $controller();
634                        }
635                    } else {
636                        $params = ($this->router->hasRouteParams()) ? $this->router->getRouteParams() : null;
637                        $controller->dispatch($this->router->getAction(), $params);
638                    }
639                } else {
640                    $this->router->noRouteFound($exit);
641                }
642
643                // Trigger any app.dispatch.post events
644                $this->trigger('app.dispatch.post');
645            }
646        } catch (Exception $exception) {
647            // Trigger any app.error events
648            $this->trigger('app.error', ['exception' => $exception]);
649        }
650    }
651
652    /**
653     * Set a pre-designated value in the application object
654     *
655     * @param  string $name
656     * @param  mixed $value
657     * @throws Exception
658     * @return void
659     */
660    public function __set(string $name, mixed $value): void
661    {
662        switch ($name) {
663            case 'config':
664                $this->registerConfig($value);
665                break;
666            case 'router':
667                $this->registerRouter($value);
668                break;
669            case 'services':
670                $this->registerServices($value);
671                break;
672            case 'events':
673                $this->registerEvents($value);
674                break;
675            case 'modules':
676                $this->registerModules($value);
677                break;
678            case 'autoloader':
679                $this->registerAutoloader($value);
680                break;
681        }
682    }
683
684    /**
685     * Get a pre-designated value from the application object
686     *
687     * @param  string $name
688     * @return mixed
689     */
690    public function __get(string $name): mixed
691    {
692        return match ($name) {
693            'config'     => $this->config,
694            'router'     => $this->router,
695            'services'   => $this->services,
696            'events'     => $this->events,
697            'modules'    => $this->modules,
698            'autoloader' => $this->autoloader,
699            default      => null,
700        };
701    }
702
703    /**
704     * Determine if a pre-designated value in the application object exists
705     *
706     * @param  string $name
707     * @return bool
708     */
709    public function __isset(string $name): bool
710    {
711        return match ($name) {
712            'config'     => ($this->config !== null),
713            'router'     => ($this->router !== null),
714            'services'   => ($this->services !== null),
715            'events'     => ($this->events !== null),
716            'modules'    => ($this->modules !== null),
717            'autoloader' => ($this->autoloader !== null),
718            default      => false,
719        };
720    }
721
722    /**
723     * Unset a pre-designated value in the application object
724     *
725     * @param  string $name
726     * @return void
727     */
728    public function __unset(string $name): void
729    {
730        switch ($name) {
731            case 'config':
732                $this->config = null;
733                break;
734            case 'router':
735                $this->router = null;
736                break;
737            case 'services':
738                $this->services = null;
739                break;
740            case 'events':
741                $this->events = null;
742                break;
743            case 'modules':
744                $this->modules = null;
745                break;
746            case 'autoloader':
747                $this->autoloader = null;
748                break;
749        }
750    }
751
752    /**
753     * Set a pre-designated value in the application object
754     *
755     * @param  mixed $offset
756     * @param  mixed $value
757     * @throws Exception
758     * @return void
759     */
760    public function offsetSet(mixed $offset, mixed $value): void
761    {
762        $this->__set($offset, $value);
763    }
764
765    /**
766     * Get a pre-designated value from the application object
767     *
768     * @param  mixed $offset
769     * @return mixed
770     */
771    public function offsetGet(mixed $offset): mixed
772    {
773        return $this->__get($offset);
774    }
775
776    /**
777     * Determine if a pre-designated value in the application object exists
778     *
779     * @param  mixed $offset
780     * @return bool
781     */
782    public function offsetExists(mixed $offset): bool
783    {
784        return $this->__isset($offset);
785    }
786
787    /**
788     * Unset a pre-designated value in the application object
789     *
790     * @param  mixed $offset
791     * @return void
792     */
793    public function offsetUnset(mixed $offset): void
794    {
795        $this->__unset($offset);
796    }
797
798}