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

codeigniter4 / CodeIgniter4 / 29359191853

14 Jul 2026 06:45PM UTC coverage: 88.71%. Remained the same
29359191853

Pull #10369

github

web-flow
Merge a84be7231 into b63543460
Pull Request #10369: refactor: simplify and optimize handleRequest method in CodeIgniter

22322 of 25163 relevant lines covered (88.71%)

213.88 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
        } elseif (! $this->controller instanceof Closure) {
511
            $controller = $this->createController();
512

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

517
            // Is there a "post_controller_constructor" event?
518
            Events::trigger('post_controller_constructor');
519

520
            $returned = $this->runController($controller);
521
        } else {
522
            $this->benchmark->stop('controller_constructor');
523
            $this->benchmark->stop('controller');
524
        }
525

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

533
        if ($this->enableFilters) {
534
            /** @var Filters $filters */
535
            $filters = service('filters');
536
            $filters->setResponse($this->response);
537

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

543
            if ($response instanceof ResponseInterface) {
544
                $this->response = $response;
545
            }
546
        }
547

548
        // Execute controller attributes' after() methods AFTER framework filters.
549
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
550
            $this->benchmark->start('route_attributes_after');
551
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
552
            $this->benchmark->stop('route_attributes_after');
553
        }
554

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

565
        return $this->response;
566
    }
567

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

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

612
            exit(EXIT_ERROR); // EXIT_ERROR
613
            // @codeCoverageIgnoreEnd
614
        }
615
    }
616

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

631
        $this->benchmark = Services::timer();
100✔
632
        $this->benchmark->start('total_execution', $this->startTime);
100✔
633
        $this->benchmark->start('bootstrap');
100✔
634
    }
635

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

651
        return $this;
38✔
652
    }
653

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

664
            return;
40✔
665
        }
666

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

673
        $this->request = service('request');
62✔
674

675
        $this->spoofRequestMethod();
62✔
676
    }
677

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

688
        if ($this->isWeb()) {
100✔
689
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
100✔
690
        }
691

692
        // Assume success until proven otherwise.
693
        $this->response->setStatusCode(200);
100✔
694
    }
695

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

716
        force_https($duration, $this->request, $this->response);
717
    }
718

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

735
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
736
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
737
            $this->response->setBody($output);
738

739
            return $this->response;
740
        }
741

742
        return false;
743
    }
744

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

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

769
        foreach ($this->response->headers() as $header) {
770
            $headers[$header->getName()] = $header->getValueLine();
771
        }
772

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

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

784
        return [
×
785
            'startTime' => $this->startTime,
×
786
            'totalTime' => $this->totalTime,
×
787
        ];
×
788
    }
789

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

801
        $uri = clone $this->request->getUri();
802

803
        $query = $config->cacheQueryString
804
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
805
            : '';
806

807
        return md5((string) $uri->setFragment('')->setQuery($query));
808
    }
809

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

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

840
        if (! $routes instanceof RouteCollectionInterface) {
98✔
841
            $routes = service('routes')->loadRoutes();
37✔
842
        }
843

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

847
        // $uri is URL-encoded.
848
        $uri = $this->request->getPath();
98✔
849

850
        $this->outputBufferingStart();
98✔
851

852
        $this->controller = $this->router->handle($uri);
98✔
853
        $this->method     = $this->router->methodName();
81✔
854

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

861
        $this->benchmark->stop('routing');
81✔
862

863
        return $this->router->getFilters();
81✔
864
    }
865

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

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

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

895
            return $controller(...$this->router->params());
30✔
896
        }
897

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

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

911
        // Execute route attributes' before() methods
912
        if ((config(Routing::class)->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
51✔
913
            $this->benchmark->start('route_attributes_before');
50✔
914
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
50✔
915
            $this->benchmark->stop('route_attributes_before');
49✔
916

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

922
                return $attributeResponse;
1✔
923
            }
924

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

931
        return null;
50✔
932
    }
933

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

943
        $class = new $this->controller();
52✔
944
        $class->initController($this->request, $this->response, Services::logger());
52✔
945

946
        $this->benchmark->stop('controller_constructor');
52✔
947

948
        return $class;
52✔
949
    }
950

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

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

975
        $this->benchmark->stop('controller');
49✔
976

977
        return $output;
49✔
978
    }
979

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

990
        // Is there a 404 Override available?
991
        $override = $this->router->get404Override();
12✔
992

993
        if ($override !== null) {
12✔
994
            $returned = null;
4✔
995

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

1002
                $this->controller = $override[0];
3✔
1003
                $this->method     = $override[1];
3✔
1004

1005
                $controller = $this->createController();
3✔
1006

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

1009
                $this->benchmark->stop('controller');
3✔
1010
            }
1011

1012
            unset($override);
4✔
1013

1014
            $cacheConfig = config(Cache::class);
4✔
1015
            $this->gatherOutput($cacheConfig, $returned);
4✔
1016

1017
            return $this->response;
4✔
1018
        }
1019

1020
        $this->outputBufferingEnd();
8✔
1021

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

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

1043
        if ($returned instanceof DownloadResponse) {
1044
            $this->response = $returned;
1045

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

1059
        if (is_string($returned)) {
1060
            $this->output .= $returned;
1061
        }
1062

1063
        $this->response->setBody($this->output);
1064
    }
1065

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

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

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

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

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

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

1127
        $method = $this->request->getPost('_method');
16✔
1128

1129
        if ($method === null) {
16✔
1130
            return;
14✔
1131
        }
1132

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

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

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

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

1179
        return $this;
39✔
1180
    }
1181

1182
    protected function outputBufferingStart(): void
1183
    {
1184
        $this->bufferLevel = ob_get_level();
98✔
1185
        ob_start();
98✔
1186
    }
1187

1188
    protected function outputBufferingEnd(): string
1189
    {
1190
        $buffer = '';
98✔
1191

1192
        while (ob_get_level() > $this->bufferLevel) {
98✔
1193
            $buffer .= ob_get_contents();
98✔
1194
            ob_end_clean();
98✔
1195
        }
1196

1197
        return $buffer;
98✔
1198
    }
1199
}
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