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

codeigniter4 / CodeIgniter4 / 28671502129

03 Jul 2026 04:02PM UTC coverage: 88.715% (+0.01%) from 88.704%
28671502129

Pull #10369

github

web-flow
Merge 635d10eac into 5e7c6efbd
Pull Request #10369: refactor: simplify and optimize handleRequest method in CodeIgniter

18 of 19 new or added lines in 1 file covered. (94.74%)

1 existing line in 1 file now uncovered.

22310 of 25148 relevant lines covered (88.71%)

213.91 hits per line

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

88.46
/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\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.7.4-dev';
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|null
99
     */
100
    protected $response;
101

102
    /**
103
     * Router to use.
104
     *
105
     * @var Router|null
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|null
120
     */
121
    protected $method;
122

123
    /**
124
     * Output handler to use.
125
     *
126
     * @var string|null
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
     * @var 'php-cli'|'web'|null
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);
7,413✔
176
        $this->config    = $config;
7,413✔
177

178
        $this->pageCache = Services::responsecache();
7,413✔
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);
7,413✔
190

191
        // Set default timezone on the server
192
        date_default_timezone_set($this->config->appTimezone);
7,413✔
193
    }
194

195
    /**
196
     * Reset request-specific state for worker mode.
197
     * Clears all request/response data to prepare for the next request.
198
     */
199
    public function resetForWorkerMode(): void
200
    {
201
        $this->request    = null;
1✔
202
        $this->response   = null;
1✔
203
        $this->router     = null;
1✔
204
        $this->controller = null;
1✔
205
        $this->method     = null;
1✔
206
        $this->output     = null;
1✔
207

208
        // Reset timing
209
        $this->startTime = null;
1✔
210
        $this->totalTime = 0;
1✔
211

212
        $this->resetKintForWorkerMode();
1✔
213
    }
214

215
    /**
216
     * Resets Kint request-specific state for worker mode.
217
     */
218
    private function resetKintForWorkerMode(): void
219
    {
220
        if (! CI_DEBUG || ! class_exists(Kint::class, false)) {
1✔
221
            return;
×
222
        }
223

224
        $csp = service('csp');
1✔
225
        if ($csp->enabled()) {
1✔
226
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
1✔
227
            RichRenderer::$css_nonce = $csp->getStyleNonce();
1✔
228
        } else {
229
            RichRenderer::$js_nonce  = null;
×
230
            RichRenderer::$css_nonce = null;
×
231
        }
232

233
        RichRenderer::$needs_pre_render = true;
1✔
234
    }
235

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

254
        helper('kint');
255
    }
256

257
    /**
258
     * @deprecated 4.5.0 Moved to Autoloader.
259
     */
260
    private function autoloadKint(): void
261
    {
262
        // If we have KINT_DIR it means it's already loaded via composer
263
        if (! defined('KINT_DIR')) {
264
            spl_autoload_register(function ($class): void {
265
                $class = explode('\\', $class);
266

267
                if (array_shift($class) !== 'Kint') {
268
                    return;
269
                }
270

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

273
                if (is_file($file)) {
274
                    require_once $file;
275
                }
276
            });
277

278
            require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
279
        }
280
    }
281

282
    /**
283
     * @deprecated 4.5.0 Moved to Autoloader.
284
     */
285
    private function configureKint(): void
286
    {
287
        $config = new KintConfig();
288

289
        Kint::$depth_limit         = $config->maxDepth;
290
        Kint::$display_called_from = $config->displayCalledFrom;
291
        Kint::$expanded            = $config->expanded;
292

293
        if (isset($config->plugins) && is_array($config->plugins)) {
294
            Kint::$plugins = $config->plugins;
295
        }
296

297
        $csp = Services::csp();
298
        if ($csp->enabled()) {
299
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
300
            RichRenderer::$css_nonce = $csp->getStyleNonce();
301
        }
302

303
        RichRenderer::$theme  = $config->richTheme;
304
        RichRenderer::$folder = $config->richFolder;
305

306
        if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) {
307
            RichRenderer::$value_plugins = $config->richObjectPlugins;
308
        }
309
        if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) {
310
            RichRenderer::$tab_plugins = $config->richTabPlugins;
311
        }
312

313
        CliRenderer::$cli_colors         = $config->cliColors;
314
        CliRenderer::$force_utf8         = $config->cliForceUTF8;
315
        CliRenderer::$detect_width       = $config->cliDetectWidth;
316
        CliRenderer::$min_terminal_width = $config->cliMinWidth;
317
    }
318

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

340
        $this->pageCache->setTtl(0);
100✔
341
        $this->bufferLevel = ob_get_level();
100✔
342

343
        $this->startBenchmark();
100✔
344

345
        $this->getRequestObject();
100✔
346
        $this->getResponseObject();
100✔
347

348
        Events::trigger('pre_system');
100✔
349

350
        $this->benchmark->stop('bootstrap');
100✔
351

352
        $this->benchmark->start('required_before_filters');
100✔
353
        // Start up the filters
354
        $filters = Services::filters();
100✔
355
        // Run required before filters
356
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
100✔
357

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

367
                $this->response = $e->getResponse();
6✔
368
            } catch (PageNotFoundException $e) {
12✔
369
                $this->response = $this->display404errors($e);
12✔
370
            } catch (Throwable $e) {
×
371
                $this->outputBufferingEnd();
×
372

373
                throw $e;
×
374
            }
375
        }
376

377
        $this->runRequiredAfterFilters($filters);
92✔
378

379
        // Is there a post-system event?
380
        Events::trigger('post_system');
92✔
381

382
        if ($returnResponse) {
92✔
383
            return $this->response;
36✔
384
        }
385

386
        $this->sendResponse();
56✔
387

388
        return null;
56✔
389
    }
390

391
    /**
392
     * Run required before filters.
393
     */
394
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
395
    {
396
        $possibleResponse = $filters->runRequired('before');
100✔
397
        $this->benchmark->stop('required_before_filters');
100✔
398

399
        // If a ResponseInterface instance is returned then send it back to the client and stop
400
        if ($possibleResponse instanceof ResponseInterface) {
100✔
401
            return $possibleResponse;
3✔
402
        }
403

404
        return null;
99✔
405
    }
406

407
    /**
408
     * Run required after filters.
409
     */
410
    private function runRequiredAfterFilters(Filters $filters): void
411
    {
412
        $filters->setResponse($this->response);
92✔
413

414
        // Run required after filters
415
        $this->benchmark->start('required_after_filters');
92✔
416
        $response = $filters->runRequired('after');
92✔
417
        $this->benchmark->stop('required_after_filters');
92✔
418

419
        if ($response instanceof ResponseInterface) {
92✔
420
            $this->response = $response;
92✔
421
        }
422
    }
423

424
    /**
425
     * Invoked via php-cli command?
426
     */
427
    private function isPhpCli(): bool
428
    {
429
        return $this->context === 'php-cli';
62✔
430
    }
431

432
    /**
433
     * Web access?
434
     */
435
    private function isWeb(): bool
436
    {
437
        return $this->context === 'web';
100✔
438
    }
439

440
    /**
441
     * Disables Controller Filters.
442
     */
443
    public function disableFilters(): void
444
    {
445
        $this->enableFilters = false;
1✔
446
    }
447

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

464
        $routeFilters = $this->tryToRouteIt($routes);
465

466
        // Run "before" filters
467
        $possibleResponse = $this->applyFilters('before', $routeFilters);
468

469
        // If a ResponseInterface instance is returned then send it back to the client and stop
470
        if ($possibleResponse instanceof ResponseInterface) {
471
            $this->outputBufferingEnd();
472

473
            return $possibleResponse;
474
        }
475

476
        $returned = $this->startController();
477

478
        $this->serveResponse($cacheConfig, $returned);
479

480
        // Run "after" filters
481
        $this->applyFilters('after');
482

483
        // Execute controller attributes' after() methods AFTER framework filters
484
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
485
            $this->benchmark->start('route_attributes_after');
486
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
487
            $this->benchmark->stop('route_attributes_after');
488
        }
489

490
        // Skip unnecessary processing for special Responses.
491
        if (
492
            ! $this->response instanceof DownloadResponse
493
            && ! $this->response instanceof RedirectResponse
494
        ) {
495
            // Save our current URI as the previous URI in the session
496
            // for safer, more accurate use with `previous_url()` helper function.
497
            $this->storePreviousURL(current_url(true));
498
        }
499

500
        return $this->response;
501
    }
502

503
    /**
504
     * Runs filters.
505
     *
506
     * @param string                   $position     'before' or 'after'
507
     * @param list<string>|string|null $routeFilters
508
     */
509
    protected function applyFilters(string $position, $routeFilters = null): ?ResponseInterface
510
    {
511
        if (! $this->enableFilters) {
81✔
512
            return null;
1✔
513
        }
514

515
        $uri = $this->request->getPath();
80✔
516

517
        /** @var Filters $filters */
518
        $filters = service('filters');
80✔
519

520
        if ($position === 'before') {
80✔
521
            if ($routeFilters !== null) {
80✔
522
                if (is_string($routeFilters)) {
80✔
NEW
523
                    $routeFilters = [$routeFilters];
×
524
                }
525

526
                $filters->enableFilters($routeFilters, 'before');
80✔
527

528
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property
80✔
529
                if (! $oldFilterOrder) {
80✔
530
                    $routeFilters = array_reverse($routeFilters);
80✔
531
                }
532

533
                $filters->enableFilters($routeFilters, 'after');
80✔
534
            }
535

536
            $this->benchmark->start('before_filters');
80✔
537
            $possibleResponse = $filters->run($uri, 'before');
80✔
538
            $this->benchmark->stop('before_filters');
80✔
539

540
            if ($possibleResponse instanceof ResponseInterface) {
80✔
541
                return $possibleResponse;
1✔
542
            }
543

544
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
79✔
545
                $this->request = $possibleResponse;
79✔
546
            }
547

548
            return null;
79✔
549
        }
550

551
        // after position
552
        $filters->setResponse($this->response);
78✔
553

554
        $this->benchmark->start('after_filters');
78✔
555
        $response = $filters->run($uri, 'after');
78✔
556
        $this->benchmark->stop('after_filters');
78✔
557

558
        if ($response instanceof ResponseInterface) {
78✔
559
            $this->response = $response;
78✔
560
        }
561

562
        return null;
78✔
563
    }
564

565
    /**
566
     * Start the controller, run it, and gather output.
567
     *
568
     * @param mixed $returned
569
     */
570
    protected function serveResponse(Cache $cacheConfig, $returned): void
571
    {
572
        // If startController returned a Response (from an attribute or Closure), use it
573
        if ($returned instanceof ResponseInterface) {
79✔
574
            $this->handleCache($cacheConfig, $returned);
9✔
575

576
            return;
9✔
577
        }
578

579
        // Closure controller has run in startController().
580
        if (! is_callable($this->controller)) {
71✔
581
            $controller = $this->createController();
49✔
582

583
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
49✔
UNCOV
584
                throw PageNotFoundException::forMethodNotFound($this->method);
×
585
            }
586

587
            // Is there a "post_controller_constructor" event?
588
            Events::trigger('post_controller_constructor');
49✔
589

590
            $returned = $this->runController($controller);
49✔
591
        } else {
592
            $this->benchmark->stop('controller_constructor');
22✔
593
            $this->benchmark->stop('controller');
22✔
594
        }
595

596
        // If $returned is a string, then the controller output something,
597
        // probably a view, instead of echoing it directly. Send it along
598
        // so it can be used with the output.
599
        $this->handleCache($cacheConfig, $returned);
71✔
600
    }
601

602
    /**
603
     * Gathers the script output and handles caching.
604
     *
605
     * @param mixed $returned
606
     */
607
    protected function handleCache(Cache $cacheConfig, $returned): void
608
    {
609
        $this->gatherOutput($cacheConfig, $returned);
79✔
610
    }
611

612
    /**
613
     * You can load different configurations depending on your
614
     * current environment. Setting the environment also influences
615
     * things like logging and error reporting.
616
     *
617
     * This can be set to anything, but default usage is:
618
     *
619
     *     development
620
     *     testing
621
     *     production
622
     *
623
     * @codeCoverageIgnore
624
     *
625
     * @return void
626
     *
627
     * @deprecated 4.4.0 No longer used. Moved to index.php and spark.
628
     */
629
    protected function detectEnvironment()
630
    {
631
        // Make sure ENVIRONMENT isn't already set by other means.
632
        if (! defined('ENVIRONMENT')) {
633
            define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
634
        }
635
    }
636

637
    /**
638
     * Load any custom boot files based upon the current environment.
639
     *
640
     * If no boot file exists, we shouldn't continue because something
641
     * is wrong. At the very least, they should have error reporting setup.
642
     *
643
     * @return void
644
     *
645
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
646
     */
647
    protected function bootstrapEnvironment()
648
    {
649
        if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) {
650
            require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
651
        } else {
652
            // @codeCoverageIgnoreStart
653
            header('HTTP/1.1 503 Service Unavailable.', true, 503);
654
            echo 'The application environment is not set correctly.';
655

656
            exit(EXIT_ERROR); // EXIT_ERROR
657
            // @codeCoverageIgnoreEnd
658
        }
659
    }
660

661
    /**
662
     * Start the Benchmark
663
     *
664
     * The timer is used to display total script execution both in the
665
     * debug toolbar, and potentially on the displayed page.
666
     *
667
     * @return void
668
     */
669
    protected function startBenchmark()
670
    {
671
        if ($this->startTime === null) {
100✔
672
            $this->startTime = microtime(true);
×
673
        }
674

675
        $this->benchmark = Services::timer();
100✔
676
        $this->benchmark->start('total_execution', $this->startTime);
100✔
677
        $this->benchmark->start('bootstrap');
100✔
678
    }
679

680
    /**
681
     * Sets a Request object to be used for this request.
682
     * Used when running certain tests.
683
     *
684
     * @param CLIRequest|IncomingRequest $request
685
     *
686
     * @return $this
687
     *
688
     * @internal Used for testing purposes only.
689
     * @testTag
690
     */
691
    public function setRequest($request)
692
    {
693
        $this->request = $request;
38✔
694

695
        return $this;
38✔
696
    }
697

698
    /**
699
     * Get our Request object, (either IncomingRequest or CLIRequest).
700
     *
701
     * @return void
702
     */
703
    protected function getRequestObject()
704
    {
705
        if ($this->request instanceof Request) {
100✔
706
            $this->spoofRequestMethod();
40✔
707

708
            return;
40✔
709
        }
710

711
        if ($this->isPhpCli()) {
62✔
712
            Services::createRequest($this->config, true);
×
713
        } else {
714
            Services::createRequest($this->config);
62✔
715
        }
716

717
        $this->request = service('request');
62✔
718

719
        $this->spoofRequestMethod();
62✔
720
    }
721

722
    /**
723
     * Get our Response object, and set some default values, including
724
     * the HTTP protocol version and a default successful response.
725
     *
726
     * @return void
727
     */
728
    protected function getResponseObject()
729
    {
730
        $this->response = Services::response($this->config);
100✔
731

732
        if ($this->isWeb()) {
100✔
733
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
100✔
734
        }
735

736
        // Assume success until proven otherwise.
737
        $this->response->setStatusCode(200);
100✔
738
    }
739

740
    /**
741
     * Force Secure Site Access? If the config value 'forceGlobalSecureRequests'
742
     * is true, will enforce that all requests to this site are made through
743
     * HTTPS. Will redirect the user to the current page with HTTPS, as well
744
     * as set the HTTP Strict Transport Security header for those browsers
745
     * that support it.
746
     *
747
     * @param int $duration How long the Strict Transport Security
748
     *                      should be enforced for this URL.
749
     *
750
     * @return void
751
     *
752
     * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter.
753
     */
754
    protected function forceSecureAccess($duration = 31_536_000)
755
    {
756
        if ($this->config->forceGlobalSecureRequests !== true) {
757
            return;
758
        }
759

760
        force_https($duration, $this->request, $this->response);
761
    }
762

763
    /**
764
     * Determines if a response has been cached for the given URI.
765
     *
766
     * @return false|ResponseInterface
767
     *
768
     * @throws Exception
769
     *
770
     * @deprecated 4.5.0 PageCache required filter is used. No longer used.
771
     * @deprecated 4.4.2 The parameter $config is deprecated. No longer used.
772
     */
773
    public function displayCache(Cache $config)
774
    {
775
        $cachedResponse = $this->pageCache->get($this->request, $this->response);
776
        if ($cachedResponse instanceof ResponseInterface) {
777
            $this->response = $cachedResponse;
778

779
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
780
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
781
            $this->response->setBody($output);
782

783
            return $this->response;
784
        }
785

786
        return false;
787
    }
788

789
    /**
790
     * Tells the app that the final output should be cached.
791
     *
792
     * @deprecated 4.4.0 Moved to ResponseCache::setTtl(). No longer used.
793
     *
794
     * @return void
795
     */
796
    public static function cache(int $time)
797
    {
798
        static::$cacheTTL = $time;
799
    }
800

801
    /**
802
     * Caches the full response from the current request. Used for
803
     * full-page caching for very high performance.
804
     *
805
     * @return bool
806
     *
807
     * @deprecated 4.4.0 No longer used.
808
     */
809
    public function cachePage(Cache $config)
810
    {
811
        $headers = [];
812

813
        foreach ($this->response->headers() as $header) {
814
            $headers[$header->getName()] = $header->getValueLine();
815
        }
816

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

820
    /**
821
     * Returns an array with our basic performance stats collected.
822
     */
823
    public function getPerformanceStats(): array
824
    {
825
        // After filter debug toolbar requires 'total_execution'.
826
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
827

828
        return [
×
829
            'startTime' => $this->startTime,
×
830
            'totalTime' => $this->totalTime,
×
831
        ];
×
832
    }
833

834
    /**
835
     * Generates the cache name to use for our full-page caching.
836
     *
837
     * @deprecated 4.4.0 No longer used.
838
     */
839
    protected function generateCacheName(Cache $config): string
840
    {
841
        if ($this->request instanceof CLIRequest) {
842
            return md5($this->request->getPath());
843
        }
844

845
        $uri = clone $this->request->getUri();
846

847
        $query = $config->cacheQueryString
848
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
849
            : '';
850

851
        return md5((string) $uri->setFragment('')->setQuery($query));
852
    }
853

854
    /**
855
     * Replaces the elapsed_time and memory_usage tag.
856
     *
857
     * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used.
858
     */
859
    public function displayPerformanceMetrics(string $output): string
860
    {
861
        return str_replace(
862
            ['{elapsed_time}', '{memory_usage}'],
863
            [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)],
864
            $output,
865
        );
866
    }
867

868
    /**
869
     * Try to Route It - As it sounds like, works with the router to
870
     * match a route against the current URI. If the route is a
871
     * "redirect route", will also handle the redirect.
872
     *
873
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
874
     *                                              of the config file.
875
     *
876
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
877
     *
878
     * @throws RedirectException
879
     */
880
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
881
    {
882
        $this->benchmark->start('routing');
98✔
883

884
        if (! $routes instanceof RouteCollectionInterface) {
98✔
885
            $routes = service('routes')->loadRoutes();
37✔
886
        }
887

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

891
        // $uri is URL-encoded.
892
        $uri = $this->request->getPath();
98✔
893

894
        $this->outputBufferingStart();
98✔
895

896
        $this->controller = $this->router->handle($uri);
98✔
897
        $this->method     = $this->router->methodName();
81✔
898

899
        // If a {locale} segment was matched in the final route,
900
        // then we need to set the correct locale on our Request.
901
        if ($this->router->hasLocale()) {
81✔
902
            $this->request->setLocale($this->router->getLocale());
×
903
        }
904

905
        $this->benchmark->stop('routing');
81✔
906

907
        return $this->router->getFilters();
81✔
908
    }
909

910
    /**
911
     * Determines the path to use for us to try to route to, based
912
     * on the CLI/IncomingRequest path.
913
     *
914
     * @return string
915
     *
916
     * @deprecated 4.5.0 No longer used.
917
     */
918
    protected function determinePath()
919
    {
920
        return $this->request->getPath();
921
    }
922

923
    /**
924
     * Now that everything has been setup, this method attempts to run the
925
     * controller method and make the script go. If it's not able to, will
926
     * show the appropriate Page Not Found error.
927
     *
928
     * @return ResponseInterface|string|null
929
     */
930
    protected function startController()
931
    {
932
        $this->benchmark->start('controller');
81✔
933
        $this->benchmark->start('controller_constructor');
81✔
934

935
        // Is it routed to a Closure?
936
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
81✔
937
            $controller = $this->controller;
30✔
938

939
            return $controller(...$this->router->params());
30✔
940
        }
941

942
        // No controller specified - we don't know what to do now.
943
        if (! isset($this->controller)) {
51✔
944
            throw PageNotFoundException::forEmptyController();
×
945
        }
946

947
        // Try to autoload the class
948
        if (
949
            ! class_exists($this->controller, true)
51✔
950
            || ($this->method[0] === '_' && $this->method !== '__invoke')
51✔
951
        ) {
952
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
953
        }
954

955
        // Execute route attributes' before() methods
956
        // This runs after routing/validation but BEFORE expensive controller instantiation
957
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
51✔
958
            $this->benchmark->start('route_attributes_before');
50✔
959
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
50✔
960
            $this->benchmark->stop('route_attributes_before');
49✔
961

962
            // If attribute returns a Response, short-circuit
963
            if ($attributeResponse instanceof ResponseInterface) {
49✔
964
                $this->benchmark->stop('controller_constructor');
1✔
965
                $this->benchmark->stop('controller');
1✔
966

967
                return $attributeResponse;
1✔
968
            }
969

970
            // If attribute returns a modified Request, use it
971
            if ($attributeResponse instanceof RequestInterface) {
49✔
972
                $this->request = $attributeResponse;
49✔
973
            }
974
        }
975

976
        return null;
50✔
977
    }
978

979
    /**
980
     * Instantiates the controller class.
981
     *
982
     * @return Controller
983
     */
984
    protected function createController()
985
    {
986
        assert(is_string($this->controller));
987

988
        $class = new $this->controller();
52✔
989
        $class->initController($this->request, $this->response, Services::logger());
52✔
990

991
        $this->benchmark->stop('controller_constructor');
52✔
992

993
        return $class;
52✔
994
    }
995

996
    /**
997
     * Runs the controller, allowing for _remap methods to function.
998
     *
999
     * CI4 supports three types of requests:
1000
     *  1. Web: URI segments become parameters, sent to Controllers via Routes,
1001
     *      output controlled by Headers to browser
1002
     *  2. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments,
1003
     *      sent to Controllers via Routes, output varies
1004
     *
1005
     * @param Controller $class
1006
     *
1007
     * @return false|ResponseInterface|string|void
1008
     */
1009
    protected function runController($class)
1010
    {
1011
        // This is a Web request or PHP CLI request
1012
        $params = $this->router->params();
49✔
1013

1014
        // The controller method param types may not be string.
1015
        // So cannot set `declare(strict_types=1)` in this file.
1016
        $output = method_exists($class, '_remap')
49✔
1017
            ? $class->_remap($this->method, ...$params)
×
1018
            : $class->{$this->method}(...$params);
49✔
1019

1020
        $this->benchmark->stop('controller');
49✔
1021

1022
        return $output;
49✔
1023
    }
1024

1025
    /**
1026
     * Displays a 404 Page Not Found error. If set, will try to
1027
     * call the 404Override controller/method that was set in routing config.
1028
     *
1029
     * @return ResponseInterface|void
1030
     */
1031
    protected function display404errors(PageNotFoundException $e)
1032
    {
1033
        $this->response->setStatusCode($e->getCode());
12✔
1034

1035
        // Is there a 404 Override available?
1036
        $override = $this->router->get404Override();
12✔
1037

1038
        if ($override !== null) {
12✔
1039
            $returned = null;
4✔
1040

1041
            if ($override instanceof Closure) {
4✔
1042
                echo $override($e->getMessage());
1✔
1043
            } elseif (is_array($override)) {
3✔
1044
                $this->benchmark->start('controller');
3✔
1045
                $this->benchmark->start('controller_constructor');
3✔
1046

1047
                $this->controller = $override[0];
3✔
1048
                $this->method     = $override[1];
3✔
1049

1050
                $controller = $this->createController();
3✔
1051

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

1054
                $this->benchmark->stop('controller');
3✔
1055
            }
1056

1057
            unset($override);
4✔
1058

1059
            $cacheConfig = config(Cache::class);
4✔
1060
            $this->gatherOutput($cacheConfig, $returned);
4✔
1061

1062
            return $this->response;
4✔
1063
        }
1064

1065
        $this->outputBufferingEnd();
8✔
1066

1067
        // Throws new PageNotFoundException and remove exception message on production.
1068
        throw PageNotFoundException::forPageNotFound(
8✔
1069
            (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : null,
8✔
1070
        );
8✔
1071
    }
1072

1073
    /**
1074
     * Gathers the script output from the buffer, replaces some execution
1075
     * time tag in the output and displays the debug toolbar, if required.
1076
     *
1077
     * @param Cache|null                    $cacheConfig Deprecated. No longer used.
1078
     * @param ResponseInterface|string|null $returned
1079
     *
1080
     * @deprecated $cacheConfig is deprecated.
1081
     *
1082
     * @return void
1083
     */
1084
    protected function gatherOutput(?Cache $cacheConfig = null, $returned = null)
1085
    {
1086
        $this->output = $this->outputBufferingEnd();
1087

1088
        if ($returned instanceof DownloadResponse) {
1089
            $this->response = $returned;
1090

1091
            return;
1092
        }
1093
        // If the controller returned a response object,
1094
        // we need to grab the body from it so it can
1095
        // be added to anything else that might have been
1096
        // echoed already.
1097
        // We also need to save the instance locally
1098
        // so that any status code changes, etc, take place.
1099
        if ($returned instanceof ResponseInterface) {
1100
            $this->response = $returned;
1101
            $returned       = $returned->getBody();
1102
        }
1103

1104
        if (is_string($returned)) {
1105
            $this->output .= $returned;
1106
        }
1107

1108
        $this->response->setBody($this->output);
1109
    }
1110

1111
    /**
1112
     * If we have a session object to use, store the current URI
1113
     * as the previous URI. This is called just prior to sending the
1114
     * response to the client, and will make it available next request.
1115
     *
1116
     * This helps provider safer, more reliable previous_url() detection.
1117
     *
1118
     * @param string|URI $uri
1119
     *
1120
     * @return void
1121
     */
1122
    public function storePreviousURL($uri)
1123
    {
1124
        // Ignore CLI requests
1125
        if (! $this->isWeb()) {
77✔
1126
            return;
×
1127
        }
1128
        // Ignore AJAX requests
1129
        if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
77✔
1130
            return;
×
1131
        }
1132

1133
        // Ignore unroutable responses
1134
        if ($this->response instanceof DownloadResponse || $this->response instanceof RedirectResponse) {
77✔
1135
            return;
×
1136
        }
1137

1138
        // Ignore non-HTML responses
1139
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
77✔
1140
            return;
12✔
1141
        }
1142

1143
        // This is mainly needed during testing...
1144
        if (is_string($uri)) {
65✔
1145
            $uri = new URI($uri);
×
1146
        }
1147

1148
        if (isset($_SESSION)) {
65✔
1149
            session()->set('_ci_previous_url', URI::createURIString(
65✔
1150
                $uri->getScheme(),
65✔
1151
                $uri->getAuthority(),
65✔
1152
                $uri->getPath(),
65✔
1153
                $uri->getQuery(),
65✔
1154
                $uri->getFragment(),
65✔
1155
            ));
65✔
1156
        }
1157
    }
1158

1159
    /**
1160
     * Modifies the Request Object to use a different method if a POST
1161
     * variable called _method is found.
1162
     *
1163
     * @return void
1164
     */
1165
    public function spoofRequestMethod()
1166
    {
1167
        // Only works with POSTED forms
1168
        if ($this->request->getMethod() !== Method::POST) {
100✔
1169
            return;
85✔
1170
        }
1171

1172
        $method = $this->request->getPost('_method');
16✔
1173

1174
        if ($method === null) {
16✔
1175
            return;
14✔
1176
        }
1177

1178
        // Only allows PUT, PATCH, DELETE
1179
        if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
2✔
1180
            $this->request = $this->request->setMethod($method);
1✔
1181
        }
1182
    }
1183

1184
    /**
1185
     * Sends the output of this request back to the client.
1186
     * This is what they've been waiting for!
1187
     *
1188
     * @return void
1189
     */
1190
    protected function sendResponse()
1191
    {
1192
        $this->response->send();
56✔
1193
    }
1194

1195
    /**
1196
     * Exits the application, setting the exit code for CLI-based applications
1197
     * that might be watching.
1198
     *
1199
     * Made into a separate method so that it can be mocked during testing
1200
     * without actually stopping script execution.
1201
     *
1202
     * @param int $code
1203
     *
1204
     * @deprecated 4.4.0 No longer Used. Moved to index.php.
1205
     *
1206
     * @return void
1207
     */
1208
    protected function callExit($code)
1209
    {
1210
        exit($code); // @codeCoverageIgnore
1211
    }
1212

1213
    /**
1214
     * Sets the app context.
1215
     *
1216
     * @param 'php-cli'|'web' $context
1217
     *
1218
     * @return $this
1219
     */
1220
    public function setContext(string $context)
1221
    {
1222
        $this->context = $context;
39✔
1223

1224
        return $this;
39✔
1225
    }
1226

1227
    protected function outputBufferingStart(): void
1228
    {
1229
        $this->bufferLevel = ob_get_level();
98✔
1230
        ob_start();
98✔
1231
    }
1232

1233
    protected function outputBufferingEnd(): string
1234
    {
1235
        $buffer = '';
98✔
1236

1237
        while (ob_get_level() > $this->bufferLevel) {
98✔
1238
            $buffer .= ob_get_contents();
98✔
1239
            ob_end_clean();
98✔
1240
        }
1241

1242
        return $buffer;
98✔
1243
    }
1244
}
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