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

codeigniter4 / CodeIgniter4 / 18295054237

06 Oct 2025 09:31PM UTC coverage: 84.361% (+0.04%) from 84.325%
18295054237

Pull #9745

github

web-flow
Merge 399b3b300 into 3473349b6
Pull Request #9745: feat(app): Added controller attributes

146 of 162 new or added lines in 5 files covered. (90.12%)

53 existing lines in 1 file now uncovered.

21238 of 25175 relevant lines covered (84.36%)

195.32 hits per line

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

69.06
/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\ResponsableInterface;
29
use CodeIgniter\HTTP\ResponseInterface;
30
use CodeIgniter\HTTP\URI;
31
use CodeIgniter\Router\RouteCollectionInterface;
32
use CodeIgniter\Router\Router;
33
use Config\App;
34
use Config\Cache;
35
use Config\Feature;
36
use Config\Kint as KintConfig;
37
use Config\Services;
38
use Exception;
39
use Kint;
40
use Kint\Renderer\CliRenderer;
41
use Kint\Renderer\RichRenderer;
42
use Locale;
43
use Throwable;
44

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

194
    /**
195
     * Initializes Kint
196
     *
197
     * @return void
198
     *
199
     * @deprecated 4.5.0 Moved to Autoloader.
200
     */
201
    protected function initializeKint()
202
    {
UNCOV
203
        if (CI_DEBUG) {
×
204
            $this->autoloadKint();
×
205
            $this->configureKint();
×
206
        } elseif (class_exists(Kint::class)) {
×
207
            // In case that Kint is already loaded via Composer.
UNCOV
208
            Kint::$enabled_mode = false;
×
209
            // @codeCoverageIgnore
210
        }
211

UNCOV
212
        helper('kint');
×
213
    }
214

215
    /**
216
     * @deprecated 4.5.0 Moved to Autoloader.
217
     */
218
    private function autoloadKint(): void
219
    {
220
        // If we have KINT_DIR it means it's already loaded via composer
UNCOV
221
        if (! defined('KINT_DIR')) {
×
222
            spl_autoload_register(function ($class): void {
×
223
                $class = explode('\\', $class);
×
224

UNCOV
225
                if (array_shift($class) !== 'Kint') {
×
226
                    return;
×
227
                }
228

UNCOV
229
                $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';
×
230

UNCOV
231
                if (is_file($file)) {
×
232
                    require_once $file;
×
233
                }
UNCOV
234
            });
×
235

UNCOV
236
            require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
×
237
        }
238
    }
239

240
    /**
241
     * @deprecated 4.5.0 Moved to Autoloader.
242
     */
243
    private function configureKint(): void
244
    {
UNCOV
245
        $config = new KintConfig();
×
246

UNCOV
247
        Kint::$depth_limit         = $config->maxDepth;
×
248
        Kint::$display_called_from = $config->displayCalledFrom;
×
249
        Kint::$expanded            = $config->expanded;
×
250

UNCOV
251
        if (isset($config->plugins) && is_array($config->plugins)) {
×
252
            Kint::$plugins = $config->plugins;
×
253
        }
254

UNCOV
255
        $csp = Services::csp();
×
256
        if ($csp->enabled()) {
×
257
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
×
258
            RichRenderer::$css_nonce = $csp->getStyleNonce();
×
259
        }
260

UNCOV
261
        RichRenderer::$theme  = $config->richTheme;
×
262
        RichRenderer::$folder = $config->richFolder;
×
263

UNCOV
264
        if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) {
×
265
            RichRenderer::$value_plugins = $config->richObjectPlugins;
×
266
        }
UNCOV
267
        if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) {
×
268
            RichRenderer::$tab_plugins = $config->richTabPlugins;
×
269
        }
270

UNCOV
271
        CliRenderer::$cli_colors         = $config->cliColors;
×
272
        CliRenderer::$force_utf8         = $config->cliForceUTF8;
×
273
        CliRenderer::$detect_width       = $config->cliDetectWidth;
×
274
        CliRenderer::$min_terminal_width = $config->cliMinWidth;
×
275
    }
276

277
    /**
278
     * Launch the application!
279
     *
280
     * This is "the loop" if you will. The main entry point into the script
281
     * that gets the required class instances, fires off the filters,
282
     * tries to route the response, loads the controller and generally
283
     * makes all the pieces work together.
284
     *
285
     * @param bool $returnResponse Used for testing purposes only.
286
     *
287
     * @return ResponseInterface|null
288
     */
289
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
290
    {
291
        if ($this->context === null) {
98✔
UNCOV
292
            throw new LogicException(
×
293
                'Context must be set before run() is called. If you are upgrading from 4.1.x, '
×
294
                . 'you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.',
×
295
            );
×
296
        }
297

298
        $this->pageCache->setTtl(0);
98✔
299
        $this->bufferLevel = ob_get_level();
98✔
300

301
        $this->startBenchmark();
98✔
302

303
        $this->getRequestObject();
98✔
304
        $this->getResponseObject();
98✔
305

306
        Events::trigger('pre_system');
98✔
307

308
        $this->benchmark->stop('bootstrap');
98✔
309

310
        $this->benchmark->start('required_before_filters');
98✔
311
        // Start up the filters
312
        $filters = Services::filters();
98✔
313
        // Run required before filters
314
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
98✔
315

316
        // If a ResponseInterface instance is returned then send it back to the client and stop
317
        if ($possibleResponse instanceof ResponseInterface) {
98✔
318
            $this->response = $possibleResponse;
3✔
319
        } else {
320
            try {
321
                $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
97✔
322
            } catch (ResponsableInterface $e) {
18✔
323
                $this->outputBufferingEnd();
6✔
324

325
                $this->response = $e->getResponse();
6✔
326
            } catch (PageNotFoundException $e) {
12✔
327
                $this->response = $this->display404errors($e);
12✔
UNCOV
328
            } catch (Throwable $e) {
×
329
                $this->outputBufferingEnd();
×
330

UNCOV
331
                throw $e;
×
332
            }
333
        }
334

335
        $this->runRequiredAfterFilters($filters);
90✔
336

337
        // Is there a post-system event?
338
        Events::trigger('post_system');
90✔
339

340
        if ($returnResponse) {
90✔
341
            return $this->response;
36✔
342
        }
343

344
        $this->sendResponse();
54✔
345

346
        return null;
54✔
347
    }
348

349
    /**
350
     * Run required before filters.
351
     */
352
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
353
    {
354
        $possibleResponse = $filters->runRequired('before');
98✔
355
        $this->benchmark->stop('required_before_filters');
98✔
356

357
        // If a ResponseInterface instance is returned then send it back to the client and stop
358
        if ($possibleResponse instanceof ResponseInterface) {
98✔
359
            return $possibleResponse;
3✔
360
        }
361

362
        return null;
97✔
363
    }
364

365
    /**
366
     * Run required after filters.
367
     */
368
    private function runRequiredAfterFilters(Filters $filters): void
369
    {
370
        $filters->setResponse($this->response);
90✔
371

372
        // Run required after filters
373
        $this->benchmark->start('required_after_filters');
90✔
374
        $response = $filters->runRequired('after');
90✔
375
        $this->benchmark->stop('required_after_filters');
90✔
376

377
        if ($response instanceof ResponseInterface) {
90✔
378
            $this->response = $response;
90✔
379
        }
380
    }
381

382
    /**
383
     * Invoked via php-cli command?
384
     */
385
    private function isPhpCli(): bool
386
    {
387
        return $this->context === 'php-cli';
60✔
388
    }
389

390
    /**
391
     * Web access?
392
     */
393
    private function isWeb(): bool
394
    {
395
        return $this->context === 'web';
98✔
396
    }
397

398
    /**
399
     * Disables Controller Filters.
400
     */
401
    public function disableFilters(): void
402
    {
403
        $this->enableFilters = false;
1✔
404
    }
405

406
    /**
407
     * Handles the main request logic and fires the controller.
408
     *
409
     * @return ResponseInterface
410
     *
411
     * @throws PageNotFoundException
412
     * @throws RedirectException
413
     *
414
     * @deprecated $returnResponse is deprecated.
415
     */
416
    protected function handleRequest(?RouteCollectionInterface $routes, Cache $cacheConfig, bool $returnResponse = false)
417
    {
418
        if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
97✔
419
            return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
1✔
420
        }
421

422
        $routeFilters = $this->tryToRouteIt($routes);
96✔
423

424
        // $uri is URL-encoded.
425
        $uri = $this->request->getPath();
79✔
426

427
        if ($this->enableFilters) {
79✔
428
            /** @var Filters $filters */
429
            $filters = service('filters');
78✔
430

431
            // If any filters were specified within the routes file,
432
            // we need to ensure it's active for the current request
433
            if ($routeFilters !== null) {
78✔
434
                $filters->enableFilters($routeFilters, 'before');
78✔
435

436
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
78✔
437
                if (! $oldFilterOrder) {
78✔
438
                    $routeFilters = array_reverse($routeFilters);
78✔
439
                }
440

441
                $filters->enableFilters($routeFilters, 'after');
78✔
442
            }
443

444
            // Run "before" filters
445
            $this->benchmark->start('before_filters');
78✔
446
            $possibleResponse = $filters->run($uri, 'before');
78✔
447
            $this->benchmark->stop('before_filters');
78✔
448

449
            // If a ResponseInterface instance is returned then send it back to the client and stop
450
            if ($possibleResponse instanceof ResponseInterface) {
78✔
451
                $this->outputBufferingEnd();
1✔
452

453
                return $possibleResponse;
1✔
454
            }
455

456
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
77✔
457
                $this->request = $possibleResponse;
77✔
458
            }
459
        }
460

461
        $returned = $this->startController();
78✔
462

463
        // If startController returned a Response (from an attribute or Closure), use it
464
        if ($returned instanceof ResponseInterface) {
77✔
465
            $this->gatherOutput($cacheConfig, $returned);
8✔
466
        }
467
        // Closure controller has run in startController().
468
        elseif (! is_callable($this->controller)) {
70✔
469
            $controller = $this->createController();
48✔
470

471
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
48✔
UNCOV
472
                throw PageNotFoundException::forMethodNotFound($this->method);
×
473
            }
474

475
            // Is there a "post_controller_constructor" event?
476
            Events::trigger('post_controller_constructor');
48✔
477

478
            $returned = $this->runController($controller);
48✔
479
        } else {
480
            $this->benchmark->stop('controller_constructor');
22✔
481
            $this->benchmark->stop('controller');
22✔
482
        }
483

484
        // If $returned is a string, then the controller output something,
485
        // probably a view, instead of echoing it directly. Send it along
486
        // so it can be used with the output.
487
        $this->gatherOutput($cacheConfig, $returned);
77✔
488

489
        if ($this->enableFilters) {
77✔
490
            /** @var Filters $filters */
491
            $filters = service('filters');
76✔
492
            $filters->setResponse($this->response);
76✔
493

494
            // Run "after" filters
495
            $this->benchmark->start('after_filters');
76✔
496
            $response = $filters->run($uri, 'after');
76✔
497
            $this->benchmark->stop('after_filters');
76✔
498

499
            if ($response instanceof ResponseInterface) {
76✔
500
                $this->response = $response;
76✔
501
            }
502
        }
503

504
        // Execute controller attributes' after() methods AFTER framework filters
505
        if (config('routing')->useControllerAttributes === true) {
77✔
506
            $this->benchmark->start('route_attributes_after');
76✔
507
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
76✔
508
            $this->benchmark->stop('route_attributes_after');
76✔
509
        }
510

511
        // Skip unnecessary processing for special Responses.
512
        if (
513
            ! $this->response instanceof DownloadResponse
77✔
514
            && ! $this->response instanceof RedirectResponse
77✔
515
        ) {
516
            // Save our current URI as the previous URI in the session
517
            // for safer, more accurate use with `previous_url()` helper function.
518
            $this->storePreviousURL(current_url(true));
75✔
519
        }
520

521
        unset($uri);
77✔
522

523
        return $this->response;
77✔
524
    }
525

526
    /**
527
     * You can load different configurations depending on your
528
     * current environment. Setting the environment also influences
529
     * things like logging and error reporting.
530
     *
531
     * This can be set to anything, but default usage is:
532
     *
533
     *     development
534
     *     testing
535
     *     production
536
     *
537
     * @codeCoverageIgnore
538
     *
539
     * @return void
540
     *
541
     * @deprecated 4.4.0 No longer used. Moved to index.php and spark.
542
     */
543
    protected function detectEnvironment()
544
    {
545
        // Make sure ENVIRONMENT isn't already set by other means.
UNCOV
546
        if (! defined('ENVIRONMENT')) {
×
547
            define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
×
548
        }
549
    }
550

551
    /**
552
     * Load any custom boot files based upon the current environment.
553
     *
554
     * If no boot file exists, we shouldn't continue because something
555
     * is wrong. At the very least, they should have error reporting setup.
556
     *
557
     * @return void
558
     *
559
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
560
     */
561
    protected function bootstrapEnvironment()
562
    {
UNCOV
563
        if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) {
×
564
            require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
×
565
        } else {
566
            // @codeCoverageIgnoreStart
UNCOV
567
            header('HTTP/1.1 503 Service Unavailable.', true, 503);
×
568
            echo 'The application environment is not set correctly.';
×
569

UNCOV
570
            exit(EXIT_ERROR); // EXIT_ERROR
×
571
            // @codeCoverageIgnoreEnd
572
        }
573
    }
574

575
    /**
576
     * Start the Benchmark
577
     *
578
     * The timer is used to display total script execution both in the
579
     * debug toolbar, and potentially on the displayed page.
580
     *
581
     * @return void
582
     */
583
    protected function startBenchmark()
584
    {
585
        if ($this->startTime === null) {
98✔
UNCOV
586
            $this->startTime = microtime(true);
×
587
        }
588

589
        $this->benchmark = Services::timer();
98✔
590
        $this->benchmark->start('total_execution', $this->startTime);
98✔
591
        $this->benchmark->start('bootstrap');
98✔
592
    }
593

594
    /**
595
     * Sets a Request object to be used for this request.
596
     * Used when running certain tests.
597
     *
598
     * @param CLIRequest|IncomingRequest $request
599
     *
600
     * @return $this
601
     *
602
     * @internal Used for testing purposes only.
603
     * @testTag
604
     */
605
    public function setRequest($request)
606
    {
607
        $this->request = $request;
38✔
608

609
        return $this;
38✔
610
    }
611

612
    /**
613
     * Get our Request object, (either IncomingRequest or CLIRequest).
614
     *
615
     * @return void
616
     */
617
    protected function getRequestObject()
618
    {
619
        if ($this->request instanceof Request) {
98✔
620
            $this->spoofRequestMethod();
40✔
621

622
            return;
40✔
623
        }
624

625
        if ($this->isPhpCli()) {
60✔
UNCOV
626
            Services::createRequest($this->config, true);
×
627
        } else {
628
            Services::createRequest($this->config);
60✔
629
        }
630

631
        $this->request = service('request');
60✔
632

633
        $this->spoofRequestMethod();
60✔
634
    }
635

636
    /**
637
     * Get our Response object, and set some default values, including
638
     * the HTTP protocol version and a default successful response.
639
     *
640
     * @return void
641
     */
642
    protected function getResponseObject()
643
    {
644
        $this->response = Services::response($this->config);
98✔
645

646
        if ($this->isWeb()) {
98✔
647
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
98✔
648
        }
649

650
        // Assume success until proven otherwise.
651
        $this->response->setStatusCode(200);
98✔
652
    }
653

654
    /**
655
     * Force Secure Site Access? If the config value 'forceGlobalSecureRequests'
656
     * is true, will enforce that all requests to this site are made through
657
     * HTTPS. Will redirect the user to the current page with HTTPS, as well
658
     * as set the HTTP Strict Transport Security header for those browsers
659
     * that support it.
660
     *
661
     * @param int $duration How long the Strict Transport Security
662
     *                      should be enforced for this URL.
663
     *
664
     * @return void
665
     *
666
     * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter.
667
     */
668
    protected function forceSecureAccess($duration = 31_536_000)
669
    {
UNCOV
670
        if ($this->config->forceGlobalSecureRequests !== true) {
×
671
            return;
×
672
        }
673

UNCOV
674
        force_https($duration, $this->request, $this->response);
×
675
    }
676

677
    /**
678
     * Determines if a response has been cached for the given URI.
679
     *
680
     * @return false|ResponseInterface
681
     *
682
     * @throws Exception
683
     *
684
     * @deprecated 4.5.0 PageCache required filter is used. No longer used.
685
     * @deprecated 4.4.2 The parameter $config is deprecated. No longer used.
686
     */
687
    public function displayCache(Cache $config)
688
    {
UNCOV
689
        $cachedResponse = $this->pageCache->get($this->request, $this->response);
×
690
        if ($cachedResponse instanceof ResponseInterface) {
×
691
            $this->response = $cachedResponse;
×
692

UNCOV
693
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
694
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
×
695
            $this->response->setBody($output);
×
696

UNCOV
697
            return $this->response;
×
698
        }
699

UNCOV
700
        return false;
×
701
    }
702

703
    /**
704
     * Tells the app that the final output should be cached.
705
     *
706
     * @deprecated 4.4.0 Moved to ResponseCache::setTtl(). No longer used.
707
     *
708
     * @return void
709
     */
710
    public static function cache(int $time)
711
    {
712
        static::$cacheTTL = $time;
1✔
713
    }
714

715
    /**
716
     * Caches the full response from the current request. Used for
717
     * full-page caching for very high performance.
718
     *
719
     * @return bool
720
     *
721
     * @deprecated 4.4.0 No longer used.
722
     */
723
    public function cachePage(Cache $config)
724
    {
UNCOV
725
        $headers = [];
×
726

UNCOV
727
        foreach ($this->response->headers() as $header) {
×
728
            $headers[$header->getName()] = $header->getValueLine();
×
729
        }
730

UNCOV
731
        return cache()->save($this->generateCacheName($config), serialize(['headers' => $headers, 'output' => $this->output]), static::$cacheTTL);
×
732
    }
733

734
    /**
735
     * Returns an array with our basic performance stats collected.
736
     */
737
    public function getPerformanceStats(): array
738
    {
739
        // After filter debug toolbar requires 'total_execution'.
UNCOV
740
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
741

UNCOV
742
        return [
×
743
            'startTime' => $this->startTime,
×
744
            'totalTime' => $this->totalTime,
×
745
        ];
×
746
    }
747

748
    /**
749
     * Generates the cache name to use for our full-page caching.
750
     *
751
     * @deprecated 4.4.0 No longer used.
752
     */
753
    protected function generateCacheName(Cache $config): string
754
    {
UNCOV
755
        if ($this->request instanceof CLIRequest) {
×
756
            return md5($this->request->getPath());
×
757
        }
758

UNCOV
759
        $uri = clone $this->request->getUri();
×
760

UNCOV
761
        $query = $config->cacheQueryString
×
762
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
×
763
            : '';
×
764

UNCOV
765
        return md5((string) $uri->setFragment('')->setQuery($query));
×
766
    }
767

768
    /**
769
     * Replaces the elapsed_time and memory_usage tag.
770
     *
771
     * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used.
772
     */
773
    public function displayPerformanceMetrics(string $output): string
774
    {
UNCOV
775
        return str_replace(
×
776
            ['{elapsed_time}', '{memory_usage}'],
×
777
            [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)],
×
778
            $output,
×
779
        );
×
780
    }
781

782
    /**
783
     * Try to Route It - As it sounds like, works with the router to
784
     * match a route against the current URI. If the route is a
785
     * "redirect route", will also handle the redirect.
786
     *
787
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
788
     *                                              of the config file.
789
     *
790
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
791
     *
792
     * @throws RedirectException
793
     */
794
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
795
    {
796
        $this->benchmark->start('routing');
96✔
797

798
        if (! $routes instanceof RouteCollectionInterface) {
96✔
799
            $routes = service('routes')->loadRoutes();
36✔
800
        }
801

802
        // $routes is defined in Config/Routes.php
803
        $this->router = Services::router($routes, $this->request);
96✔
804

805
        // $uri is URL-encoded.
806
        $uri = $this->request->getPath();
96✔
807

808
        $this->outputBufferingStart();
96✔
809

810
        $this->controller = $this->router->handle($uri);
96✔
811
        $this->method     = $this->router->methodName();
79✔
812

813
        // If a {locale} segment was matched in the final route,
814
        // then we need to set the correct locale on our Request.
815
        if ($this->router->hasLocale()) {
79✔
UNCOV
816
            $this->request->setLocale($this->router->getLocale());
×
817
        }
818

819
        $this->benchmark->stop('routing');
79✔
820

821
        return $this->router->getFilters();
79✔
822
    }
823

824
    /**
825
     * Determines the path to use for us to try to route to, based
826
     * on the CLI/IncomingRequest path.
827
     *
828
     * @return string
829
     *
830
     * @deprecated 4.5.0 No longer used.
831
     */
832
    protected function determinePath()
833
    {
UNCOV
834
        return $this->request->getPath();
×
835
    }
836

837
    /**
838
     * Now that everything has been setup, this method attempts to run the
839
     * controller method and make the script go. If it's not able to, will
840
     * show the appropriate Page Not Found error.
841
     *
842
     * @return ResponseInterface|string|null
843
     */
844
    protected function startController()
845
    {
846
        $this->benchmark->start('controller');
79✔
847
        $this->benchmark->start('controller_constructor');
79✔
848

849
        // Is it routed to a Closure?
850
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
79✔
851
            $controller = $this->controller;
29✔
852

853
            return $controller(...$this->router->params());
29✔
854
        }
855

856
        // No controller specified - we don't know what to do now.
857
        if (! isset($this->controller)) {
50✔
UNCOV
858
            throw PageNotFoundException::forEmptyController();
×
859
        }
860

861
        // Try to autoload the class
862
        if (
863
            ! class_exists($this->controller, true)
50✔
864
            || ($this->method[0] === '_' && $this->method !== '__invoke')
50✔
865
        ) {
UNCOV
866
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
867
        }
868

869
        // Execute route attributes' before() methods
870
        // This runs after routing/validation but BEFORE expensive controller instantiation
871
        if (config('routing')->useControllerAttributes === true) {
50✔
872
            $this->benchmark->start('route_attributes_before');
49✔
873
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
49✔
874
            $this->benchmark->stop('route_attributes_before');
48✔
875

876
            // If attribute returns a Response, short-circuit
877
            if ($attributeResponse instanceof ResponseInterface) {
48✔
878
                $this->benchmark->stop('controller_constructor');
1✔
879
                $this->benchmark->stop('controller');
1✔
880

881
                return $attributeResponse;
1✔
882
            }
883

884
            // If attribute returns a modified Request, use it
885
            if ($attributeResponse instanceof Request) {
48✔
886
                $this->request = $attributeResponse;
48✔
887
            }
888
        }
889

890
        return null;
49✔
891
    }
892

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

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

905
        $this->benchmark->stop('controller_constructor');
51✔
906

907
        return $class;
51✔
908
    }
909

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

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

934
        $this->benchmark->stop('controller');
48✔
935

936
        return $output;
48✔
937
    }
938

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

949
        // Is there a 404 Override available?
950
        $override = $this->router->get404Override();
12✔
951

952
        if ($override !== null) {
12✔
953
            $returned = null;
4✔
954

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

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

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

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

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

971
            unset($override);
4✔
972

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

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

979
        $this->outputBufferingEnd();
8✔
980

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

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

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

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

1018
        if (is_string($returned)) {
80✔
1019
            $this->output .= $returned;
72✔
1020
        }
1021

1022
        $this->response->setBody($this->output);
80✔
1023
    }
1024

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

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

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

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

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

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

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

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

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

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

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

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

1138
        return $this;
39✔
1139
    }
1140

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

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

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

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

© 2025 Coveralls, Inc