Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
558 / 558
100.00% covered (success)
100.00%
58 / 58
CRAP
100.00% covered (success)
100.00%
1 / 1
App
100.00% covered (success)
100.00%
558 / 558
100.00% covered (success)
100.00%
58 / 58
193
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 debugStart
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadConfigs
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 loadAutoloader
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 loadExceptionHandler
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 prepareToRun
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 runHttp
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 debugEnd
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 run
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 runCli
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 config
100.00% covered (success)
100.00%
1 / 1
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
 setService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeService
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 autoloader
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 setAutoloader
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 cache
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setCache
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 console
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setConsole
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 addCommand
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 debugger
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setDebugger
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 exceptionHandler
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setExceptionHandler
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 antiCsrf
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setAntiCsrf
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 database
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setDatabase
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 mailer
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setMailer
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 migrator
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setMigrator
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 language
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setLanguage
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
13
 negotiateLanguage
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 locator
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setLocator
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 logger
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setLogger
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 router
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 setRouter
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
10
 requireRouterFiles
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 request
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setServerVars
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setRequest
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 response
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 setResponse
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
9
 session
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setSession
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 validation
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setValidation
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 view
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 setView
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 isCli
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 setIsCli
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDebugging
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addDebugData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework MVC Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\MVC;
11
12use Framework\Autoload\Autoloader;
13use Framework\Autoload\Locator;
14use Framework\Cache\Cache;
15use Framework\Cache\Debug\CacheCollector;
16use Framework\Cache\Serializer;
17use Framework\CLI\Command;
18use Framework\CLI\Console;
19use Framework\Config\Config;
20use Framework\Database\Database;
21use Framework\Database\Debug\DatabaseCollector;
22use Framework\Database\Extra\Migrator;
23use Framework\Debug\Debugger;
24use Framework\Debug\ExceptionHandler;
25use Framework\Email\Debug\EmailCollector;
26use Framework\Email\Mailer;
27use Framework\Email\Mailers\SMTPMailer;
28use Framework\Helpers\Isolation;
29use Framework\HTTP\AntiCSRF;
30use Framework\HTTP\CSP;
31use Framework\HTTP\Debug\HTTPCollector;
32use Framework\HTTP\Request;
33use Framework\HTTP\Response;
34use Framework\Language\Debug\LanguageCollector;
35use Framework\Language\FallbackLevel;
36use Framework\Language\Language;
37use Framework\Log\Debug\LogCollector;
38use Framework\Log\Logger;
39use Framework\Log\Loggers\MultiFileLogger;
40use Framework\Log\LogLevel;
41use Framework\MVC\Debug\AppCollector;
42use Framework\MVC\Debug\ViewCollector;
43use Framework\Routing\Debug\RoutingCollector;
44use Framework\Routing\Router;
45use Framework\Session\Debug\SessionCollector;
46use Framework\Session\SaveHandlers\DatabaseHandler;
47use Framework\Session\Session;
48use Framework\Validation\Debug\ValidationCollector;
49use Framework\Validation\FilesValidator;
50use Framework\Validation\Validation;
51use LogicException;
52use ReflectionClass;
53use ReflectionException;
54
55/**
56 * Class App.
57 *
58 * @package mvc
59 */
60class App
61{
62    /**
63     * Array with keys with names of services and their values have arrays where
64     * the keys are the names of the instances and the values are the objects.
65     *
66     * @var array<string,array<string,object>>
67     */
68    protected static array $services = [];
69    /**
70     * Tells if the App is running.
71     *
72     * @var bool
73     */
74    protected static bool $isRunning = false;
75    /**
76     * The Config instance.
77     *
78     * @var Config|null
79     */
80    protected static ?Config $config;
81    /**
82     * Tells if the request is by command line. Updating directly makes it
83     * possible to run tests simulating HTTP or CLI.
84     *
85     * @var bool|null
86     */
87    protected static ?bool $isCli = null;
88    /**
89     * The App collector instance that is set when in debug mode.
90     *
91     * @var AppCollector
92     */
93    protected static AppCollector $debugCollector;
94    /**
95     * Variables set in the $_SERVER super-global in command-line requests.
96     *
97     * @var array<string,mixed>
98     */
99    protected static array $defaultServerVars = [
100        'REMOTE_ADDR' => '127.0.0.1',
101        'REQUEST_METHOD' => 'GET',
102        'REQUEST_URI' => '/',
103        'SERVER_PROTOCOL' => 'HTTP/1.1',
104        'HTTP_HOST' => 'localhost',
105    ];
106
107    /**
108     * Initialize the App.
109     *
110     * @param array<string,mixed>|Config|string|null $config The config
111     * @param bool $debug Set true to enable debug mode. False to disable.
112     */
113    public function __construct(Config | array | string $config = null, bool $debug = false)
114    {
115        if ($debug) {
116            $this->debugStart();
117        }
118        if (isset(static::$config)) {
119            throw new LogicException('App already initialized');
120        }
121        if ( ! $config instanceof Config) {
122            $config = new Config($config);
123        }
124        static::$config = $config;
125        if ($debug) {
126            static::debugger()->addCollector(static::$debugCollector, 'App');
127        }
128    }
129
130    /**
131     * Start debugging the App.
132     */
133    protected function debugStart() : void
134    {
135        static::$debugCollector = new AppCollector();
136        static::$debugCollector->setStartTime()->setStartMemory();
137        static::$debugCollector->setApp($this);
138    }
139
140    /**
141     * Load service configs catching exceptions.
142     *
143     * @param string $name The service name
144     *
145     * @return array<string,array<string,mixed>>|null The service configs or null
146     */
147    protected function loadConfigs(string $name) : array | null
148    {
149        $config = static::config();
150        try {
151            $config->load($name);
152        } catch (\LogicException) {
153        }
154        return $config->getInstances($name);
155    }
156
157    /**
158     * Make sure to load the autoloader service if its default config is set.
159     */
160    protected function loadAutoloader() : void
161    {
162        $config = static::config();
163        $autoloaderConfigs = $config->getInstances('autoloader');
164        if ($config->getDir() !== null) {
165            $autoloaderConfigs ??= $this->loadConfigs('autoloader');
166        }
167        if (isset($autoloaderConfigs['default'])) {
168            static::autoloader();
169        }
170    }
171
172    /**
173     * Make sure to load the exceptionHandler service if its default config is set.
174     */
175    protected function loadExceptionHandler() : void
176    {
177        $config = static::config();
178        $exceptionHandlerConfigs = $config->getInstances('exceptionHandler');
179        if ($config->getDir() !== null) {
180            $exceptionHandlerConfigs ??= $this->loadConfigs('exceptionHandler');
181        }
182        if ( ! isset($exceptionHandlerConfigs['default'])) {
183            $environment = static::isDebugging()
184                ? ExceptionHandler::DEVELOPMENT
185                : ExceptionHandler::PRODUCTION;
186            $config->set('exceptionHandler', [
187                'environment' => $environment,
188            ]);
189            $exceptionHandlerConfigs = $config->getInstances('exceptionHandler');
190        }
191        if (isset($exceptionHandlerConfigs['default'])) {
192            static::exceptionHandler();
193        }
194    }
195
196    /**
197     * Prepare the App to run via CLI or HTTP.
198     */
199    protected function prepareToRun() : void
200    {
201        if (static::$isRunning) {
202            throw new LogicException('App is already running');
203        }
204        static::$isRunning = true;
205        $this->loadAutoloader();
206        $this->loadExceptionHandler();
207    }
208
209    /**
210     * Run the App on HTTP requests.
211     */
212    public function runHttp() : void
213    {
214        $this->prepareToRun();
215        $router = static::router();
216        $response = $router->getResponse();
217        $router->match()
218            ->run($response->getRequest(), $response)
219            ->send();
220        if (static::isDebugging()) {
221            $this->debugEnd();
222        }
223    }
224
225    /**
226     * Ends the debugging of the App and prints the debugbar if there is no
227     * download file, if the request is not via AJAX and the Content-Type is
228     * text/html.
229     */
230    protected function debugEnd() : void
231    {
232        static::$debugCollector->setEndTime()->setEndMemory();
233        $response = static::router()->getResponse();
234        if ( ! $response->hasDownload()
235            && ! $response->getRequest()->isAjax()
236            && \str_contains(
237                (string) $response->getHeader('Content-Type'),
238                'text/html'
239            )
240        ) {
241            echo static::debugger()->renderDebugbar();
242        }
243    }
244
245    /**
246     * Detects if the request is via command-line and runs as a CLI request,
247     * otherwise runs as HTTP.
248     */
249    public function run() : void
250    {
251        static::isCli() ? $this->runCli() : $this->runHttp();
252    }
253
254    /**
255     * Run the App on CLI requests.
256     */
257    public function runCli() : void
258    {
259        $this->prepareToRun();
260        static::console()->run();
261    }
262
263    /**
264     * Get the Config instance.
265     *
266     * @return Config
267     */
268    public static function config() : Config
269    {
270        return static::$config;
271    }
272
273    /**
274     * Get a service.
275     *
276     * @param string $name Service name
277     * @param string $instance Service instance name
278     *
279     * @return object|null The service object or null
280     */
281    public static function getService(string $name, string $instance = 'default') : ?object
282    {
283        return static::$services[$name][$instance] ?? null;
284    }
285
286    /**
287     * Set a service.
288     *
289     * @template T of object
290     *
291     * @param string $name Service name
292     * @param T $service Service object
293     * @param string $instance Service instance name
294     *
295     * @return T The service object
296     */
297    public static function setService(
298        string $name,
299        object $service,
300        string $instance = 'default'
301    ) : object {
302        return static::$services[$name][$instance] = $service;
303    }
304
305    /**
306     * Remove services.
307     *
308     * @param string $name Service name
309     * @param string|null $instance Service instance name or null to remove all instances
310     */
311    public static function removeService(string $name, ?string $instance = 'default') : void
312    {
313        if ($instance === null) {
314            unset(static::$services[$name]);
315            return;
316        }
317        unset(static::$services[$name][$instance]);
318    }
319
320    /**
321     * Get a autoloader service.
322     *
323     * @param string $instance The autoloader instance name
324     *
325     * @return Autoloader
326     */
327    public static function autoloader(string $instance = 'default') : Autoloader
328    {
329        $service = static::getService('autoloader', $instance);
330        if ($service) {
331            return $service; // @phpstan-ignore-line
332        }
333        if (static::isDebugging()) {
334            $start = \microtime(true);
335            $service = static::setAutoloader($instance);
336            $end = \microtime(true);
337            $service->setDebugCollector(name: $instance);
338            static::debugger()->addCollector($service->getDebugCollector(), 'Autoload');
339            static::addDebugData('autoloader', $instance, $start, $end);
340            return $service;
341        }
342        return static::setAutoloader($instance);
343    }
344
345    /**
346     * Set a autoloader service.
347     *
348     * @param string $instance The autoloader instance name
349     *
350     * @return Autoloader
351     */
352    protected static function setAutoloader(string $instance) : Autoloader
353    {
354        $config = static::config()->get('autoloader', $instance);
355        $service = new Autoloader($config['register'] ?? true, $config['extensions'] ?? '.php');
356        if (isset($config['namespaces'])) {
357            $service->setNamespaces($config['namespaces']);
358        }
359        if (isset($config['classes'])) {
360            $service->setClasses($config['classes']);
361        }
362        return static::setService('autoloader', $service, $instance);
363    }
364
365    /**
366     * Get a cache service.
367     *
368     * @param string $instance The cache instance name
369     *
370     * @return Cache
371     */
372    public static function cache(string $instance = 'default') : Cache
373    {
374        $service = static::getService('cache', $instance);
375        if ($service) {
376            return $service; // @phpstan-ignore-line
377        }
378        if (static::isDebugging()) {
379            $start = \microtime(true);
380            $service = static::setCache($instance);
381            $end = \microtime(true);
382            $collector = new CacheCollector($instance);
383            $service->setDebugCollector($collector);
384            static::debugger()->addCollector($collector, 'Cache');
385            static::addDebugData('cache', $instance, $start, $end);
386            return $service;
387        }
388        return static::setCache($instance);
389    }
390
391    /**
392     * Set a cache service.
393     *
394     * @param string $instance The cache instance name
395     *
396     * @return Cache
397     */
398    protected static function setCache(string $instance) : Cache
399    {
400        $config = static::config()->get('cache', $instance);
401        $logger = null;
402        if (isset($config['logger_instance'])) {
403            $logger = static::logger($config['logger_instance']);
404        }
405        $config['serializer'] ??= Serializer::PHP;
406        if (\is_string($config['serializer'])) {
407            $config['serializer'] = Serializer::from($config['serializer']);
408        }
409        /**
410         * @var Cache $service
411         */
412        $service = new $config['class'](
413            $config['configs'] ?? [],
414            $config['prefix'] ?? null,
415            $config['serializer'],
416            $logger
417        );
418        return static::setService('cache', $service, $instance);
419    }
420
421    /**
422     * Get a console service.
423     *
424     * @param string $instance The console instance name
425     *
426     * @throws ReflectionException
427     *
428     * @return Console
429     */
430    public static function console(string $instance = 'default') : Console
431    {
432        $service = static::getService('console', $instance);
433        if ($service) {
434            return $service; // @phpstan-ignore-line
435        }
436        if (static::isDebugging()) {
437            $start = \microtime(true);
438            $service = static::setConsole($instance);
439            $end = \microtime(true);
440            static::addDebugData('console', $instance, $start, $end);
441            return $service;
442        }
443        return static::setConsole($instance);
444    }
445
446    /**
447     * Set a console service.
448     *
449     * @param string $instance The console instance name
450     *
451     * @throws ReflectionException
452     *
453     * @return Console
454     */
455    protected static function setConsole(string $instance) : Console
456    {
457        $config = static::config()->get('console', $instance);
458        $language = null;
459        if (isset($config['language_instance'])) {
460            $language = static::language($config['language_instance']);
461        }
462        $service = new Console($language);
463        $locator = static::locator($config['locator_instance'] ?? 'default');
464        if (isset($config['find_in_namespaces']) && $config['find_in_namespaces'] === true) {
465            foreach ($locator->getFiles('Commands') as $file) {
466                static::addCommand($file, $service, $locator);
467            }
468        }
469        if (isset($config['directories'])) {
470            foreach ($config['directories'] as $dir) {
471                foreach ((array) $locator->listFiles($dir) as $file) {
472                    static::addCommand($file, $service, $locator);
473                }
474            }
475        }
476        return static::setService('console', $service, $instance);
477    }
478
479    /**
480     * Detects if the file has a command and adds it to the console.
481     *
482     * @param string $file The file to get the command class
483     * @param Console $console The console to add the class
484     * @param Locator $locator The locator to get the class name in the file
485     *
486     * @throws ReflectionException
487     *
488     * @return bool True if the command was added. If not, it's false.
489     */
490    protected static function addCommand(string $file, Console $console, Locator $locator) : bool
491    {
492        $className = $locator->getClassName($file);
493        if ($className === null) {
494            return false;
495        }
496        if ( ! \class_exists($className)) {
497            Isolation::require($file);
498        }
499        $class = new ReflectionClass($className); // @phpstan-ignore-line
500        if ($class->isInstantiable() && $class->isSubclassOf(Command::class)) {
501            $console->addCommand($className); // @phpstan-ignore-line
502            return true;
503        }
504        return false;
505    }
506
507    /**
508     * Get a debugger service.
509     *
510     * @param string $instance The debugger instance name
511     *
512     * @return Debugger
513     */
514    public static function debugger(string $instance = 'default') : Debugger
515    {
516        $service = static::getService('debugger', $instance);
517        if ($service) {
518            return $service; // @phpstan-ignore-line
519        }
520        if (static::isDebugging()) {
521            $start = \microtime(true);
522            $service = static::setDebugger($instance);
523            $end = \microtime(true);
524            static::addDebugData('debugger', $instance, $start, $end);
525            return $service;
526        }
527        return static::setDebugger($instance);
528    }
529
530    /**
531     * Set a debugger service.
532     *
533     * @param string $instance The debugger instance name
534     *
535     * @return Debugger
536     */
537    protected static function setDebugger(string $instance) : Debugger
538    {
539        $config = static::config()->get('debugger');
540        $service = new Debugger();
541        if (isset($config['debugbar_view'])) {
542            $service->setDebugbarView($config['debugbar_view']);
543        }
544        if (isset($config['options'])) {
545            $service->setOptions($config['options']);
546        }
547        return static::setService('debugger', $service, $instance);
548    }
549
550    /**
551     * Get a exceptionHandler service.
552     *
553     * @param string $instance The exceptionHandler instance name
554     *
555     * @return ExceptionHandler
556     */
557    public static function exceptionHandler(string $instance = 'default') : ExceptionHandler
558    {
559        $service = static::getService('exceptionHandler', $instance);
560        if ($service) {
561            return $service; // @phpstan-ignore-line
562        }
563        if (static::isDebugging()) {
564            $start = \microtime(true);
565            $service = static::setExceptionHandler($instance);
566            $end = \microtime(true);
567            static::addDebugData('exceptionHandler', $instance, $start, $end);
568            return $service;
569        }
570        return static::setExceptionHandler($instance);
571    }
572
573    /**
574     * Set a exceptionHandler service.
575     *
576     * @param string $instance The exceptionHandler instance name
577     *
578     * @return ExceptionHandler
579     */
580    protected static function setExceptionHandler(string $instance) : ExceptionHandler
581    {
582        $config = static::config()->get('exceptionHandler');
583        $environment = $config['environment'] ?? ExceptionHandler::PRODUCTION;
584        $logger = null;
585        if (isset($config['logger_instance'])) {
586            $logger = static::logger($config['logger_instance']);
587        }
588        $language = null;
589        if (isset($config['language_instance'])) {
590            $language = static::language($config['language_instance']);
591        }
592        $service = new ExceptionHandler($environment, $logger, $language);
593        if (isset($config['development_view'])) {
594            $service->setDevelopmentView($config['development_view']);
595        }
596        if (isset($config['production_view'])) {
597            $service->setProductionView($config['production_view']);
598        }
599        $config['initialize'] ??= true;
600        if ($config['initialize'] === true) {
601            $service->initialize($config['handle_errors'] ?? true);
602        }
603        return static::setService('exceptionHandler', $service, $instance);
604    }
605
606    /**
607     * Get a antiCsrf service.
608     *
609     * @param string $instance The antiCsrf instance name
610     *
611     * @return AntiCSRF
612     */
613    public static function antiCsrf(string $instance = 'default') : AntiCSRF
614    {
615        $service = static::getService('antiCsrf', $instance);
616        if ($service) {
617            return $service; // @phpstan-ignore-line
618        }
619        if (static::isDebugging()) {
620            $start = \microtime(true);
621            $service = static::setAntiCsrf($instance);
622            $end = \microtime(true);
623            static::addDebugData('antiCsrf', $instance, $start, $end);
624            return $service;
625        }
626        return static::setAntiCsrf($instance);
627    }
628
629    /**
630     * Set a antiCsrf service.
631     *
632     * @param string $instance The antiCsrf instance name
633     *
634     * @return AntiCSRF
635     */
636    protected static function setAntiCsrf(string $instance) : AntiCSRF
637    {
638        $config = static::config()->get('antiCsrf', $instance);
639        static::session($config['session_instance'] ?? 'default');
640        $service = new AntiCSRF(static::request($config['request_instance'] ?? 'default'));
641        if (isset($config['token_name'])) {
642            $service->setTokenName($config['token_name']);
643        }
644        if (isset($config['enabled']) && $config['enabled'] === false) {
645            $service->disable();
646        }
647        return static::setService('antiCsrf', $service, $instance);
648    }
649
650    /**
651     * Get a database service.
652     *
653     * @param string $instance The database instance name
654     *
655     * @return Database
656     */
657    public static function database(string $instance = 'default') : Database
658    {
659        $service = static::getService('database', $instance);
660        if ($service) {
661            return $service; // @phpstan-ignore-line
662        }
663        if (static::isDebugging()) {
664            $start = \microtime(true);
665            $service = static::setDatabase($instance);
666            $end = \microtime(true);
667            $collector = new DatabaseCollector($instance);
668            $service->setDebugCollector($collector);
669            static::debugger()->addCollector($collector, 'Database');
670            static::addDebugData('database', $instance, $start, $end);
671            return $service;
672        }
673        return static::setDatabase($instance);
674    }
675
676    /**
677     * Set a database service.
678     *
679     * @param string $instance The database instance name
680     *
681     * @return Database
682     */
683    protected static function setDatabase(string $instance) : Database
684    {
685        $config = static::config()->get('database', $instance);
686        $logger = null;
687        if (isset($config['logger_instance'])) {
688            $logger = static::logger($config['logger_instance']);
689        }
690        return static::setService(
691            'database',
692            new Database($config['config'], logger: $logger),
693            $instance
694        );
695    }
696
697    /**
698     * Get a mailer service.
699     *
700     * @param string $instance The mailer instance name
701     *
702     * @return Mailer
703     */
704    public static function mailer(string $instance = 'default') : Mailer
705    {
706        $service = static::getService('mailer', $instance);
707        if ($service) {
708            return $service; // @phpstan-ignore-line
709        }
710        if (static::isDebugging()) {
711            $start = \microtime(true);
712            $service = static::setMailer($instance);
713            $end = \microtime(true);
714            $collector = new EmailCollector($instance);
715            $service->setDebugCollector($collector);
716            static::debugger()->addCollector($collector, 'Email');
717            static::addDebugData('mailer', $instance, $start, $end);
718            return $service;
719        }
720        return static::setMailer($instance);
721    }
722
723    /**
724     * Set a mailer service.
725     *
726     * @param string $instance The mailer instance name
727     *
728     * @return Mailer
729     */
730    protected static function setMailer(string $instance) : Mailer
731    {
732        $config = static::config()->get('mailer', $instance);
733        /**
734         * @var class-string<Mailer> $class
735         */
736        $class = $config['class'] ?? SMTPMailer::class;
737        return static::setService(
738            'mailer',
739            new $class($config['config']),
740            $instance
741        );
742    }
743
744    /**
745     * Get a migrator service.
746     *
747     * @param string $instance The migrator instance name
748     *
749     * @return Migrator
750     */
751    public static function migrator(string $instance = 'default') : Migrator
752    {
753        $service = static::getService('migrator', $instance);
754        if ($service) {
755            return $service; // @phpstan-ignore-line
756        }
757        if (static::isDebugging()) {
758            $start = \microtime(true);
759            $service = static::setMigrator($instance);
760            $end = \microtime(true);
761            static::addDebugData('migrator', $instance, $start, $end);
762            return $service;
763        }
764        return static::setMigrator($instance);
765    }
766
767    /**
768     * Set a migrator service.
769     *
770     * @param string $instance The migrator instance name
771     *
772     * @return Migrator
773     */
774    protected static function setMigrator(string $instance) : Migrator
775    {
776        $config = static::config()->get('migrator', $instance);
777        return static::setService(
778            'migrator',
779            new Migrator(
780                static::database($config['database_instance'] ?? 'default'),
781                $config['directories'],
782                $config['table'] ?? 'Migrations',
783            ),
784            $instance
785        );
786    }
787
788    /**
789     * Get a language service.
790     *
791     * @param string $instance The language instance name
792     *
793     * @return Language
794     */
795    public static function language(string $instance = 'default') : Language
796    {
797        $service = static::getService('language', $instance);
798        if ($service) {
799            return $service; // @phpstan-ignore-line
800        }
801        if (static::isDebugging()) {
802            $start = \microtime(true);
803            $service = static::setLanguage($instance);
804            $end = \microtime(true);
805            $collector = new LanguageCollector($instance);
806            $service->setDebugCollector($collector);
807            static::debugger()->addCollector($collector, 'Language');
808            static::addDebugData('language', $instance, $start, $end);
809            return $service;
810        }
811        return static::setLanguage($instance);
812    }
813
814    /**
815     * Set a language service.
816     *
817     * @param string $instance The language instance name
818     *
819     * @return Language
820     */
821    protected static function setLanguage(string $instance) : Language
822    {
823        $config = static::config()->get('language', $instance);
824        $service = new Language($config['default'] ?? 'en');
825        if (isset($config['current'])) {
826            $service->setCurrentLocale($config['current']);
827        }
828        if (isset($config['supported'])) {
829            $service->setSupportedLocales($config['supported']);
830        }
831        if (isset($config['negotiate']) && $config['negotiate'] === true) {
832            $service->setCurrentLocale(
833                static::negotiateLanguage($service, $config['request_instance'] ?? 'default')
834            );
835        }
836        if (isset($config['fallback_level'])) {
837            if (\is_int($config['fallback_level'])) {
838                $config['fallback_level'] = FallbackLevel::from($config['fallback_level']);
839            }
840            $service->setFallbackLevel($config['fallback_level']);
841        }
842        $config['directories'] ??= [];
843        if (isset($config['find_in_namespaces']) && $config['find_in_namespaces'] === true) {
844            foreach (static::autoloader($config['autoloader_instance'] ?? 'default')
845                ->getNamespaces() as $directories) {
846                foreach ($directories as $directory) {
847                    $directory .= 'Languages';
848                    if (\is_dir($directory)) {
849                        $config['directories'][] = $directory;
850                    }
851                }
852            }
853        }
854        if ($config['directories']) {
855            $service->setDirectories($config['directories']);
856        }
857        $service->addDirectory(__DIR__ . '/Languages');
858        return static::setService('language', $service, $instance);
859    }
860
861    /**
862     * Negotiates the language either via the command line or over HTTP.
863     *
864     * @param Language $language The current Language instance
865     * @param string $requestInstance The name of the Request instance to be used
866     *
867     * @return string The negotiated language
868     */
869    protected static function negotiateLanguage(Language $language, string $requestInstance = 'default') : string
870    {
871        if (static::isCli()) {
872            $supported = \array_map('\strtolower', $language->getSupportedLocales());
873            $lang = \getenv('LANG');
874            if ($lang) {
875                $lang = \explode('.', $lang, 2);
876                $lang = \strtolower($lang[0]);
877                $lang = \strtr($lang, ['_' => '-']);
878                if (\in_array($lang, $supported, true)) {
879                    return $lang;
880                }
881            }
882            return $language->getDefaultLocale();
883        }
884        return static::request($requestInstance)->negotiateLanguage(
885            $language->getSupportedLocales()
886        );
887    }
888
889    /**
890     * Get a locator service.
891     *
892     * @param string $instance The locator instance name
893     *
894     * @return Locator
895     */
896    public static function locator(string $instance = 'default') : Locator
897    {
898        $service = static::getService('locator', $instance);
899        if ($service) {
900            return $service; // @phpstan-ignore-line
901        }
902        if (static::isDebugging()) {
903            $start = \microtime(true);
904            $service = static::setLocator($instance);
905            $end = \microtime(true);
906            static::addDebugData('locator', $instance, $start, $end);
907            return $service;
908        }
909        return static::setLocator($instance);
910    }
911
912    /**
913     * Set a locator service.
914     *
915     * @param string $instance The locator instance name
916     *
917     * @return Locator
918     */
919    protected static function setLocator(string $instance) : Locator
920    {
921        $config = static::config()->get('locator', $instance);
922        return static::setService(
923            'locator',
924            new Locator(static::autoloader($config['autoloader_instance'] ?? 'default')),
925            $instance
926        );
927    }
928
929    /**
930     * Get a logger service.
931     *
932     * @param string $instance The logger instance name
933     *
934     * @return Logger
935     */
936    public static function logger(string $instance = 'default') : Logger
937    {
938        $service = static::getService('logger', $instance);
939        if ($service) {
940            return $service; // @phpstan-ignore-line
941        }
942        if (static::isDebugging()) {
943            $start = \microtime(true);
944            $service = static::setLogger($instance);
945            $end = \microtime(true);
946            $collector = new LogCollector($instance);
947            $service->setDebugCollector($collector);
948            static::debugger()->addCollector($collector, 'Log');
949            static::addDebugData('logger', $instance, $start, $end);
950            return $service;
951        }
952        return static::setLogger($instance);
953    }
954
955    /**
956     * Set a logger service.
957     *
958     * @param string $instance The logger instance name
959     *
960     * @return Logger
961     */
962    protected static function setLogger(string $instance) : Logger
963    {
964        $config = static::config()->get('logger', $instance);
965        /**
966         * @var class-string<Logger> $class
967         */
968        $class = $config['class'] ?? MultiFileLogger::class;
969        $config['level'] ??= LogLevel::DEBUG;
970        if (\is_int($config['level'])) {
971            $config['level'] = LogLevel::from($config['level']);
972        }
973        return static::setService(
974            'logger',
975            new $class(
976                $config['destination'],
977                $config['level'],
978                $config['config'] ?? [],
979            ),
980            $instance
981        );
982    }
983
984    /**
985     * Get a router service.
986     *
987     * @param string $instance The router instance name
988     *
989     * @return Router
990     */
991    public static function router(string $instance = 'default') : Router
992    {
993        $service = static::getService('router', $instance);
994        if ($service) {
995            return $service; // @phpstan-ignore-line
996        }
997        if (static::isDebugging()) {
998            $start = \microtime(true);
999            $config = (array) static::config()->get('router', $instance);
1000            $service = static::setRouter($instance, $config);
1001            $collector = new RoutingCollector($instance);
1002            $service->setDebugCollector($collector);
1003            if (isset($config['files'])) {
1004                static::requireRouterFiles($config['files'], $service);
1005            }
1006            $end = \microtime(true);
1007            static::debugger()->addCollector($collector, 'Routing');
1008            static::addDebugData('router', $instance, $start, $end);
1009            return $service;
1010        }
1011        return static::setRouter($instance);
1012    }
1013
1014    /**
1015     * Set a router service.
1016     *
1017     * @param string $instance The router instance name
1018     * @param array<mixed>|null $config The router instance configs or null
1019     *
1020     * @return Router
1021     */
1022    protected static function setRouter(string $instance, array $config = null) : Router
1023    {
1024        $requireFiles = $config === null;
1025        $config ??= static::config()->get('router', $instance);
1026        $language = null;
1027        if (isset($config['language_instance'])) {
1028            $language = static::language($config['language_instance']);
1029        }
1030        $service = static::setService('router', new Router(
1031            static::response($config['response_instance'] ?? 'default'),
1032            $language
1033        ), $instance);
1034        if (isset($config['auto_options']) && $config['auto_options'] === true) {
1035            $service->setAutoOptions();
1036        }
1037        if (isset($config['auto_methods']) && $config['auto_methods'] === true) {
1038            $service->setAutoMethods();
1039        }
1040        if ( ! empty($config['placeholders'])) {
1041            $service->addPlaceholder($config['placeholders']);
1042        }
1043        if ($requireFiles && isset($config['files'])) {
1044            static::requireRouterFiles($config['files'], $service);
1045        }
1046        if (isset($config['callback'])) {
1047            $config['callback']($service);
1048        }
1049        return $service;
1050    }
1051
1052    /**
1053     * Load files that set the routes.
1054     *
1055     * @param array<string> $files The path of the router files
1056     * @param Router $router
1057     */
1058    protected static function requireRouterFiles(array $files, Router $router) : void
1059    {
1060        foreach ($files as $file) {
1061            if ( ! \is_file($file)) {
1062                throw new LogicException('Invalid router file: ' . $file);
1063            }
1064            Isolation::require($file, ['router' => $router]);
1065        }
1066    }
1067
1068    /**
1069     * Get a request service.
1070     *
1071     * @param string $instance The request instance name
1072     *
1073     * @return Request
1074     */
1075    public static function request(string $instance = 'default') : Request
1076    {
1077        $service = static::getService('request', $instance);
1078        if ($service) {
1079            return $service; // @phpstan-ignore-line
1080        }
1081        if (static::isDebugging()) {
1082            $start = \microtime(true);
1083            $service = static::setRequest($instance);
1084            $end = \microtime(true);
1085            $collector = new HTTPCollector($instance);
1086            $collector->setRequest($service);
1087            static::debugger()->addCollector($collector, 'HTTP');
1088            static::addDebugData('request', $instance, $start, $end);
1089            return $service;
1090        }
1091        return static::setRequest($instance);
1092    }
1093
1094    /**
1095     * Overrides variables to be set in the $_SERVER super-global when the
1096     * request is made via the command line.
1097     *
1098     * @param array<string,mixed> $vars
1099     */
1100    protected static function setServerVars(array $vars = []) : void
1101    {
1102        $vars = \array_replace(static::$defaultServerVars, $vars);
1103        foreach ($vars as $key => $value) {
1104            $_SERVER[$key] ??= $value;
1105        }
1106    }
1107
1108    /**
1109     * Set a request service.
1110     *
1111     * @param string $instance The request instance name
1112     *
1113     * @return Request
1114     */
1115    protected static function setRequest(string $instance) : Request
1116    {
1117        $config = static::config()->get('request', $instance);
1118        if (static::isCli()) {
1119            static::setServerVars($config['server_vars'] ?? []);
1120        }
1121        $service = new Request($config['allowed_hosts'] ?? []);
1122        if (isset($config['force_https']) && $config['force_https'] === true) {
1123            $service->forceHttps();
1124        }
1125        return static::setService('request', $service, $instance);
1126    }
1127
1128    /**
1129     * Get a response service.
1130     *
1131     * @param string $instance The response instance name
1132     *
1133     * @return Response
1134     */
1135    public static function response(string $instance = 'default') : Response
1136    {
1137        $service = static::getService('response', $instance);
1138        if ($service) {
1139            return $service; // @phpstan-ignore-line
1140        }
1141        if (static::isDebugging()) {
1142            $start = \microtime(true);
1143            $service = static::setResponse($instance);
1144            $end = \microtime(true);
1145            $collection = static::debugger()->getCollection('HTTP');
1146            foreach ($collection->getCollectors() as $collector) {
1147                if ($collector->getName() === $instance) {
1148                    $service->setDebugCollector($collector); // @phpstan-ignore-line
1149                    break;
1150                }
1151            }
1152            static::addDebugData('response', $instance, $start, $end);
1153            return $service;
1154        }
1155        return static::setResponse($instance);
1156    }
1157
1158    /**
1159     * Set a response service.
1160     *
1161     * @param string $instance The response instance name
1162     *
1163     * @return Response
1164     */
1165    protected static function setResponse(string $instance) : Response
1166    {
1167        $config = static::config()->get('response', $instance);
1168        $service = new Response(static::request($config['request_instance'] ?? 'default'));
1169        if ( ! empty($config['headers'])) {
1170            $service->setHeaders($config['headers']);
1171        }
1172        if ( ! empty($config['auto_etag'])) {
1173            $service->setAutoEtag(
1174                $config['auto_etag']['active'] ?? true,
1175                $config['auto_etag']['hash_algo'] ?? null
1176            );
1177        }
1178        if (isset($config['auto_language']) && $config['auto_language'] === true) {
1179            $service->setContentLanguage(
1180                static::language($config['language_instance'] ?? 'default')->getCurrentLocale()
1181            );
1182        }
1183        if (isset($config['cache'])) {
1184            $config['cache'] === false
1185                ? $service->setNoCache()
1186                : $service->setCache($config['cache']['seconds'], $config['cache']['public'] ?? false);
1187        }
1188        if ( ! empty($config['csp'])) {
1189            $service->setCsp(new CSP($config['csp']));
1190        }
1191        if ( ! empty($config['csp_report_only'])) {
1192            $service->setCspReportOnly(new CSP($config['csp_report_only']));
1193        }
1194        return static::setService('response', $service, $instance);
1195    }
1196
1197    /**
1198     * Get a session service.
1199     *
1200     * @param string $instance The session instance name
1201     *
1202     * @return Session
1203     */
1204    public static function session(string $instance = 'default') : Session
1205    {
1206        $service = static::getService('session', $instance);
1207        if ($service) {
1208            return $service; // @phpstan-ignore-line
1209        }
1210        if (static::isDebugging()) {
1211            $start = \microtime(true);
1212            $service = static::setSession($instance);
1213            $end = \microtime(true);
1214            $collector = new SessionCollector($instance);
1215            $service->setDebugCollector($collector);
1216            static::debugger()->addCollector($collector, 'Session');
1217            static::addDebugData('session', $instance, $start, $end);
1218            return $service;
1219        }
1220        return static::setSession($instance);
1221    }
1222
1223    /**
1224     * Set a session service.
1225     *
1226     * @param string $instance The session instance name
1227     *
1228     * @return Session
1229     */
1230    protected static function setSession(string $instance) : Session
1231    {
1232        $config = static::config()->get('session', $instance);
1233        if (isset($config['save_handler']['class'])) {
1234            $logger = null;
1235            if (isset($config['logger_instance'])) {
1236                $logger = static::logger($config['logger_instance']);
1237            }
1238            $saveHandler = new $config['save_handler']['class'](
1239                $config['save_handler']['config'] ?? [],
1240                $logger
1241            );
1242            if ($saveHandler instanceof DatabaseHandler
1243                && isset($config['save_handler']['database_instance'])
1244            ) {
1245                $saveHandler->setDatabase(
1246                    static::database($config['save_handler']['database_instance'])
1247                );
1248            }
1249        }
1250        // @phpstan-ignore-next-line
1251        $service = new Session($config['options'] ?? [], $saveHandler ?? null);
1252        if (isset($config['auto_start']) && $config['auto_start'] === true) {
1253            $service->start();
1254        }
1255        return static::setService('session', $service, $instance);
1256    }
1257
1258    /**
1259     * Get a validation service.
1260     *
1261     * @param string $instance The validation instance name
1262     *
1263     * @return Validation
1264     */
1265    public static function validation(string $instance = 'default') : Validation
1266    {
1267        $service = static::getService('validation', $instance);
1268        if ($service) {
1269            return $service; // @phpstan-ignore-line
1270        }
1271        if (static::isDebugging()) {
1272            $start = \microtime(true);
1273            $service = static::setValidation($instance);
1274            $end = \microtime(true);
1275            $collector = new ValidationCollector($instance);
1276            $service->setDebugCollector($collector);
1277            static::debugger()->addCollector($collector, 'Validation');
1278            static::addDebugData('validation', $instance, $start, $end);
1279            return $service;
1280        }
1281        return static::setValidation($instance);
1282    }
1283
1284    /**
1285     * Set a validation service.
1286     *
1287     * @param string $instance The validation instance name
1288     *
1289     * @return Validation
1290     */
1291    protected static function setValidation(string $instance) : Validation
1292    {
1293        $config = static::config()->get('validation', $instance);
1294        $language = null;
1295        if (isset($config['language_instance'])) {
1296            $language = static::language($config['language_instance']);
1297        }
1298        return static::setService(
1299            'validation',
1300            new Validation(
1301                $config['validators'] ?? [
1302                Validator::class,
1303                FilesValidator::class,
1304            ],
1305                $language
1306            ),
1307            $instance
1308        );
1309    }
1310
1311    /**
1312     * Get a view service.
1313     *
1314     * @param string $instance The view instance name
1315     *
1316     * @return View
1317     */
1318    public static function view(string $instance = 'default') : View
1319    {
1320        $service = static::getService('view', $instance);
1321        if ($service) {
1322            return $service; // @phpstan-ignore-line
1323        }
1324        if (static::isDebugging()) {
1325            $start = \microtime(true);
1326            $service = static::setView($instance);
1327            $end = \microtime(true);
1328            $collector = new ViewCollector($instance);
1329            $service->setDebugCollector($collector);
1330            static::debugger()->addCollector($collector, 'View');
1331            static::addDebugData('view', $instance, $start, $end);
1332            return $service;
1333        }
1334        return static::setView($instance);
1335    }
1336
1337    /**
1338     * Set a view service.
1339     *
1340     * @param string $instance The view instance name
1341     *
1342     * @return View
1343     */
1344    protected static function setView(string $instance) : View
1345    {
1346        $config = static::config()->get('view', $instance);
1347        $service = new View($config['base_dir'] ?? null, $config['extension'] ?? '.php');
1348        if (isset($config['layout_prefix'])) {
1349            $service->setLayoutPrefix($config['layout_prefix']);
1350        }
1351        if (isset($config['include_prefix'])) {
1352            $service->setIncludePrefix($config['include_prefix']);
1353        }
1354        if (isset($config['show_debug_comments']) && $config['show_debug_comments'] === false) {
1355            $service->disableDebugComments();
1356        }
1357        return static::setService('view', $service, $instance);
1358    }
1359
1360    /**
1361     * Tell if it is a command-line request.
1362     *
1363     * @return bool
1364     */
1365    public static function isCli() : bool
1366    {
1367        if (static::$isCli === null) {
1368            static::$isCli = \PHP_SAPI === 'cli' || \defined('STDIN');
1369        }
1370        return static::$isCli;
1371    }
1372
1373    /**
1374     * Set if it is a CLI request. Used for testing.
1375     *
1376     * @param bool $is
1377     */
1378    public static function setIsCli(bool $is) : void
1379    {
1380        static::$isCli = $is;
1381    }
1382
1383    /**
1384     * Tell if the App is in debug mode.
1385     *
1386     * @return bool
1387     */
1388    public static function isDebugging() : bool
1389    {
1390        return isset(static::$debugCollector);
1391    }
1392
1393    /**
1394     * Add services data to the debug collector.
1395     *
1396     * @param string $service Service name
1397     * @param string $instance Service instance name
1398     * @param float $start Microtime right before setting up the service
1399     * @param float $end Microtime right after setting up the service
1400     */
1401    protected static function addDebugData(
1402        string $service,
1403        string $instance,
1404        float $start,
1405        float $end
1406    ) : void {
1407        static::$debugCollector->addData([
1408            'service' => $service,
1409            'instance' => $instance,
1410            'start' => $start,
1411            'end' => $end,
1412        ]);
1413    }
1414}