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

codeigniter4 / CodeIgniter4 / 29041275974

09 Jul 2026 06:36PM UTC coverage: 88.71%. Remained the same
29041275974

Pull #10369

github

web-flow
Merge 74c896b7e into 5b46a44c2
Pull Request #10369: refactor: simplify and optimize handleRequest method in CodeIgniter

22322 of 25163 relevant lines covered (88.71%)

213.86 hits per line

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

87.05
/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\LogicException;
19
use CodeIgniter\Exceptions\PageNotFoundException;
20
use CodeIgniter\Filters\Filters;
21
use CodeIgniter\HTTP\CLIRequest;
22
use CodeIgniter\HTTP\DownloadResponse;
23
use CodeIgniter\HTTP\Exceptions\RedirectException;
24
use CodeIgniter\HTTP\IncomingRequest;
25
use CodeIgniter\HTTP\Method;
26
use CodeIgniter\HTTP\RedirectResponse;
27
use CodeIgniter\HTTP\Request;
28
use CodeIgniter\HTTP\RequestInterface;
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\Routing;
39
use Config\Services;
40
use Exception;
41
use Kint;
42
use Kint\Renderer\CliRenderer;
43
use Kint\Renderer\RichRenderer;
44
use Locale;
45
use Throwable;
46

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

61
    /**
62
     * Spoofable HTTP methods
63
     */
64
    private const SPOOFABLE_METHODS = [Method::PUT, Method::PATCH, Method::DELETE];
65

66
    /**
67
     * App startup time.
68
     *
69
     * @var float|null
70
     */
71
    protected $startTime;
72

73
    /**
74
     * Total app execution time
75
     *
76
     * @var float
77
     */
78
    protected $totalTime;
79

80
    /**
81
     * Main application configuration
82
     *
83
     * @var App
84
     */
85
    protected $config;
86

87
    /**
88
     * Timer instance.
89
     *
90
     * @var Timer
91
     */
92
    protected $benchmark;
93

94
    /**
95
     * Current request.
96
     *
97
     * @var CLIRequest|IncomingRequest|null
98
     */
99
    protected $request;
100

101
    /**
102
     * Current response.
103
     *
104
     * @var ResponseInterface|null
105
     */
106
    protected $response;
107

108
    /**
109
     * Router to use.
110
     *
111
     * @var Router|null
112
     */
113
    protected $router;
114

115
    /**
116
     * Controller to use.
117
     *
118
     * @var (Closure(mixed...): ResponseInterface|string)|string|null
119
     */
120
    protected $controller;
121

122
    /**
123
     * Controller method to invoke.
124
     *
125
     * @var string|null
126
     */
127
    protected $method;
128

129
    /**
130
     * Output handler to use.
131
     *
132
     * @var string|null
133
     */
134
    protected $output;
135

136
    /**
137
     * Cache expiration time
138
     *
139
     * @var int seconds
140
     *
141
     * @deprecated 4.4.0 Moved to ResponseCache::$ttl. No longer used.
142
     */
143
    protected static $cacheTTL = 0;
144

145
    /**
146
     * Context
147
     *  web:     Invoked by HTTP request
148
     *  php-cli: Invoked by CLI via `php public/index.php`
149
     *
150
     * @var 'php-cli'|'web'|null
151
     */
152
    protected ?string $context = null;
153

154
    /**
155
     * Whether to enable Control Filters.
156
     */
157
    protected bool $enableFilters = true;
158

159
    /**
160
     * Whether to return Response object or send response.
161
     *
162
     * @deprecated 4.4.0 No longer used.
163
     */
164
    protected bool $returnResponse = false;
165

166
    /**
167
     * Application output buffering level
168
     */
169
    protected int $bufferLevel;
170

171
    /**
172
     * Web Page Caching
173
     */
174
    protected ResponseCache $pageCache;
175

176
    /**
177
     * Constructor.
178
     */
179
    public function __construct(App $config)
180
    {
181
        $this->startTime = microtime(true);
7,427✔
182
        $this->config    = $config;
7,427✔
183

184
        $this->pageCache = Services::responsecache();
7,427✔
185
    }
186

187
    /**
188
     * Handles some basic app and environment setup.
189
     *
190
     * @return void
191
     */
192
    public function initialize()
193
    {
194
        // Set default locale on the server
195
        Locale::setDefault($this->config->defaultLocale);
7,427✔
196

197
        // Set default timezone on the server
198
        date_default_timezone_set($this->config->appTimezone);
7,427✔
199
    }
200

201
    /**
202
     * Reset request-specific state for worker mode.
203
     * Clears all request/response data to prepare for the next request.
204
     */
205
    public function resetForWorkerMode(): void
206
    {
207
        $this->request    = null;
1✔
208
        $this->response   = null;
1✔
209
        $this->router     = null;
1✔
210
        $this->controller = null;
1✔
211
        $this->method     = null;
1✔
212
        $this->output     = null;
1✔
213

214
        // Reset timing
215
        $this->startTime = null;
1✔
216
        $this->totalTime = 0;
1✔
217

218
        $this->resetKintForWorkerMode();
1✔
219
    }
220

221
    /**
222
     * Resets Kint request-specific state for worker mode.
223
     */
224
    private function resetKintForWorkerMode(): void
225
    {
226
        if (! CI_DEBUG || ! class_exists(Kint::class, false)) {
1✔
227
            return;
×
228
        }
229

230
        $csp = Services::csp();
1✔
231
        if ($csp->enabled()) {
1✔
232
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
1✔
233
            RichRenderer::$css_nonce = $csp->getStyleNonce();
1✔
234
        } else {
235
            RichRenderer::$js_nonce  = null;
×
236
            RichRenderer::$css_nonce = null;
×
237
        }
238

239
        RichRenderer::$needs_pre_render = true;
1✔
240
    }
241

242
    /**
243
     * Initializes Kint
244
     *
245
     * @return void
246
     *
247
     * @deprecated 4.5.0 Moved to Autoloader.
248
     */
249
    protected function initializeKint()
250
    {
251
        if (CI_DEBUG) {
252
            $this->autoloadKint();
253
            $this->configureKint();
254
        } elseif (class_exists(Kint::class)) {
255
            // In case that Kint is already loaded via Composer.
256
            Kint::$enabled_mode = false;
257
            // @codeCoverageIgnore
258
        }
259

260
        helper('kint');
261
    }
262

263
    /**
264
     * @deprecated 4.5.0 Moved to Autoloader.
265
     */
266
    private function autoloadKint(): void
267
    {
268
        // If we have KINT_DIR it means it's already loaded via composer
269
        if (! defined('KINT_DIR')) {
270
            spl_autoload_register(function ($class): void {
271
                $class = explode('\\', $class);
272

273
                if (array_shift($class) !== 'Kint') {
274
                    return;
275
                }
276

277
                $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';
278

279
                if (is_file($file)) {
280
                    require_once $file;
281
                }
282
            });
283

284
            require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
285
        }
286
    }
287

288
    /**
289
     * @deprecated 4.5.0 Moved to Autoloader.
290
     */
291
    private function configureKint(): void
292
    {
293
        $config = new KintConfig();
294

295
        Kint::$depth_limit         = $config->maxDepth;
296
        Kint::$display_called_from = $config->displayCalledFrom;
297
        Kint::$expanded            = $config->expanded;
298

299
        if (isset($config->plugins) && is_array($config->plugins)) {
300
            Kint::$plugins = $config->plugins;
301
        }
302

303
        $csp = Services::csp();
304
        if ($csp->enabled()) {
305
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
306
            RichRenderer::$css_nonce = $csp->getStyleNonce();
307
        }
308

309
        RichRenderer::$theme  = $config->richTheme;
310
        RichRenderer::$folder = $config->richFolder;
311

312
        if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) {
313
            RichRenderer::$value_plugins = $config->richObjectPlugins;
314
        }
315
        if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) {
316
            RichRenderer::$tab_plugins = $config->richTabPlugins;
317
        }
318

319
        CliRenderer::$cli_colors         = $config->cliColors;
320
        CliRenderer::$force_utf8         = $config->cliForceUTF8;
321
        CliRenderer::$detect_width       = $config->cliDetectWidth;
322
        CliRenderer::$min_terminal_width = $config->cliMinWidth;
323
    }
324

325
    /**
326
     * Launch the application!
327
     *
328
     * This is "the loop" if you will. The main entry point into the script
329
     * that gets the required class instances, fires off the filters,
330
     * tries to route the response, loads the controller and generally
331
     * makes all the pieces work together.
332
     *
333
     * @param bool $returnResponse Used for testing purposes only.
334
     *
335
     * @return ResponseInterface|null
336
     */
337
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
338
    {
339
        if ($this->context === null) {
100✔
340
            throw new LogicException(
×
341
                'Context must be set before run() is called. If you are upgrading from 4.1.x, '
×
342
                . 'you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.',
×
343
            );
×
344
        }
345

346
        $this->pageCache->setTtl(0);
100✔
347
        $this->bufferLevel = ob_get_level();
100✔
348

349
        $this->startBenchmark();
100✔
350

351
        $this->getRequestObject();
100✔
352
        $this->getResponseObject();
100✔
353

354
        Events::trigger('pre_system');
100✔
355

356
        $this->benchmark->stop('bootstrap');
100✔
357

358
        $this->benchmark->start('required_before_filters');
100✔
359
        // Start up the filters
360
        $filters = Services::filters();
100✔
361
        // Run required before filters
362
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
100✔
363

364
        // If a ResponseInterface instance is returned then send it back to the client and stop
365
        if ($possibleResponse instanceof ResponseInterface) {
100✔
366
            $this->response = $possibleResponse;
3✔
367
        } else {
368
            try {
369
                $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
99✔
370
            } catch (ResponsableInterface $e) {
18✔
371
                $this->outputBufferingEnd();
6✔
372

373
                $this->response = $e->getResponse();
6✔
374
            } catch (PageNotFoundException $e) {
12✔
375
                $this->response = $this->display404errors($e);
12✔
376
            } catch (Throwable $e) {
×
377
                $this->outputBufferingEnd();
×
378

379
                throw $e;
×
380
            }
381
        }
382

383
        $this->runRequiredAfterFilters($filters);
92✔
384

385
        // Is there a post-system event?
386
        Events::trigger('post_system');
92✔
387

388
        if ($returnResponse) {
92✔
389
            return $this->response;
36✔
390
        }
391

392
        $this->sendResponse();
56✔
393

394
        return null;
56✔
395
    }
396

397
    /**
398
     * Run required before filters.
399
     */
400
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
401
    {
402
        $possibleResponse = $filters->runRequired('before');
100✔
403
        $this->benchmark->stop('required_before_filters');
100✔
404

405
        // If a ResponseInterface instance is returned then send it back to the client and stop
406
        if ($possibleResponse instanceof ResponseInterface) {
100✔
407
            return $possibleResponse;
3✔
408
        }
409

410
        return null;
99✔
411
    }
412

413
    /**
414
     * Run required after filters.
415
     */
416
    private function runRequiredAfterFilters(Filters $filters): void
417
    {
418
        $filters->setResponse($this->response);
92✔
419

420
        // Run required after filters
421
        $this->benchmark->start('required_after_filters');
92✔
422
        $response = $filters->runRequired('after');
92✔
423
        $this->benchmark->stop('required_after_filters');
92✔
424

425
        if ($response instanceof ResponseInterface) {
92✔
426
            $this->response = $response;
92✔
427
        }
428
    }
429

430
    /**
431
     * Invoked via php-cli command?
432
     */
433
    private function isPhpCli(): bool
434
    {
435
        return $this->context === 'php-cli';
62✔
436
    }
437

438
    /**
439
     * Web access?
440
     */
441
    private function isWeb(): bool
442
    {
443
        return $this->context === 'web';
100✔
444
    }
445

446
    /**
447
     * Disables Controller Filters.
448
     */
449
    public function disableFilters(): void
450
    {
451
        $this->enableFilters = false;
1✔
452
    }
453

454
    /**
455
     * Handles the main request logic and fires the controller.
456
     *
457
     * @return ResponseInterface
458
     *
459
     * @throws PageNotFoundException
460
     * @throws RedirectException
461
     *
462
     * @deprecated $returnResponse is deprecated.
463
     */
464
    protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false)
465
    {
466
        if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
467
            return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
468
        }
469

470
        // $uri is URL-encoded.
471
        $uri = $this->request->getPath();
472

473
        $routeFilters = $this->tryToRouteIt($routes, $uri);
474

475
        if ($this->enableFilters) {
476
            /** @var Filters $filters */
477
            $filters = Services::filters();
478

479
            // If any filters were specified within the routes file,
480
            // we need to ensure it's active for the current request
481
            if ($routeFilters !== null) {
482
                $filters->enableFilters($routeFilters, 'before');
483

484
                if (! config(Feature::class)->oldFilterOrder) {
485
                    $routeFilters = array_reverse($routeFilters);
486
                }
487

488
                $filters->enableFilters($routeFilters, 'after');
489
            }
490

491
            // Run "before" filters
492
            $this->benchmark->start('before_filters');
493
            $possibleResponse = $filters->run($uri, 'before');
494
            $this->benchmark->stop('before_filters');
495

496
            // If a ResponseInterface instance is returned then send it back to the client and stop
497
            if ($possibleResponse instanceof ResponseInterface) {
498
                $this->outputBufferingEnd();
499

500
                return $possibleResponse;
501
            }
502

503
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
504
                $this->request = $possibleResponse;
505
            }
506
        }
507

508
        $returned = $this->startController();
509
        $gathered = false;
510

511
        // If startController returned a Response (from an attribute or Closure), use it
512
        if ($returned instanceof ResponseInterface) {
513
            $this->gatherOutput($cacheConfig, $returned);
514
            $gathered = true;
515
        }
516
        // Closure controller has already run inside startController().
517
        // Use instanceof instead of is_callable() — 88x faster for this check.
518
        elseif (! $this->controller instanceof Closure) {
519
            $controller = $this->createController();
520

521
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
522
                throw PageNotFoundException::forMethodNotFound($this->method);
523
            }
524

525
            // Is there a "post_controller_constructor" event?
526
            Events::trigger('post_controller_constructor');
527

528
            $returned = $this->runController($controller);
529
        } else {
530
            $this->benchmark->stop('controller_constructor');
531
            $this->benchmark->stop('controller');
532
        }
533

534
        // If $returned is a string, then the controller output something,
535
        // probably a view, instead of echoing it directly. Send it along
536
        // so it can be used with the output.
537
        if (! $gathered) {
538
            $this->gatherOutput($cacheConfig, $returned);
539
        }
540

541
        if ($this->enableFilters) {
542
            /** @var Filters $filters */
543
            $filters = Services::filters();
544
            $filters->setResponse($this->response);
545

546
            // Run "after" filters
547
            $this->benchmark->start('after_filters');
548
            $response = $filters->run($uri, 'after');
549
            $this->benchmark->stop('after_filters');
550

551
            if ($response instanceof ResponseInterface) {
552
                $this->response = $response;
553
            }
554
        }
555

556
        // Execute controller attributes' after() methods AFTER framework filters.
557
        if (config(Routing::class)->useControllerAttributes === true) {
558
            $this->benchmark->start('route_attributes_after');
559
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
560
            $this->benchmark->stop('route_attributes_after');
561
        }
562

563
        // Skip unnecessary processing for special Responses.
564
        if (
565
            ! $this->response instanceof DownloadResponse
566
            && ! $this->response instanceof RedirectResponse
567
        ) {
568
            // Save our current URI as the previous URI in the session
569
            // for safer, more accurate use with `previous_url()` helper function.
570
            $this->storePreviousURL(current_url(true));
571
        }
572

573
        return $this->response;
574
    }
575

576
    /**
577
     * You can load different configurations depending on your
578
     * current environment. Setting the environment also influences
579
     * things like logging and error reporting.
580
     *
581
     * This can be set to anything, but default usage is:
582
     *
583
     *     development
584
     *     testing
585
     *     production
586
     *
587
     * @codeCoverageIgnore
588
     *
589
     * @return void
590
     *
591
     * @deprecated 4.4.0 No longer used. Moved to index.php and spark.
592
     */
593
    protected function detectEnvironment()
594
    {
595
        // Make sure ENVIRONMENT isn't already set by other means.
596
        if (! defined('ENVIRONMENT')) {
597
            define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
598
        }
599
    }
600

601
    /**
602
     * Load any custom boot files based upon the current environment.
603
     *
604
     * If no boot file exists, we shouldn't continue because something
605
     * is wrong. At the very least, they should have error reporting setup.
606
     *
607
     * @return void
608
     *
609
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
610
     */
611
    protected function bootstrapEnvironment()
612
    {
613
        if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) {
614
            require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
615
        } else {
616
            // @codeCoverageIgnoreStart
617
            header('HTTP/1.1 503 Service Unavailable.', true, 503);
618
            echo 'The application environment is not set correctly.';
619

620
            exit(EXIT_ERROR); // EXIT_ERROR
621
            // @codeCoverageIgnoreEnd
622
        }
623
    }
624

625
    /**
626
     * Start the Benchmark
627
     *
628
     * The timer is used to display total script execution both in the
629
     * debug toolbar, and potentially on the displayed page.
630
     *
631
     * @return void
632
     */
633
    protected function startBenchmark()
634
    {
635
        if ($this->startTime === null) {
100✔
636
            $this->startTime = microtime(true);
×
637
        }
638

639
        $this->benchmark = Services::timer();
100✔
640
        $this->benchmark->start('total_execution', $this->startTime);
100✔
641
        $this->benchmark->start('bootstrap');
100✔
642
    }
643

644
    /**
645
     * Sets a Request object to be used for this request.
646
     * Used when running certain tests.
647
     *
648
     * @param CLIRequest|IncomingRequest $request
649
     *
650
     * @return $this
651
     *
652
     * @internal Used for testing purposes only.
653
     * @testTag
654
     */
655
    public function setRequest($request)
656
    {
657
        $this->request = $request;
38✔
658

659
        return $this;
38✔
660
    }
661

662
    /**
663
     * Get our Request object, (either IncomingRequest or CLIRequest).
664
     *
665
     * @return void
666
     */
667
    protected function getRequestObject()
668
    {
669
        if ($this->request instanceof Request) {
100✔
670
            $this->spoofRequestMethod();
40✔
671

672
            return;
40✔
673
        }
674

675
        if ($this->isPhpCli()) {
62✔
676
            Services::createRequest($this->config, true);
×
677
        } else {
678
            Services::createRequest($this->config);
62✔
679
        }
680

681
        $this->request = Services::request();
62✔
682

683
        $this->spoofRequestMethod();
62✔
684
    }
685

686
    /**
687
     * Get our Response object, and set some default values, including
688
     * the HTTP protocol version and a default successful response.
689
     *
690
     * @return void
691
     */
692
    protected function getResponseObject()
693
    {
694
        $this->response = Services::response($this->config);
100✔
695

696
        if ($this->isWeb()) {
100✔
697
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
100✔
698
        }
699

700
        // Assume success until proven otherwise.
701
        $this->response->setStatusCode(200);
100✔
702
    }
703

704
    /**
705
     * Force Secure Site Access? If the config value 'forceGlobalSecureRequests'
706
     * is true, will enforce that all requests to this site are made through
707
     * HTTPS. Will redirect the user to the current page with HTTPS, as well
708
     * as set the HTTP Strict Transport Security header for those browsers
709
     * that support it.
710
     *
711
     * @param int $duration How long the Strict Transport Security
712
     *                      should be enforced for this URL.
713
     *
714
     * @return void
715
     *
716
     * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter.
717
     */
718
    protected function forceSecureAccess($duration = 31_536_000)
719
    {
720
        if ($this->config->forceGlobalSecureRequests !== true) {
721
            return;
722
        }
723

724
        force_https($duration, $this->request, $this->response);
725
    }
726

727
    /**
728
     * Determines if a response has been cached for the given URI.
729
     *
730
     * @return false|ResponseInterface
731
     *
732
     * @throws Exception
733
     *
734
     * @deprecated 4.5.0 PageCache required filter is used. No longer used.
735
     * @deprecated 4.4.2 The parameter $config is deprecated. No longer used.
736
     */
737
    public function displayCache(Cache $config)
738
    {
739
        $cachedResponse = $this->pageCache->get($this->request, $this->response);
740
        if ($cachedResponse instanceof ResponseInterface) {
741
            $this->response = $cachedResponse;
742

743
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
744
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
745
            $this->response->setBody($output);
746

747
            return $this->response;
748
        }
749

750
        return false;
751
    }
752

753
    /**
754
     * Tells the app that the final output should be cached.
755
     *
756
     * @deprecated 4.4.0 Moved to ResponseCache::setTtl(). No longer used.
757
     *
758
     * @return void
759
     */
760
    public static function cache(int $time)
761
    {
762
        static::$cacheTTL = $time;
763
    }
764

765
    /**
766
     * Caches the full response from the current request. Used for
767
     * full-page caching for very high performance.
768
     *
769
     * @return bool
770
     *
771
     * @deprecated 4.4.0 No longer used.
772
     */
773
    public function cachePage(Cache $config)
774
    {
775
        $headers = [];
776

777
        foreach ($this->response->headers() as $header) {
778
            $headers[$header->getName()] = $header->getValueLine();
779
        }
780

781
        return cache()->save($this->generateCacheName($config), serialize(['headers' => $headers, 'output' => $this->output]), static::$cacheTTL);
782
    }
783

784
    /**
785
     * Returns an array with our basic performance stats collected.
786
     */
787
    public function getPerformanceStats(): array
788
    {
789
        // After filter debug toolbar requires 'total_execution'.
790
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
791

792
        return [
×
793
            'startTime' => $this->startTime,
×
794
            'totalTime' => $this->totalTime,
×
795
        ];
×
796
    }
797

798
    /**
799
     * Generates the cache name to use for our full-page caching.
800
     *
801
     * @deprecated 4.4.0 No longer used.
802
     */
803
    protected function generateCacheName(Cache $config): string
804
    {
805
        if ($this->request instanceof CLIRequest) {
806
            return md5($this->request->getPath());
807
        }
808

809
        $uri = clone $this->request->getUri();
810

811
        $query = $config->cacheQueryString
812
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
813
            : '';
814

815
        return md5((string) $uri->setFragment('')->setQuery($query));
816
    }
817

818
    /**
819
     * Replaces the elapsed_time and memory_usage tag.
820
     *
821
     * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used.
822
     */
823
    public function displayPerformanceMetrics(string $output): string
824
    {
825
        return str_replace(
826
            ['{elapsed_time}', '{memory_usage}'],
827
            [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)],
828
            $output,
829
        );
830
    }
831

832
    /**
833
     * Try to Route It - As it sounds like, works with the router to
834
     * match a route against the current URI. If the route is a
835
     * "redirect route", will also handle the redirect.
836
     *
837
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
838
     *                                              of the config file.
839
     *
840
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
841
     *
842
     * @throws RedirectException
843
     */
844
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null, ?string $uri = null)
845
    {
846
        $this->benchmark->start('routing');
98✔
847

848
        if (! $routes instanceof RouteCollectionInterface) {
98✔
849
            $routes = Services::routes()->loadRoutes();
37✔
850
        }
851

852
        // $routes is defined in Config/Routes.php
853
        $this->router = Services::router($routes, $this->request);
98✔
854

855
        // $uri is URL-encoded.
856
        $uri ??= $this->request->getPath();
98✔
857

858
        $this->outputBufferingStart();
98✔
859

860
        $this->controller = $this->router->handle($uri);
98✔
861
        $this->method     = $this->router->methodName();
81✔
862

863
        // If a {locale} segment was matched in the final route,
864
        // then we need to set the correct locale on our Request.
865
        if ($this->router->hasLocale()) {
81✔
866
            $this->request->setLocale($this->router->getLocale());
×
867
        }
868

869
        $this->benchmark->stop('routing');
81✔
870

871
        return $this->router->getFilters();
81✔
872
    }
873

874
    /**
875
     * Determines the path to use for us to try to route to, based
876
     * on the CLI/IncomingRequest path.
877
     *
878
     * @return string
879
     *
880
     * @deprecated 4.5.0 No longer used.
881
     */
882
    protected function determinePath()
883
    {
884
        return $this->request->getPath();
885
    }
886

887
    /**
888
     * Now that everything has been setup, this method attempts to run the
889
     * controller method and make the script go. If it's not able to, will
890
     * show the appropriate Page Not Found error.
891
     *
892
     * @return ResponseInterface|string|null
893
     */
894
    protected function startController()
895
    {
896
        $this->benchmark->start('controller');
81✔
897
        $this->benchmark->start('controller_constructor');
81✔
898

899
        // Is it routed to a Closure? Use instanceof — faster than is_object() + ::class string check.
900
        if ($this->controller instanceof Closure) {
81✔
901
            $controller = $this->controller;
30✔
902

903
            return $controller(...$this->router->params());
30✔
904
        }
905

906
        // No controller specified - we don't know what to do now.
907
        if (! isset($this->controller)) {
51✔
908
            throw PageNotFoundException::forEmptyController();
×
909
        }
910

911
        // Try to autoload the class
912
        if (
913
            ! class_exists($this->controller, true)
51✔
914
            || ($this->method[0] === '_' && $this->method !== '__invoke')
51✔
915
        ) {
916
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
917
        }
918

919
        // Execute route attributes' before() methods
920
        if (config(Routing::class)->useControllerAttributes === true) {
51✔
921
            $this->benchmark->start('route_attributes_before');
50✔
922
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
50✔
923
            $this->benchmark->stop('route_attributes_before');
49✔
924

925
            // If attribute returns a Response, short-circuit
926
            if ($attributeResponse instanceof ResponseInterface) {
49✔
927
                $this->benchmark->stop('controller_constructor');
1✔
928
                $this->benchmark->stop('controller');
1✔
929

930
                return $attributeResponse;
1✔
931
            }
932

933
            // If attribute returns a modified Request, use it
934
            if ($attributeResponse instanceof RequestInterface) {
49✔
935
                $this->request = $attributeResponse;
49✔
936
            }
937
        }
938

939
        return null;
50✔
940
    }
941

942
    /**
943
     * Instantiates the controller class.
944
     *
945
     * @return Controller
946
     */
947
    protected function createController()
948
    {
949
        assert(is_string($this->controller));
950

951
        $class = new $this->controller();
52✔
952
        $class->initController($this->request, $this->response, Services::logger());
52✔
953

954
        $this->benchmark->stop('controller_constructor');
52✔
955

956
        return $class;
52✔
957
    }
958

959
    /**
960
     * Runs the controller, allowing for _remap methods to function.
961
     *
962
     * CI4 supports three types of requests:
963
     *  1. Web: URI segments become parameters, sent to Controllers via Routes,
964
     *      output controlled by Headers to browser
965
     *  2. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments,
966
     *      sent to Controllers via Routes, output varies
967
     *
968
     * @param Controller $class
969
     *
970
     * @return false|ResponseInterface|string|void
971
     */
972
    protected function runController($class)
973
    {
974
        // This is a Web request or PHP CLI request
975
        $params = $this->router->params();
49✔
976

977
        // The controller method param types may not be string.
978
        // So cannot set `declare(strict_types=1)` in this file.
979
        $output = method_exists($class, '_remap')
49✔
980
            ? $class->_remap($this->method, ...$params)
×
981
            : $class->{$this->method}(...$params);
49✔
982

983
        $this->benchmark->stop('controller');
49✔
984

985
        return $output;
49✔
986
    }
987

988
    /**
989
     * Displays a 404 Page Not Found error. If set, will try to
990
     * call the 404Override controller/method that was set in routing config.
991
     *
992
     * @return ResponseInterface|void
993
     */
994
    protected function display404errors(PageNotFoundException $e)
995
    {
996
        $this->response->setStatusCode($e->getCode());
12✔
997

998
        // Is there a 404 Override available?
999
        $override = $this->router->get404Override();
12✔
1000

1001
        if ($override !== null) {
12✔
1002
            $returned = null;
4✔
1003

1004
            if ($override instanceof Closure) {
4✔
1005
                echo $override($e->getMessage());
1✔
1006
            } elseif (is_array($override)) {
3✔
1007
                $this->benchmark->start('controller');
3✔
1008
                $this->benchmark->start('controller_constructor');
3✔
1009

1010
                $this->controller = $override[0];
3✔
1011
                $this->method     = $override[1];
3✔
1012

1013
                $controller = $this->createController();
3✔
1014

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

1017
                $this->benchmark->stop('controller');
3✔
1018
            }
1019

1020
            unset($override);
4✔
1021

1022
            $cacheConfig = config(Cache::class);
4✔
1023
            $this->gatherOutput($cacheConfig, $returned);
4✔
1024

1025
            return $this->response;
4✔
1026
        }
1027

1028
        $this->outputBufferingEnd();
8✔
1029

1030
        // Throws new PageNotFoundException and remove exception message on production.
1031
        throw PageNotFoundException::forPageNotFound(
8✔
1032
            (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : null,
8✔
1033
        );
8✔
1034
    }
1035

1036
    /**
1037
     * Gathers the script output from the buffer, replaces some execution
1038
     * time tag in the output and displays the debug toolbar, if required.
1039
     *
1040
     * @param Cache|null                    $cacheConfig Deprecated. No longer used.
1041
     * @param ResponseInterface|string|null $returned
1042
     *
1043
     * @deprecated $cacheConfig is deprecated.
1044
     *
1045
     * @return void
1046
     */
1047
    protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
1048
    {
1049
        $this->output = $this->outputBufferingEnd();
1050

1051
        if ($returned instanceof DownloadResponse) {
1052
            $this->response = $returned;
1053

1054
            return;
1055
        }
1056
        // If the controller returned a response object,
1057
        // we need to grab the body from it so it can
1058
        // be added to anything else that might have been
1059
        // echoed already.
1060
        // We also need to save the instance locally
1061
        // so that any status code changes, etc, take place.
1062
        if ($returned instanceof ResponseInterface) {
1063
            $this->response = $returned;
1064
            $returned       = $returned->getBody();
1065
        }
1066

1067
        if (is_string($returned)) {
1068
            $this->output .= $returned;
1069
        }
1070

1071
        $this->response->setBody($this->output);
1072
    }
1073

1074
    /**
1075
     * If we have a session object to use, store the current URI
1076
     * as the previous URI. This is called just prior to sending the
1077
     * response to the client, and will make it available next request.
1078
     *
1079
     * This helps provider safer, more reliable previous_url() detection.
1080
     *
1081
     * @param string|URI $uri
1082
     *
1083
     * @return void
1084
     */
1085
    public function storePreviousURL($uri)
1086
    {
1087
        // Ignore CLI requests
1088
        if (! $this->isWeb()) {
77✔
1089
            return;
×
1090
        }
1091
        // Ignore AJAX requests. Use instanceof instead of method_exists() — faster
1092
        // since CLIRequest never has isAJAX(), only IncomingRequest does.
1093
        if ($this->request instanceof IncomingRequest && $this->request->isAJAX()) {
77✔
1094
            return;
×
1095
        }
1096

1097
        // Ignore unroutable responses
1098
        if ($this->response instanceof DownloadResponse || $this->response instanceof RedirectResponse) {
77✔
1099
            return;
×
1100
        }
1101

1102
        // Ignore non-HTML responses
1103
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
77✔
1104
            return;
12✔
1105
        }
1106

1107
        // This is mainly needed during testing...
1108
        if (is_string($uri)) {
65✔
1109
            $uri = new URI($uri);
×
1110
        }
1111

1112
        if (isset($_SESSION)) {
65✔
1113
            session()->set('_ci_previous_url', URI::createURIString(
65✔
1114
                $uri->getScheme(),
65✔
1115
                $uri->getAuthority(),
65✔
1116
                $uri->getPath(),
65✔
1117
                $uri->getQuery(),
65✔
1118
                $uri->getFragment(),
65✔
1119
            ));
65✔
1120
        }
1121
    }
1122

1123
    /**
1124
     * Modifies the Request Object to use a different method if a POST
1125
     * variable called _method is found.
1126
     *
1127
     * @return void
1128
     */
1129
    public function spoofRequestMethod()
1130
    {
1131
        // Only works with POSTED forms
1132
        if ($this->request->getMethod() !== Method::POST) {
100✔
1133
            return;
85✔
1134
        }
1135

1136
        $method = $this->request->getPost('_method');
16✔
1137

1138
        if ($method === null) {
16✔
1139
            return;
14✔
1140
        }
1141

1142
        // Only allows PUT, PATCH, DELETE
1143
        if (in_array($method, self::SPOOFABLE_METHODS, true)) {
2✔
1144
            $this->request = $this->request->setMethod($method);
1✔
1145
        }
1146
    }
1147

1148
    /**
1149
     * Sends the output of this request back to the client.
1150
     * This is what they've been waiting for!
1151
     *
1152
     * @return void
1153
     */
1154
    protected function sendResponse()
1155
    {
1156
        $this->response->send();
56✔
1157
    }
1158

1159
    /**
1160
     * Exits the application, setting the exit code for CLI-based applications
1161
     * that might be watching.
1162
     *
1163
     * Made into a separate method so that it can be mocked during testing
1164
     * without actually stopping script execution.
1165
     *
1166
     * @param int $code
1167
     *
1168
     * @deprecated 4.4.0 No longer Used. Moved to index.php.
1169
     *
1170
     * @return void
1171
     */
1172
    protected function callExit($code)
1173
    {
1174
        exit($code); // @codeCoverageIgnore
1175
    }
1176

1177
    /**
1178
     * Sets the app context.
1179
     *
1180
     * @param 'php-cli'|'web' $context
1181
     *
1182
     * @return $this
1183
     */
1184
    public function setContext(string $context)
1185
    {
1186
        $this->context = $context;
39✔
1187

1188
        return $this;
39✔
1189
    }
1190

1191
    protected function outputBufferingStart(): void
1192
    {
1193
        $this->bufferLevel = ob_get_level();
98✔
1194
        ob_start();
98✔
1195
    }
1196

1197
    protected function outputBufferingEnd(): string
1198
    {
1199
        $buffer = '';
98✔
1200

1201
        while (ob_get_level() > $this->bufferLevel) {
98✔
1202
            $buffer .= ob_get_contents();
98✔
1203
            ob_end_clean();
98✔
1204
        }
1205

1206
        return $buffer;
98✔
1207
    }
1208
}
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

© 2026 Coveralls, Inc