• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12855700662

19 Jan 2025 05:28PM UTC coverage: 84.546% (+0.08%) from 84.469%
12855700662

push

github

web-flow
Merge pull request #9417 from codeigniter4/4.6

4.6.0 Merge code

720 of 841 new or added lines in 68 files covered. (85.61%)

8 existing lines in 6 files now uncovered.

20811 of 24615 relevant lines covered (84.55%)

191.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.78
/system/CodeIgniter.php
1
<?php
2

3
/**
4
 * This file is part of CodeIgniter 4 framework.
5
 *
6
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11

12
namespace CodeIgniter;
13

14
use Closure;
15
use CodeIgniter\Cache\ResponseCache;
16
use CodeIgniter\Debug\Timer;
17
use CodeIgniter\Events\Events;
18
use CodeIgniter\Exceptions\FrameworkException;
19
use CodeIgniter\Exceptions\LogicException;
20
use CodeIgniter\Exceptions\PageNotFoundException;
21
use CodeIgniter\Filters\Filters;
22
use CodeIgniter\HTTP\CLIRequest;
23
use CodeIgniter\HTTP\DownloadResponse;
24
use CodeIgniter\HTTP\Exceptions\RedirectException;
25
use CodeIgniter\HTTP\IncomingRequest;
26
use CodeIgniter\HTTP\Method;
27
use CodeIgniter\HTTP\RedirectResponse;
28
use CodeIgniter\HTTP\Request;
29
use CodeIgniter\HTTP\ResponsableInterface;
30
use CodeIgniter\HTTP\ResponseInterface;
31
use CodeIgniter\HTTP\URI;
32
use CodeIgniter\Router\RouteCollectionInterface;
33
use CodeIgniter\Router\Router;
34
use Config\App;
35
use Config\Cache;
36
use Config\Feature;
37
use Config\Kint as KintConfig;
38
use Config\Services;
39
use Exception;
40
use Kint;
41
use Kint\Renderer\CliRenderer;
42
use Kint\Renderer\RichRenderer;
43
use Locale;
44
use Throwable;
45

46
/**
47
 * This class is the core of the framework, and will analyse the
48
 * request, route it to a controller, and send back the response.
49
 * Of course, there are variations to that flow, but this is the brains.
50
 *
51
 * @see \CodeIgniter\CodeIgniterTest
52
 */
53
class CodeIgniter
54
{
55
    /**
56
     * The current version of CodeIgniter Framework
57
     */
58
    public const CI_VERSION = '4.5.8';
59

60
    /**
61
     * App startup time.
62
     *
63
     * @var float|null
64
     */
65
    protected $startTime;
66

67
    /**
68
     * Total app execution time
69
     *
70
     * @var float
71
     */
72
    protected $totalTime;
73

74
    /**
75
     * Main application configuration
76
     *
77
     * @var App
78
     */
79
    protected $config;
80

81
    /**
82
     * Timer instance.
83
     *
84
     * @var Timer
85
     */
86
    protected $benchmark;
87

88
    /**
89
     * Current request.
90
     *
91
     * @var CLIRequest|IncomingRequest|null
92
     */
93
    protected $request;
94

95
    /**
96
     * Current response.
97
     *
98
     * @var ResponseInterface
99
     */
100
    protected $response;
101

102
    /**
103
     * Router to use.
104
     *
105
     * @var Router
106
     */
107
    protected $router;
108

109
    /**
110
     * Controller to use.
111
     *
112
     * @var (Closure(mixed...): ResponseInterface|string)|string|null
113
     */
114
    protected $controller;
115

116
    /**
117
     * Controller method to invoke.
118
     *
119
     * @var string
120
     */
121
    protected $method;
122

123
    /**
124
     * Output handler to use.
125
     *
126
     * @var string
127
     */
128
    protected $output;
129

130
    /**
131
     * Cache expiration time
132
     *
133
     * @var int seconds
134
     *
135
     * @deprecated 4.4.0 Moved to ResponseCache::$ttl. No longer used.
136
     */
137
    protected static $cacheTTL = 0;
138

139
    /**
140
     * Context
141
     *  web:     Invoked by HTTP request
142
     *  php-cli: Invoked by CLI via `php public/index.php`
143
     *
144
     * @phpstan-var 'php-cli'|'web'
145
     */
146
    protected ?string $context = null;
147

148
    /**
149
     * Whether to enable Control Filters.
150
     */
151
    protected bool $enableFilters = true;
152

153
    /**
154
     * Whether to return Response object or send response.
155
     *
156
     * @deprecated 4.4.0 No longer used.
157
     */
158
    protected bool $returnResponse = false;
159

160
    /**
161
     * Application output buffering level
162
     */
163
    protected int $bufferLevel;
164

165
    /**
166
     * Web Page Caching
167
     */
168
    protected ResponseCache $pageCache;
169

170
    /**
171
     * Constructor.
172
     */
173
    public function __construct(App $config)
174
    {
175
        $this->startTime = microtime(true);
6,576✔
176
        $this->config    = $config;
6,576✔
177

178
        $this->pageCache = Services::responsecache();
6,576✔
179
    }
180

181
    /**
182
     * Handles some basic app and environment setup.
183
     *
184
     * @return void
185
     */
186
    public function initialize()
187
    {
188
        // Set default locale on the server
189
        Locale::setDefault($this->config->defaultLocale ?? 'en');
6,576✔
190

191
        // Set default timezone on the server
192
        date_default_timezone_set($this->config->appTimezone ?? 'UTC');
6,576✔
193
    }
194

195
    /**
196
     * Checks system for missing required PHP extensions.
197
     *
198
     * @return void
199
     *
200
     * @throws FrameworkException
201
     *
202
     * @codeCoverageIgnore
203
     *
204
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
205
     */
206
    protected function resolvePlatformExtensions()
207
    {
208
        $requiredExtensions = [
×
209
            'intl',
×
210
            'json',
×
211
            'mbstring',
×
212
        ];
×
213

214
        $missingExtensions = [];
×
215

216
        foreach ($requiredExtensions as $extension) {
×
217
            if (! extension_loaded($extension)) {
×
218
                $missingExtensions[] = $extension;
×
219
            }
220
        }
221

222
        if ($missingExtensions !== []) {
×
223
            throw FrameworkException::forMissingExtension(implode(', ', $missingExtensions));
×
224
        }
225
    }
226

227
    /**
228
     * Initializes Kint
229
     *
230
     * @return void
231
     *
232
     * @deprecated 4.5.0 Moved to Autoloader.
233
     */
234
    protected function initializeKint()
235
    {
236
        if (CI_DEBUG) {
×
237
            $this->autoloadKint();
×
238
            $this->configureKint();
×
239
        } elseif (class_exists(Kint::class)) {
×
240
            // In case that Kint is already loaded via Composer.
241
            Kint::$enabled_mode = false;
×
242
            // @codeCoverageIgnore
243
        }
244

245
        helper('kint');
×
246
    }
247

248
    /**
249
     * @deprecated 4.5.0 Moved to Autoloader.
250
     */
251
    private function autoloadKint(): void
252
    {
253
        // If we have KINT_DIR it means it's already loaded via composer
254
        if (! defined('KINT_DIR')) {
×
255
            spl_autoload_register(function ($class): void {
×
256
                $class = explode('\\', $class);
×
257

258
                if (array_shift($class) !== 'Kint') {
×
259
                    return;
×
260
                }
261

262
                $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';
×
263

264
                if (is_file($file)) {
×
265
                    require_once $file;
×
266
                }
267
            });
×
268

269
            require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
×
270
        }
271
    }
272

273
    /**
274
     * @deprecated 4.5.0 Moved to Autoloader.
275
     */
276
    private function configureKint(): void
277
    {
278
        $config = new KintConfig();
×
279

280
        Kint::$depth_limit         = $config->maxDepth;
×
281
        Kint::$display_called_from = $config->displayCalledFrom;
×
282
        Kint::$expanded            = $config->expanded;
×
283

284
        if (isset($config->plugins) && is_array($config->plugins)) {
×
285
            Kint::$plugins = $config->plugins;
×
286
        }
287

288
        $csp = Services::csp();
×
289
        if ($csp->enabled()) {
×
290
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
×
291
            RichRenderer::$css_nonce = $csp->getStyleNonce();
×
292
        }
293

294
        RichRenderer::$theme  = $config->richTheme;
×
295
        RichRenderer::$folder = $config->richFolder;
×
296

297
        if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) {
×
298
            RichRenderer::$value_plugins = $config->richObjectPlugins;
×
299
        }
300
        if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) {
×
301
            RichRenderer::$tab_plugins = $config->richTabPlugins;
×
302
        }
303

304
        CliRenderer::$cli_colors         = $config->cliColors;
×
305
        CliRenderer::$force_utf8         = $config->cliForceUTF8;
×
306
        CliRenderer::$detect_width       = $config->cliDetectWidth;
×
307
        CliRenderer::$min_terminal_width = $config->cliMinWidth;
×
308
    }
309

310
    /**
311
     * Launch the application!
312
     *
313
     * This is "the loop" if you will. The main entry point into the script
314
     * that gets the required class instances, fires off the filters,
315
     * tries to route the response, loads the controller and generally
316
     * makes all the pieces work together.
317
     *
318
     * @param bool $returnResponse Used for testing purposes only.
319
     *
320
     * @return ResponseInterface|null
321
     */
322
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
323
    {
324
        if ($this->context === null) {
90✔
325
            throw new LogicException(
×
326
                'Context must be set before run() is called. If you are upgrading from 4.1.x, '
×
327
                . 'you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.',
×
328
            );
×
329
        }
330

331
        $this->pageCache->setTtl(0);
90✔
332
        $this->bufferLevel = ob_get_level();
90✔
333

334
        $this->startBenchmark();
90✔
335

336
        $this->getRequestObject();
90✔
337
        $this->getResponseObject();
90✔
338

339
        Events::trigger('pre_system');
90✔
340

341
        $this->benchmark->stop('bootstrap');
90✔
342

343
        $this->benchmark->start('required_before_filters');
90✔
344
        // Start up the filters
345
        $filters = Services::filters();
90✔
346
        // Run required before filters
347
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
90✔
348

349
        // If a ResponseInterface instance is returned then send it back to the client and stop
350
        if ($possibleResponse instanceof ResponseInterface) {
90✔
351
            $this->response = $possibleResponse;
3✔
352
        } else {
353
            try {
354
                $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
89✔
355
            } catch (ResponsableInterface $e) {
17✔
356
                $this->outputBufferingEnd();
6✔
357

358
                $this->response = $e->getResponse();
6✔
359
            } catch (PageNotFoundException $e) {
11✔
360
                $this->response = $this->display404errors($e);
11✔
UNCOV
361
            } catch (Throwable $e) {
×
UNCOV
362
                $this->outputBufferingEnd();
×
363

UNCOV
364
                throw $e;
×
365
            }
366
        }
367

368
        $this->runRequiredAfterFilters($filters);
83✔
369

370
        // Is there a post-system event?
371
        Events::trigger('post_system');
83✔
372

373
        if ($returnResponse) {
83✔
374
            return $this->response;
36✔
375
        }
376

377
        $this->sendResponse();
47✔
378

379
        return null;
47✔
380
    }
381

382
    /**
383
     * Run required before filters.
384
     */
385
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
386
    {
387
        $possibleResponse = $filters->runRequired('before');
90✔
388
        $this->benchmark->stop('required_before_filters');
90✔
389

390
        // If a ResponseInterface instance is returned then send it back to the client and stop
391
        if ($possibleResponse instanceof ResponseInterface) {
90✔
392
            return $possibleResponse;
3✔
393
        }
394

395
        return null;
89✔
396
    }
397

398
    /**
399
     * Run required after filters.
400
     */
401
    private function runRequiredAfterFilters(Filters $filters): void
402
    {
403
        $filters->setResponse($this->response);
83✔
404

405
        // Run required after filters
406
        $this->benchmark->start('required_after_filters');
83✔
407
        $response = $filters->runRequired('after');
83✔
408
        $this->benchmark->stop('required_after_filters');
83✔
409

410
        if ($response instanceof ResponseInterface) {
83✔
411
            $this->response = $response;
83✔
412
        }
413
    }
414

415
    /**
416
     * Invoked via php-cli command?
417
     */
418
    private function isPhpCli(): bool
419
    {
420
        return $this->context === 'php-cli';
52✔
421
    }
422

423
    /**
424
     * Web access?
425
     */
426
    private function isWeb(): bool
427
    {
428
        return $this->context === 'web';
90✔
429
    }
430

431
    /**
432
     * Disables Controller Filters.
433
     */
434
    public function disableFilters(): void
435
    {
436
        $this->enableFilters = false;
1✔
437
    }
438

439
    /**
440
     * Handles the main request logic and fires the controller.
441
     *
442
     * @return ResponseInterface
443
     *
444
     * @throws PageNotFoundException
445
     * @throws RedirectException
446
     *
447
     * @deprecated $returnResponse is deprecated.
448
     */
449
    protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false)
450
    {
451
        if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
89✔
452
            return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
1✔
453
        }
454

455
        $routeFilters = $this->tryToRouteIt($routes);
88✔
456

457
        // $uri is URL-encoded.
458
        $uri = $this->request->getPath();
71✔
459

460
        if ($this->enableFilters) {
71✔
461
            /** @var Filters $filters */
462
            $filters = service('filters');
70✔
463

464
            // If any filters were specified within the routes file,
465
            // we need to ensure it's active for the current request
466
            if ($routeFilters !== null) {
70✔
467
                $filters->enableFilters($routeFilters, 'before');
70✔
468

469
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
70✔
470
                if (! $oldFilterOrder) {
70✔
471
                    $routeFilters = array_reverse($routeFilters);
70✔
472
                }
473

474
                $filters->enableFilters($routeFilters, 'after');
70✔
475
            }
476

477
            // Run "before" filters
478
            $this->benchmark->start('before_filters');
70✔
479
            $possibleResponse = $filters->run($uri, 'before');
70✔
480
            $this->benchmark->stop('before_filters');
70✔
481

482
            // If a ResponseInterface instance is returned then send it back to the client and stop
483
            if ($possibleResponse instanceof ResponseInterface) {
70✔
484
                $this->outputBufferingEnd();
1✔
485

486
                return $possibleResponse;
1✔
487
            }
488

489
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
69✔
490
                $this->request = $possibleResponse;
69✔
491
            }
492
        }
493

494
        $returned = $this->startController();
70✔
495

496
        // Closure controller has run in startController().
497
        if (! is_callable($this->controller)) {
70✔
498
            $controller = $this->createController();
41✔
499

500
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
41✔
501
                throw PageNotFoundException::forMethodNotFound($this->method);
×
502
            }
503

504
            // Is there a "post_controller_constructor" event?
505
            Events::trigger('post_controller_constructor');
41✔
506

507
            $returned = $this->runController($controller);
41✔
508
        } else {
509
            $this->benchmark->stop('controller_constructor');
29✔
510
            $this->benchmark->stop('controller');
29✔
511
        }
512

513
        // If $returned is a string, then the controller output something,
514
        // probably a view, instead of echoing it directly. Send it along
515
        // so it can be used with the output.
516
        $this->gatherOutput($cacheConfig, $returned);
70✔
517

518
        if ($this->enableFilters) {
70✔
519
            /** @var Filters $filters */
520
            $filters = service('filters');
69✔
521
            $filters->setResponse($this->response);
69✔
522

523
            // Run "after" filters
524
            $this->benchmark->start('after_filters');
69✔
525
            $response = $filters->run($uri, 'after');
69✔
526
            $this->benchmark->stop('after_filters');
69✔
527

528
            if ($response instanceof ResponseInterface) {
69✔
529
                $this->response = $response;
69✔
530
            }
531
        }
532

533
        // Skip unnecessary processing for special Responses.
534
        if (
535
            ! $this->response instanceof DownloadResponse
70✔
536
            && ! $this->response instanceof RedirectResponse
70✔
537
        ) {
538
            // Save our current URI as the previous URI in the session
539
            // for safer, more accurate use with `previous_url()` helper function.
540
            $this->storePreviousURL(current_url(true));
68✔
541
        }
542

543
        unset($uri);
70✔
544

545
        return $this->response;
70✔
546
    }
547

548
    /**
549
     * You can load different configurations depending on your
550
     * current environment. Setting the environment also influences
551
     * things like logging and error reporting.
552
     *
553
     * This can be set to anything, but default usage is:
554
     *
555
     *     development
556
     *     testing
557
     *     production
558
     *
559
     * @codeCoverageIgnore
560
     *
561
     * @return void
562
     *
563
     * @deprecated 4.4.0 No longer used. Moved to index.php and spark.
564
     */
565
    protected function detectEnvironment()
566
    {
567
        // Make sure ENVIRONMENT isn't already set by other means.
568
        if (! defined('ENVIRONMENT')) {
×
569
            define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
×
570
        }
571
    }
572

573
    /**
574
     * Load any custom boot files based upon the current environment.
575
     *
576
     * If no boot file exists, we shouldn't continue because something
577
     * is wrong. At the very least, they should have error reporting setup.
578
     *
579
     * @return void
580
     *
581
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
582
     */
583
    protected function bootstrapEnvironment()
584
    {
585
        if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) {
×
586
            require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
×
587
        } else {
588
            // @codeCoverageIgnoreStart
589
            header('HTTP/1.1 503 Service Unavailable.', true, 503);
×
590
            echo 'The application environment is not set correctly.';
×
591

592
            exit(EXIT_ERROR); // EXIT_ERROR
×
593
            // @codeCoverageIgnoreEnd
594
        }
595
    }
596

597
    /**
598
     * Start the Benchmark
599
     *
600
     * The timer is used to display total script execution both in the
601
     * debug toolbar, and potentially on the displayed page.
602
     *
603
     * @return void
604
     */
605
    protected function startBenchmark()
606
    {
607
        if ($this->startTime === null) {
90✔
608
            $this->startTime = microtime(true);
×
609
        }
610

611
        $this->benchmark = Services::timer();
90✔
612
        $this->benchmark->start('total_execution', $this->startTime);
90✔
613
        $this->benchmark->start('bootstrap');
90✔
614
    }
615

616
    /**
617
     * Sets a Request object to be used for this request.
618
     * Used when running certain tests.
619
     *
620
     * @param CLIRequest|IncomingRequest $request
621
     *
622
     * @return $this
623
     *
624
     * @internal Used for testing purposes only.
625
     * @testTag
626
     */
627
    public function setRequest($request)
628
    {
629
        $this->request = $request;
38✔
630

631
        return $this;
38✔
632
    }
633

634
    /**
635
     * Get our Request object, (either IncomingRequest or CLIRequest).
636
     *
637
     * @return void
638
     */
639
    protected function getRequestObject()
640
    {
641
        if ($this->request instanceof Request) {
90✔
642
            $this->spoofRequestMethod();
40✔
643

644
            return;
40✔
645
        }
646

647
        if ($this->isPhpCli()) {
52✔
648
            Services::createRequest($this->config, true);
×
649
        } else {
650
            Services::createRequest($this->config);
52✔
651
        }
652

653
        $this->request = service('request');
52✔
654

655
        $this->spoofRequestMethod();
52✔
656
    }
657

658
    /**
659
     * Get our Response object, and set some default values, including
660
     * the HTTP protocol version and a default successful response.
661
     *
662
     * @return void
663
     */
664
    protected function getResponseObject()
665
    {
666
        $this->response = Services::response($this->config);
90✔
667

668
        if ($this->isWeb()) {
90✔
669
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
90✔
670
        }
671

672
        // Assume success until proven otherwise.
673
        $this->response->setStatusCode(200);
90✔
674
    }
675

676
    /**
677
     * Force Secure Site Access? If the config value 'forceGlobalSecureRequests'
678
     * is true, will enforce that all requests to this site are made through
679
     * HTTPS. Will redirect the user to the current page with HTTPS, as well
680
     * as set the HTTP Strict Transport Security header for those browsers
681
     * that support it.
682
     *
683
     * @param int $duration How long the Strict Transport Security
684
     *                      should be enforced for this URL.
685
     *
686
     * @return void
687
     *
688
     * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter.
689
     */
690
    protected function forceSecureAccess($duration = 31_536_000)
691
    {
692
        if ($this->config->forceGlobalSecureRequests !== true) {
×
693
            return;
×
694
        }
695

696
        force_https($duration, $this->request, $this->response);
×
697
    }
698

699
    /**
700
     * Determines if a response has been cached for the given URI.
701
     *
702
     * @return false|ResponseInterface
703
     *
704
     * @throws Exception
705
     *
706
     * @deprecated 4.5.0 PageCache required filter is used. No longer used.
707
     * @deprecated 4.4.2 The parameter $config is deprecated. No longer used.
708
     */
709
    public function displayCache(Cache $config)
710
    {
711
        $cachedResponse = $this->pageCache->get($this->request, $this->response);
×
712
        if ($cachedResponse instanceof ResponseInterface) {
×
713
            $this->response = $cachedResponse;
×
714

715
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
716
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
×
717
            $this->response->setBody($output);
×
718

719
            return $this->response;
×
720
        }
721

722
        return false;
×
723
    }
724

725
    /**
726
     * Tells the app that the final output should be cached.
727
     *
728
     * @deprecated 4.4.0 Moved to ResponseCache::setTtl(). No longer used.
729
     *
730
     * @return void
731
     */
732
    public static function cache(int $time)
733
    {
734
        static::$cacheTTL = $time;
1✔
735
    }
736

737
    /**
738
     * Caches the full response from the current request. Used for
739
     * full-page caching for very high performance.
740
     *
741
     * @return bool
742
     *
743
     * @deprecated 4.4.0 No longer used.
744
     */
745
    public function cachePage(Cache $config)
746
    {
747
        $headers = [];
×
748

749
        foreach ($this->response->headers() as $header) {
×
750
            $headers[$header->getName()] = $header->getValueLine();
×
751
        }
752

753
        return cache()->save($this->generateCacheName($config), serialize(['headers' => $headers, 'output' => $this->output]), static::$cacheTTL);
×
754
    }
755

756
    /**
757
     * Returns an array with our basic performance stats collected.
758
     */
759
    public function getPerformanceStats(): array
760
    {
761
        // After filter debug toolbar requires 'total_execution'.
762
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
763

764
        return [
×
765
            'startTime' => $this->startTime,
×
766
            'totalTime' => $this->totalTime,
×
767
        ];
×
768
    }
769

770
    /**
771
     * Generates the cache name to use for our full-page caching.
772
     *
773
     * @deprecated 4.4.0 No longer used.
774
     */
775
    protected function generateCacheName(Cache $config): string
776
    {
777
        if ($this->request instanceof CLIRequest) {
×
778
            return md5($this->request->getPath());
×
779
        }
780

781
        $uri = clone $this->request->getUri();
×
782

783
        $query = $config->cacheQueryString
×
784
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
×
785
            : '';
×
786

787
        return md5((string) $uri->setFragment('')->setQuery($query));
×
788
    }
789

790
    /**
791
     * Replaces the elapsed_time and memory_usage tag.
792
     *
793
     * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used.
794
     */
795
    public function displayPerformanceMetrics(string $output): string
796
    {
797
        return str_replace(
×
798
            ['{elapsed_time}', '{memory_usage}'],
×
799
            [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)],
×
800
            $output,
×
801
        );
×
802
    }
803

804
    /**
805
     * Try to Route It - As it sounds like, works with the router to
806
     * match a route against the current URI. If the route is a
807
     * "redirect route", will also handle the redirect.
808
     *
809
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
810
     *                                              of the config file.
811
     *
812
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
813
     *
814
     * @throws RedirectException
815
     */
816
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
817
    {
818
        $this->benchmark->start('routing');
88✔
819

820
        if (! $routes instanceof RouteCollectionInterface) {
88✔
821
            $routes = service('routes')->loadRoutes();
29✔
822
        }
823

824
        // $routes is defined in Config/Routes.php
825
        $this->router = Services::router($routes, $this->request);
88✔
826

827
        // $uri is URL-encoded.
828
        $uri = $this->request->getPath();
88✔
829

830
        $this->outputBufferingStart();
88✔
831

832
        $this->controller = $this->router->handle($uri);
88✔
833
        $this->method     = $this->router->methodName();
71✔
834

835
        // If a {locale} segment was matched in the final route,
836
        // then we need to set the correct locale on our Request.
837
        if ($this->router->hasLocale()) {
71✔
838
            $this->request->setLocale($this->router->getLocale());
×
839
        }
840

841
        $this->benchmark->stop('routing');
71✔
842

843
        return $this->router->getFilters();
71✔
844
    }
845

846
    /**
847
     * Determines the path to use for us to try to route to, based
848
     * on the CLI/IncomingRequest path.
849
     *
850
     * @return string
851
     *
852
     * @deprecated 4.5.0 No longer used.
853
     */
854
    protected function determinePath()
855
    {
856
        return $this->request->getPath();
×
857
    }
858

859
    /**
860
     * Now that everything has been setup, this method attempts to run the
861
     * controller method and make the script go. If it's not able to, will
862
     * show the appropriate Page Not Found error.
863
     *
864
     * @return ResponseInterface|string|null
865
     */
866
    protected function startController()
867
    {
868
        $this->benchmark->start('controller');
71✔
869
        $this->benchmark->start('controller_constructor');
71✔
870

871
        // Is it routed to a Closure?
872
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
71✔
873
            $controller = $this->controller;
29✔
874

875
            return $controller(...$this->router->params());
29✔
876
        }
877

878
        // No controller specified - we don't know what to do now.
879
        if (! isset($this->controller)) {
42✔
880
            throw PageNotFoundException::forEmptyController();
×
881
        }
882

883
        // Try to autoload the class
884
        if (
885
            ! class_exists($this->controller, true)
42✔
886
            || ($this->method[0] === '_' && $this->method !== '__invoke')
42✔
887
        ) {
888
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
889
        }
890

891
        return null;
42✔
892
    }
893

894
    /**
895
     * Instantiates the controller class.
896
     *
897
     * @return Controller
898
     */
899
    protected function createController()
900
    {
901
        assert(is_string($this->controller));
902

903
        $class = new $this->controller();
44✔
904
        $class->initController($this->request, $this->response, Services::logger());
44✔
905

906
        $this->benchmark->stop('controller_constructor');
44✔
907

908
        return $class;
44✔
909
    }
910

911
    /**
912
     * Runs the controller, allowing for _remap methods to function.
913
     *
914
     * CI4 supports three types of requests:
915
     *  1. Web: URI segments become parameters, sent to Controllers via Routes,
916
     *      output controlled by Headers to browser
917
     *  2. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments,
918
     *      sent to Controllers via Routes, output varies
919
     *
920
     * @param Controller $class
921
     *
922
     * @return false|ResponseInterface|string|void
923
     */
924
    protected function runController($class)
925
    {
926
        // This is a Web request or PHP CLI request
927
        $params = $this->router->params();
41✔
928

929
        // The controller method param types may not be string.
930
        // So cannot set `declare(strict_types=1)` in this file.
931
        $output = method_exists($class, '_remap')
41✔
932
            ? $class->_remap($this->method, ...$params)
×
933
            : $class->{$this->method}(...$params);
41✔
934

935
        $this->benchmark->stop('controller');
41✔
936

937
        return $output;
41✔
938
    }
939

940
    /**
941
     * Displays a 404 Page Not Found error. If set, will try to
942
     * call the 404Override controller/method that was set in routing config.
943
     *
944
     * @return ResponseInterface|void
945
     */
946
    protected function display404errors(PageNotFoundException $e)
947
    {
948
        $this->response->setStatusCode($e->getCode());
11✔
949

950
        // Is there a 404 Override available?
951
        if ($override = $this->router->get404Override()) {
11✔
952
            $returned = null;
4✔
953

954
            if ($override instanceof Closure) {
4✔
955
                echo $override($e->getMessage());
1✔
956
            } elseif (is_array($override)) {
3✔
957
                $this->benchmark->start('controller');
3✔
958
                $this->benchmark->start('controller_constructor');
3✔
959

960
                $this->controller = $override[0];
3✔
961
                $this->method     = $override[1];
3✔
962

963
                $controller = $this->createController();
3✔
964

965
                $returned = $controller->{$this->method}($e->getMessage());
3✔
966

967
                $this->benchmark->stop('controller');
3✔
968
            }
969

970
            unset($override);
4✔
971

972
            $cacheConfig = config(Cache::class);
4✔
973
            $this->gatherOutput($cacheConfig, $returned);
4✔
974

975
            return $this->response;
4✔
976
        }
977

978
        $this->outputBufferingEnd();
7✔
979

980
        // Throws new PageNotFoundException and remove exception message on production.
981
        throw PageNotFoundException::forPageNotFound(
7✔
982
            (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : null,
7✔
983
        );
7✔
984
    }
985

986
    /**
987
     * Gathers the script output from the buffer, replaces some execution
988
     * time tag in the output and displays the debug toolbar, if required.
989
     *
990
     * @param Cache|null                    $cacheConfig Deprecated. No longer used.
991
     * @param ResponseInterface|string|null $returned
992
     *
993
     * @deprecated $cacheConfig is deprecated.
994
     *
995
     * @return void
996
     */
997
    protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
998
    {
999
        $this->output = $this->outputBufferingEnd();
74✔
1000

1001
        if ($returned instanceof DownloadResponse) {
74✔
1002
            $this->response = $returned;
1✔
1003

1004
            return;
1✔
1005
        }
1006
        // If the controller returned a response object,
1007
        // we need to grab the body from it so it can
1008
        // be added to anything else that might have been
1009
        // echoed already.
1010
        // We also need to save the instance locally
1011
        // so that any status code changes, etc, take place.
1012
        if ($returned instanceof ResponseInterface) {
73✔
1013
            $this->response = $returned;
20✔
1014
            $returned       = $returned->getBody();
20✔
1015
        }
1016

1017
        if (is_string($returned)) {
73✔
1018
            $this->output .= $returned;
64✔
1019
        }
1020

1021
        $this->response->setBody($this->output);
73✔
1022
    }
1023

1024
    /**
1025
     * If we have a session object to use, store the current URI
1026
     * as the previous URI. This is called just prior to sending the
1027
     * response to the client, and will make it available next request.
1028
     *
1029
     * This helps provider safer, more reliable previous_url() detection.
1030
     *
1031
     * @param string|URI $uri
1032
     *
1033
     * @return void
1034
     */
1035
    public function storePreviousURL($uri)
1036
    {
1037
        // Ignore CLI requests
1038
        if (! $this->isWeb()) {
68✔
1039
            return;
×
1040
        }
1041
        // Ignore AJAX requests
1042
        if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
68✔
1043
            return;
×
1044
        }
1045

1046
        // Ignore unroutable responses
1047
        if ($this->response instanceof DownloadResponse || $this->response instanceof RedirectResponse) {
68✔
1048
            return;
×
1049
        }
1050

1051
        // Ignore non-HTML responses
1052
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
68✔
1053
            return;
12✔
1054
        }
1055

1056
        // This is mainly needed during testing...
1057
        if (is_string($uri)) {
56✔
1058
            $uri = new URI($uri);
×
1059
        }
1060

1061
        if (isset($_SESSION)) {
56✔
1062
            session()->set('_ci_previous_url', URI::createURIString(
56✔
1063
                $uri->getScheme(),
56✔
1064
                $uri->getAuthority(),
56✔
1065
                $uri->getPath(),
56✔
1066
                $uri->getQuery(),
56✔
1067
                $uri->getFragment(),
56✔
1068
            ));
56✔
1069
        }
1070
    }
1071

1072
    /**
1073
     * Modifies the Request Object to use a different method if a POST
1074
     * variable called _method is found.
1075
     *
1076
     * @return void
1077
     */
1078
    public function spoofRequestMethod()
1079
    {
1080
        // Only works with POSTED forms
1081
        if ($this->request->getMethod() !== Method::POST) {
90✔
1082
            return;
75✔
1083
        }
1084

1085
        $method = $this->request->getPost('_method');
16✔
1086

1087
        if ($method === null) {
16✔
1088
            return;
14✔
1089
        }
1090

1091
        // Only allows PUT, PATCH, DELETE
1092
        if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
2✔
1093
            $this->request = $this->request->setMethod($method);
1✔
1094
        }
1095
    }
1096

1097
    /**
1098
     * Sends the output of this request back to the client.
1099
     * This is what they've been waiting for!
1100
     *
1101
     * @return void
1102
     */
1103
    protected function sendResponse()
1104
    {
1105
        $this->response->send();
47✔
1106
    }
1107

1108
    /**
1109
     * Exits the application, setting the exit code for CLI-based applications
1110
     * that might be watching.
1111
     *
1112
     * Made into a separate method so that it can be mocked during testing
1113
     * without actually stopping script execution.
1114
     *
1115
     * @param int $code
1116
     *
1117
     * @deprecated 4.4.0 No longer Used. Moved to index.php.
1118
     *
1119
     * @return void
1120
     */
1121
    protected function callExit($code)
1122
    {
1123
        exit($code); // @codeCoverageIgnore
×
1124
    }
1125

1126
    /**
1127
     * Sets the app context.
1128
     *
1129
     * @phpstan-param 'php-cli'|'web' $context
1130
     *
1131
     * @return $this
1132
     */
1133
    public function setContext(string $context)
1134
    {
1135
        $this->context = $context;
39✔
1136

1137
        return $this;
39✔
1138
    }
1139

1140
    protected function outputBufferingStart(): void
1141
    {
1142
        $this->bufferLevel = ob_get_level();
88✔
1143
        ob_start();
88✔
1144
    }
1145

1146
    protected function outputBufferingEnd(): string
1147
    {
1148
        $buffer = '';
88✔
1149

1150
        while (ob_get_level() > $this->bufferLevel) {
88✔
1151
            $buffer .= ob_get_contents();
88✔
1152
            ob_end_clean();
88✔
1153
        }
1154

1155
        return $buffer;
88✔
1156
    }
1157
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc