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

codeigniter4 / CodeIgniter4 / 29119400030

10 Jul 2026 07:51PM UTC coverage: 88.71%. First build
29119400030

Pull #10369

github

web-flow
Merge f9effd83f into 0c101bd61
Pull Request #10369: refactor: simplify and optimize handleRequest method in CodeIgniter

1 of 2 new or added lines in 1 file covered. (50.0%)

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
     * App startup time.
63
     *
64
     * @var float|null
65
     */
66
    protected $startTime;
67

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

179
        $this->pageCache = Services::responsecache();
7,427✔
180
    }
181

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

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

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

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

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

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

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

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

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

255
        helper('kint');
256
    }
257

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

374
                throw $e;
×
375
            }
376
        }
377

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

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

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

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

389
        return null;
56✔
390
    }
391

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

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

405
        return null;
99✔
406
    }
407

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

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

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

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

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

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

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

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

467
        // $uri is URL-encoded.
468
        $uri = $this->request->getPath();
469

470
        if ($this->enableFilters) {
471
            /** @var Filters $filters */
472
            $filters = service('filters');
473

474
            // If any filters were specified within the routes file,
475
            // we need to ensure it's active for the current request
476
            if ($routeFilters !== null) {
477
                $filters->enableFilters($routeFilters, 'before');
478

479
                if (! config(Feature::class)->oldFilterOrder) {
480
                    $routeFilters = array_reverse($routeFilters);
481
                }
482

483
                $filters->enableFilters($routeFilters, 'after');
484
            }
485

486
            // Run "before" filters
487
            $this->benchmark->start('before_filters');
488
            $possibleResponse = $filters->run($uri, 'before');
489
            $this->benchmark->stop('before_filters');
490

491
            // If a ResponseInterface instance is returned then send it back to the client and stop
492
            if ($possibleResponse instanceof ResponseInterface) {
493
                $this->outputBufferingEnd();
494

495
                return $possibleResponse;
496
            }
497

498
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
499
                $this->request = $possibleResponse;
500
            }
501
        }
502

503
        $returned = $this->startController();
504
        $gathered = false;
505

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

516
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
517
                throw PageNotFoundException::forMethodNotFound($this->method);
518
            }
519

520
            // Is there a "post_controller_constructor" event?
521
            Events::trigger('post_controller_constructor');
522

523
            $returned = $this->runController($controller);
524
        } else {
525
            $this->benchmark->stop('controller_constructor');
526
            $this->benchmark->stop('controller');
527
        }
528

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

536
        if ($this->enableFilters) {
537
            /** @var Filters $filters */
538
            $filters = service('filters');
539
            $filters->setResponse($this->response);
540

541
            // Run "after" filters
542
            $this->benchmark->start('after_filters');
543
            $response = $filters->run($uri, 'after');
544
            $this->benchmark->stop('after_filters');
545

546
            if ($response instanceof ResponseInterface) {
547
                $this->response = $response;
548
            }
549
        }
550

551
        // Execute controller attributes' after() methods AFTER framework filters.
552
        if (config(Routing::class)->useControllerAttributes === true) {
553
            $this->benchmark->start('route_attributes_after');
554
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
555
            $this->benchmark->stop('route_attributes_after');
556
        }
557

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

568
        return $this->response;
569
    }
570

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

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

615
            exit(EXIT_ERROR); // EXIT_ERROR
616
            // @codeCoverageIgnoreEnd
617
        }
618
    }
619

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

634
        $this->benchmark = Services::timer();
100✔
635
        $this->benchmark->start('total_execution', $this->startTime);
100✔
636
        $this->benchmark->start('bootstrap');
100✔
637
    }
638

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

654
        return $this;
38✔
655
    }
656

657
    /**
658
     * Get our Request object, (either IncomingRequest or CLIRequest).
659
     *
660
     * @return void
661
     */
662
    protected function getRequestObject()
663
    {
664
        if ($this->request instanceof Request) {
100✔
665
            $this->spoofRequestMethod();
40✔
666

667
            return;
40✔
668
        }
669

670
        if ($this->isPhpCli()) {
62✔
671
            Services::createRequest($this->config, true);
×
672
        } else {
673
            Services::createRequest($this->config);
62✔
674
        }
675

676
        $this->request = service('request');
62✔
677

678
        $this->spoofRequestMethod();
62✔
679
    }
680

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

691
        if ($this->isWeb()) {
100✔
692
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
100✔
693
        }
694

695
        // Assume success until proven otherwise.
696
        $this->response->setStatusCode(200);
100✔
697
    }
698

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

719
        force_https($duration, $this->request, $this->response);
720
    }
721

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

738
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
739
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
740
            $this->response->setBody($output);
741

742
            return $this->response;
743
        }
744

745
        return false;
746
    }
747

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

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

772
        foreach ($this->response->headers() as $header) {
773
            $headers[$header->getName()] = $header->getValueLine();
774
        }
775

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

779
    /**
780
     * Returns an array with our basic performance stats collected.
781
     */
782
    public function getPerformanceStats(): array
783
    {
784
        // After filter debug toolbar requires 'total_execution'.
785
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
786

787
        return [
×
788
            'startTime' => $this->startTime,
×
789
            'totalTime' => $this->totalTime,
×
790
        ];
×
791
    }
792

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

804
        $uri = clone $this->request->getUri();
805

806
        $query = $config->cacheQueryString
807
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
808
            : '';
809

810
        return md5((string) $uri->setFragment('')->setQuery($query));
811
    }
812

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

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

843
        if (! $routes instanceof RouteCollectionInterface) {
98✔
844
            $routes = service('routes')->loadRoutes();
37✔
845
        }
846

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

850
        // $uri is URL-encoded.
851
        $uri = $this->request->getPath();
98✔
852

853
        $this->outputBufferingStart();
98✔
854

855
        $this->controller = $this->router->handle($uri);
98✔
856
        $this->method     = $this->router->methodName();
81✔
857

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

864
        $this->benchmark->stop('routing');
81✔
865

866
        return $this->router->getFilters();
81✔
867
    }
868

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

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

894
        // Is it routed to a Closure?
895
        if ($this->controller instanceof Closure) {
81✔
896
            $controller = $this->controller;
30✔
897

898
            return $controller(...$this->router->params());
30✔
899
        }
900

901
        // No controller specified - we don't know what to do now.
902
        if (! isset($this->controller)) {
51✔
903
            throw PageNotFoundException::forEmptyController();
×
904
        }
905

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

914
        // Execute route attributes' before() methods
915
        if (config(Routing::class)->useControllerAttributes === true) {
51✔
916
            $this->benchmark->start('route_attributes_before');
50✔
917
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
50✔
918
            $this->benchmark->stop('route_attributes_before');
49✔
919

920
            // If attribute returns a Response, short-circuit
921
            if ($attributeResponse instanceof ResponseInterface) {
49✔
922
                $this->benchmark->stop('controller_constructor');
1✔
923
                $this->benchmark->stop('controller');
1✔
924

925
                return $attributeResponse;
1✔
926
            }
927

928
            // If attribute returns a modified Request, use it
929
            if ($attributeResponse instanceof RequestInterface) {
49✔
930
                $this->request = $attributeResponse;
49✔
931
            }
932
        }
933

934
        return null;
50✔
935
    }
936

937
    /**
938
     * Instantiates the controller class.
939
     *
940
     * @return Controller
941
     */
942
    protected function createController()
943
    {
944
        assert(is_string($this->controller));
945

946
        $class = new $this->controller();
52✔
947
        $class->initController($this->request, $this->response, Services::logger());
52✔
948

949
        $this->benchmark->stop('controller_constructor');
52✔
950

951
        return $class;
52✔
952
    }
953

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

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

978
        $this->benchmark->stop('controller');
49✔
979

980
        return $output;
49✔
981
    }
982

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

993
        // Is there a 404 Override available?
994
        $override = $this->router->get404Override();
12✔
995

996
        if ($override !== null) {
12✔
997
            $returned = null;
4✔
998

999
            if ($override instanceof Closure) {
4✔
1000
                echo $override($e->getMessage());
1✔
1001
            } elseif (is_array($override)) {
3✔
1002
                $this->benchmark->start('controller');
3✔
1003
                $this->benchmark->start('controller_constructor');
3✔
1004

1005
                $this->controller = $override[0];
3✔
1006
                $this->method     = $override[1];
3✔
1007

1008
                $controller = $this->createController();
3✔
1009

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

1012
                $this->benchmark->stop('controller');
3✔
1013
            }
1014

1015
            unset($override);
4✔
1016

1017
            $cacheConfig = config(Cache::class);
4✔
1018
            $this->gatherOutput($cacheConfig, $returned);
4✔
1019

1020
            return $this->response;
4✔
1021
        }
1022

1023
        $this->outputBufferingEnd();
8✔
1024

1025
        // Throws new PageNotFoundException and remove exception message on production.
1026
        throw PageNotFoundException::forPageNotFound(
8✔
1027
            (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : null,
8✔
1028
        );
8✔
1029
    }
1030

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

1046
        if ($returned instanceof DownloadResponse) {
1047
            $this->response = $returned;
1048

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

1062
        if (is_string($returned)) {
1063
            $this->output .= $returned;
1064
        }
1065

1066
        $this->response->setBody($this->output);
1067
    }
1068

1069
    /**
1070
     * If we have a session object to use, store the current URI
1071
     * as the previous URI. This is called just prior to sending the
1072
     * response to the client, and will make it available next request.
1073
     *
1074
     * This helps provider safer, more reliable previous_url() detection.
1075
     *
1076
     * @param string|URI $uri
1077
     *
1078
     * @return void
1079
     */
1080
    public function storePreviousURL($uri)
1081
    {
1082
        // Ignore CLI requests
1083
        if (! $this->isWeb()) {
77✔
NEW
1084
            return;
×
1085
        }
1086
        // Ignore AJAX requests
1087
        if ($this->request instanceof IncomingRequest && $this->request->isAJAX()) {
77✔
1088
            return;
×
1089
        }
1090

1091
        // Ignore unroutable responses
1092
        if ($this->response instanceof DownloadResponse || $this->response instanceof RedirectResponse) {
77✔
1093
            return;
×
1094
        }
1095

1096
        // Ignore non-HTML responses
1097
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
77✔
1098
            return;
12✔
1099
        }
1100

1101
        // This is mainly needed during testing...
1102
        if (is_string($uri)) {
65✔
1103
            $uri = new URI($uri);
×
1104
        }
1105

1106
        if (isset($_SESSION)) {
65✔
1107
            session()->set('_ci_previous_url', URI::createURIString(
65✔
1108
                $uri->getScheme(),
65✔
1109
                $uri->getAuthority(),
65✔
1110
                $uri->getPath(),
65✔
1111
                $uri->getQuery(),
65✔
1112
                $uri->getFragment(),
65✔
1113
            ));
65✔
1114
        }
1115
    }
1116

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

1130
        $method = $this->request->getPost('_method');
16✔
1131

1132
        if ($method === null) {
16✔
1133
            return;
14✔
1134
        }
1135

1136
        // Only allows PUT, PATCH, DELETE
1137
        if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
2✔
1138
            $this->request = $this->request->setMethod($method);
1✔
1139
        }
1140
    }
1141

1142
    /**
1143
     * Sends the output of this request back to the client.
1144
     * This is what they've been waiting for!
1145
     *
1146
     * @return void
1147
     */
1148
    protected function sendResponse()
1149
    {
1150
        $this->response->send();
56✔
1151
    }
1152

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

1171
    /**
1172
     * Sets the app context.
1173
     *
1174
     * @param 'php-cli'|'web' $context
1175
     *
1176
     * @return $this
1177
     */
1178
    public function setContext(string $context)
1179
    {
1180
        $this->context = $context;
39✔
1181

1182
        return $this;
39✔
1183
    }
1184

1185
    protected function outputBufferingStart(): void
1186
    {
1187
        $this->bufferLevel = ob_get_level();
98✔
1188
        ob_start();
98✔
1189
    }
1190

1191
    protected function outputBufferingEnd(): string
1192
    {
1193
        $buffer = '';
98✔
1194

1195
        while (ob_get_level() > $this->bufferLevel) {
98✔
1196
            $buffer .= ob_get_contents();
98✔
1197
            ob_end_clean();
98✔
1198
        }
1199

1200
        return $buffer;
98✔
1201
    }
1202
}
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