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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

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

65.02
/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\FrameworkException;
19
use CodeIgniter\Exceptions\LogicException;
20
use CodeIgniter\Exceptions\PageNotFoundException;
21
use CodeIgniter\Filters\Filters;
22
use CodeIgniter\HTTP\CLIRequest;
23
use CodeIgniter\HTTP\DownloadResponse;
24
use CodeIgniter\HTTP\Exceptions\RedirectException;
25
use CodeIgniter\HTTP\IncomingRequest;
26
use CodeIgniter\HTTP\Method;
27
use CodeIgniter\HTTP\RedirectResponse;
28
use CodeIgniter\HTTP\Request;
29
use CodeIgniter\HTTP\ResponsableInterface;
30
use CodeIgniter\HTTP\ResponseInterface;
31
use CodeIgniter\HTTP\URI;
32
use CodeIgniter\Router\Exceptions\RedirectException as DeprecatedRedirectException;
33
use CodeIgniter\Router\RouteCollectionInterface;
34
use CodeIgniter\Router\Router;
35
use Config\App;
36
use Config\Cache;
37
use Config\Feature;
38
use Config\Kint as KintConfig;
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.5.7';
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
100
     */
101
    protected $response;
102

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

124
    /**
125
     * Output handler to use.
126
     *
127
     * @var string
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
     * @phpstan-var 'php-cli'|'web'
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);
6,525✔
177
        $this->config    = $config;
6,525✔
178

179
        $this->pageCache = Services::responsecache();
6,525✔
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 ?? 'en');
6,525✔
191

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

196
    /**
197
     * Checks system for missing required PHP extensions.
198
     *
199
     * @return void
200
     *
201
     * @throws FrameworkException
202
     *
203
     * @codeCoverageIgnore
204
     *
205
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
206
     */
207
    protected function resolvePlatformExtensions()
208
    {
209
        $requiredExtensions = [
×
210
            'intl',
×
211
            'json',
×
212
            'mbstring',
×
213
        ];
×
214

215
        $missingExtensions = [];
×
216

217
        foreach ($requiredExtensions as $extension) {
×
218
            if (! extension_loaded($extension)) {
×
219
                $missingExtensions[] = $extension;
×
220
            }
221
        }
222

223
        if ($missingExtensions !== []) {
×
224
            throw FrameworkException::forMissingExtension(implode(', ', $missingExtensions));
×
225
        }
226
    }
227

228
    /**
229
     * Initializes Kint
230
     *
231
     * @return void
232
     *
233
     * @deprecated 4.5.0 Moved to Autoloader.
234
     */
235
    protected function initializeKint()
236
    {
237
        if (CI_DEBUG) {
×
238
            $this->autoloadKint();
×
239
            $this->configureKint();
×
240
        } elseif (class_exists(Kint::class)) {
×
241
            // In case that Kint is already loaded via Composer.
242
            Kint::$enabled_mode = false;
×
243
            // @codeCoverageIgnore
244
        }
245

246
        helper('kint');
×
247
    }
248

249
    /**
250
     * @deprecated 4.5.0 Moved to Autoloader.
251
     */
252
    private function autoloadKint(): void
253
    {
254
        // If we have KINT_DIR it means it's already loaded via composer
255
        if (! defined('KINT_DIR')) {
×
256
            spl_autoload_register(function ($class): void {
×
257
                $class = explode('\\', $class);
×
258

259
                if (array_shift($class) !== 'Kint') {
×
260
                    return;
×
261
                }
262

263
                $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';
×
264

265
                if (is_file($file)) {
×
266
                    require_once $file;
×
267
                }
268
            });
×
269

270
            require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
×
271
        }
272
    }
273

274
    /**
275
     * @deprecated 4.5.0 Moved to Autoloader.
276
     */
277
    private function configureKint(): void
278
    {
279
        $config = new KintConfig();
×
280

281
        Kint::$depth_limit         = $config->maxDepth;
×
282
        Kint::$display_called_from = $config->displayCalledFrom;
×
283
        Kint::$expanded            = $config->expanded;
×
284

285
        if (isset($config->plugins) && is_array($config->plugins)) {
×
286
            Kint::$plugins = $config->plugins;
×
287
        }
288

289
        $csp = Services::csp();
×
290
        if ($csp->enabled()) {
×
291
            RichRenderer::$js_nonce  = $csp->getScriptNonce();
×
292
            RichRenderer::$css_nonce = $csp->getStyleNonce();
×
293
        }
294

295
        RichRenderer::$theme  = $config->richTheme;
×
296
        RichRenderer::$folder = $config->richFolder;
×
297

298
        if (isset($config->richObjectPlugins) && is_array($config->richObjectPlugins)) {
×
299
            RichRenderer::$value_plugins = $config->richObjectPlugins;
×
300
        }
301
        if (isset($config->richTabPlugins) && is_array($config->richTabPlugins)) {
×
302
            RichRenderer::$tab_plugins = $config->richTabPlugins;
×
303
        }
304

305
        CliRenderer::$cli_colors         = $config->cliColors;
×
306
        CliRenderer::$force_utf8         = $config->cliForceUTF8;
×
307
        CliRenderer::$detect_width       = $config->cliDetectWidth;
×
308
        CliRenderer::$min_terminal_width = $config->cliMinWidth;
×
309
    }
310

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

332
        $this->pageCache->setTtl(0);
91✔
333
        $this->bufferLevel = ob_get_level();
91✔
334

335
        $this->startBenchmark();
91✔
336

337
        $this->getRequestObject();
91✔
338
        $this->getResponseObject();
91✔
339

340
        Events::trigger('pre_system');
91✔
341

342
        $this->benchmark->stop('bootstrap');
91✔
343

344
        $this->benchmark->start('required_before_filters');
91✔
345
        // Start up the filters
346
        $filters = Services::filters();
91✔
347
        // Run required before filters
348
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
91✔
349

350
        // If a ResponseInterface instance is returned then send it back to the client and stop
351
        if ($possibleResponse instanceof ResponseInterface) {
91✔
352
            $this->response = $possibleResponse;
3✔
353
        } else {
354
            try {
355
                $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
90✔
356
            } catch (DeprecatedRedirectException|ResponsableInterface $e) {
18✔
357
                $this->outputBufferingEnd();
7✔
358
                if ($e instanceof DeprecatedRedirectException) {
7✔
359
                    $e = new RedirectException($e->getMessage(), $e->getCode(), $e);
1✔
360
                }
361

362
                $this->response = $e->getResponse();
7✔
363
            } catch (PageNotFoundException $e) {
11✔
364
                $this->response = $this->display404errors($e);
11✔
365
            } catch (Throwable $e) {
×
366
                $this->outputBufferingEnd();
×
367

368
                throw $e;
×
369
            }
370
        }
371

372
        $this->runRequiredAfterFilters($filters);
84✔
373

374
        // Is there a post-system event?
375
        Events::trigger('post_system');
84✔
376

377
        if ($returnResponse) {
84✔
378
            return $this->response;
37✔
379
        }
380

381
        $this->sendResponse();
47✔
382

383
        return null;
47✔
384
    }
385

386
    /**
387
     * Run required before filters.
388
     */
389
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
390
    {
391
        $possibleResponse = $filters->runRequired('before');
91✔
392
        $this->benchmark->stop('required_before_filters');
91✔
393

394
        // If a ResponseInterface instance is returned then send it back to the client and stop
395
        if ($possibleResponse instanceof ResponseInterface) {
91✔
396
            return $possibleResponse;
3✔
397
        }
398

399
        return null;
90✔
400
    }
401

402
    /**
403
     * Run required after filters.
404
     */
405
    private function runRequiredAfterFilters(Filters $filters): void
406
    {
407
        $filters->setResponse($this->response);
84✔
408

409
        // Run required after filters
410
        $this->benchmark->start('required_after_filters');
84✔
411
        $response = $filters->runRequired('after');
84✔
412
        $this->benchmark->stop('required_after_filters');
84✔
413

414
        if ($response instanceof ResponseInterface) {
84✔
415
            $this->response = $response;
84✔
416
        }
417
    }
418

419
    /**
420
     * Invoked via php-cli command?
421
     */
422
    private function isPhpCli(): bool
423
    {
424
        return $this->context === 'php-cli';
53✔
425
    }
426

427
    /**
428
     * Web access?
429
     */
430
    private function isWeb(): bool
431
    {
432
        return $this->context === 'web';
91✔
433
    }
434

435
    /**
436
     * Disables Controller Filters.
437
     */
438
    public function disableFilters(): void
439
    {
440
        $this->enableFilters = false;
1✔
441
    }
442

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

459
        $routeFilters = $this->tryToRouteIt($routes);
89✔
460

461
        // $uri is URL-encoded.
462
        $uri = $this->request->getPath();
72✔
463

464
        if ($this->enableFilters) {
72✔
465
            /** @var Filters $filters */
466
            $filters = service('filters');
71✔
467

468
            // If any filters were specified within the routes file,
469
            // we need to ensure it's active for the current request
470
            if ($routeFilters !== null) {
71✔
471
                $filters->enableFilters($routeFilters, 'before');
71✔
472

473
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
71✔
474
                if (! $oldFilterOrder) {
71✔
475
                    $routeFilters = array_reverse($routeFilters);
71✔
476
                }
477

478
                $filters->enableFilters($routeFilters, 'after');
71✔
479
            }
480

481
            // Run "before" filters
482
            $this->benchmark->start('before_filters');
71✔
483
            $possibleResponse = $filters->run($uri, 'before');
71✔
484
            $this->benchmark->stop('before_filters');
71✔
485

486
            // If a ResponseInterface instance is returned then send it back to the client and stop
487
            if ($possibleResponse instanceof ResponseInterface) {
71✔
488
                $this->outputBufferingEnd();
1✔
489

490
                return $possibleResponse;
1✔
491
            }
492

493
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
70✔
494
                $this->request = $possibleResponse;
70✔
495
            }
496
        }
497

498
        $returned = $this->startController();
71✔
499

500
        // Closure controller has run in startController().
501
        if (! is_callable($this->controller)) {
70✔
502
            $controller = $this->createController();
41✔
503

504
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
41✔
505
                throw PageNotFoundException::forMethodNotFound($this->method);
×
506
            }
507

508
            // Is there a "post_controller_constructor" event?
509
            Events::trigger('post_controller_constructor');
41✔
510

511
            $returned = $this->runController($controller);
41✔
512
        } else {
513
            $this->benchmark->stop('controller_constructor');
29✔
514
            $this->benchmark->stop('controller');
29✔
515
        }
516

517
        // If $returned is a string, then the controller output something,
518
        // probably a view, instead of echoing it directly. Send it along
519
        // so it can be used with the output.
520
        $this->gatherOutput($cacheConfig, $returned);
70✔
521

522
        if ($this->enableFilters) {
70✔
523
            /** @var Filters $filters */
524
            $filters = service('filters');
69✔
525
            $filters->setResponse($this->response);
69✔
526

527
            // Run "after" filters
528
            $this->benchmark->start('after_filters');
69✔
529
            $response = $filters->run($uri, 'after');
69✔
530
            $this->benchmark->stop('after_filters');
69✔
531

532
            if ($response instanceof ResponseInterface) {
69✔
533
                $this->response = $response;
69✔
534
            }
535
        }
536

537
        // Skip unnecessary processing for special Responses.
538
        if (
539
            ! $this->response instanceof DownloadResponse
70✔
540
            && ! $this->response instanceof RedirectResponse
70✔
541
        ) {
542
            // Save our current URI as the previous URI in the session
543
            // for safer, more accurate use with `previous_url()` helper function.
544
            $this->storePreviousURL(current_url(true));
68✔
545
        }
546

547
        unset($uri);
70✔
548

549
        return $this->response;
70✔
550
    }
551

552
    /**
553
     * You can load different configurations depending on your
554
     * current environment. Setting the environment also influences
555
     * things like logging and error reporting.
556
     *
557
     * This can be set to anything, but default usage is:
558
     *
559
     *     development
560
     *     testing
561
     *     production
562
     *
563
     * @codeCoverageIgnore
564
     *
565
     * @return void
566
     *
567
     * @deprecated 4.4.0 No longer used. Moved to index.php and spark.
568
     */
569
    protected function detectEnvironment()
570
    {
571
        // Make sure ENVIRONMENT isn't already set by other means.
572
        if (! defined('ENVIRONMENT')) {
×
573
            define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
×
574
        }
575
    }
576

577
    /**
578
     * Load any custom boot files based upon the current environment.
579
     *
580
     * If no boot file exists, we shouldn't continue because something
581
     * is wrong. At the very least, they should have error reporting setup.
582
     *
583
     * @return void
584
     *
585
     * @deprecated 4.5.0 Moved to system/bootstrap.php.
586
     */
587
    protected function bootstrapEnvironment()
588
    {
589
        if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) {
×
590
            require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
×
591
        } else {
592
            // @codeCoverageIgnoreStart
593
            header('HTTP/1.1 503 Service Unavailable.', true, 503);
×
594
            echo 'The application environment is not set correctly.';
×
595

596
            exit(EXIT_ERROR); // EXIT_ERROR
×
597
            // @codeCoverageIgnoreEnd
598
        }
599
    }
600

601
    /**
602
     * Start the Benchmark
603
     *
604
     * The timer is used to display total script execution both in the
605
     * debug toolbar, and potentially on the displayed page.
606
     *
607
     * @return void
608
     */
609
    protected function startBenchmark()
610
    {
611
        if ($this->startTime === null) {
91✔
612
            $this->startTime = microtime(true);
×
613
        }
614

615
        $this->benchmark = Services::timer();
91✔
616
        $this->benchmark->start('total_execution', $this->startTime);
91✔
617
        $this->benchmark->start('bootstrap');
91✔
618
    }
619

620
    /**
621
     * Sets a Request object to be used for this request.
622
     * Used when running certain tests.
623
     *
624
     * @param CLIRequest|IncomingRequest $request
625
     *
626
     * @return $this
627
     *
628
     * @internal Used for testing purposes only.
629
     * @testTag
630
     */
631
    public function setRequest($request)
632
    {
633
        $this->request = $request;
38✔
634

635
        return $this;
38✔
636
    }
637

638
    /**
639
     * Get our Request object, (either IncomingRequest or CLIRequest).
640
     *
641
     * @return void
642
     */
643
    protected function getRequestObject()
644
    {
645
        if ($this->request instanceof Request) {
91✔
646
            $this->spoofRequestMethod();
40✔
647

648
            return;
40✔
649
        }
650

651
        if ($this->isPhpCli()) {
53✔
652
            Services::createRequest($this->config, true);
×
653
        } else {
654
            Services::createRequest($this->config);
53✔
655
        }
656

657
        $this->request = service('request');
53✔
658

659
        $this->spoofRequestMethod();
53✔
660
    }
661

662
    /**
663
     * Get our Response object, and set some default values, including
664
     * the HTTP protocol version and a default successful response.
665
     *
666
     * @return void
667
     */
668
    protected function getResponseObject()
669
    {
670
        $this->response = Services::response($this->config);
91✔
671

672
        if ($this->isWeb()) {
91✔
673
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
91✔
674
        }
675

676
        // Assume success until proven otherwise.
677
        $this->response->setStatusCode(200);
91✔
678
    }
679

680
    /**
681
     * Force Secure Site Access? If the config value 'forceGlobalSecureRequests'
682
     * is true, will enforce that all requests to this site are made through
683
     * HTTPS. Will redirect the user to the current page with HTTPS, as well
684
     * as set the HTTP Strict Transport Security header for those browsers
685
     * that support it.
686
     *
687
     * @param int $duration How long the Strict Transport Security
688
     *                      should be enforced for this URL.
689
     *
690
     * @return void
691
     *
692
     * @deprecated 4.5.0 No longer used. Moved to ForceHTTPS filter.
693
     */
694
    protected function forceSecureAccess($duration = 31_536_000)
695
    {
696
        if ($this->config->forceGlobalSecureRequests !== true) {
×
697
            return;
×
698
        }
699

700
        force_https($duration, $this->request, $this->response);
×
701
    }
702

703
    /**
704
     * Determines if a response has been cached for the given URI.
705
     *
706
     * @return false|ResponseInterface
707
     *
708
     * @throws Exception
709
     *
710
     * @deprecated 4.5.0 PageCache required filter is used. No longer used.
711
     * @deprecated 4.4.2 The parameter $config is deprecated. No longer used.
712
     */
713
    public function displayCache(Cache $config)
714
    {
715
        $cachedResponse = $this->pageCache->get($this->request, $this->response);
×
716
        if ($cachedResponse instanceof ResponseInterface) {
×
717
            $this->response = $cachedResponse;
×
718

719
            $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
720
            $output          = $this->displayPerformanceMetrics($cachedResponse->getBody());
×
721
            $this->response->setBody($output);
×
722

723
            return $this->response;
×
724
        }
725

726
        return false;
×
727
    }
728

729
    /**
730
     * Tells the app that the final output should be cached.
731
     *
732
     * @deprecated 4.4.0 Moved to ResponseCache::setTtl(). No longer used.
733
     *
734
     * @return void
735
     */
736
    public static function cache(int $time)
737
    {
738
        static::$cacheTTL = $time;
1✔
739
    }
740

741
    /**
742
     * Caches the full response from the current request. Used for
743
     * full-page caching for very high performance.
744
     *
745
     * @return bool
746
     *
747
     * @deprecated 4.4.0 No longer used.
748
     */
749
    public function cachePage(Cache $config)
750
    {
751
        $headers = [];
×
752

753
        foreach ($this->response->headers() as $header) {
×
754
            $headers[$header->getName()] = $header->getValueLine();
×
755
        }
756

757
        return cache()->save($this->generateCacheName($config), serialize(['headers' => $headers, 'output' => $this->output]), static::$cacheTTL);
×
758
    }
759

760
    /**
761
     * Returns an array with our basic performance stats collected.
762
     */
763
    public function getPerformanceStats(): array
764
    {
765
        // After filter debug toolbar requires 'total_execution'.
766
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
767

768
        return [
×
769
            'startTime' => $this->startTime,
×
770
            'totalTime' => $this->totalTime,
×
771
        ];
×
772
    }
773

774
    /**
775
     * Generates the cache name to use for our full-page caching.
776
     *
777
     * @deprecated 4.4.0 No longer used.
778
     */
779
    protected function generateCacheName(Cache $config): string
780
    {
781
        if ($this->request instanceof CLIRequest) {
×
782
            return md5($this->request->getPath());
×
783
        }
784

785
        $uri = clone $this->request->getUri();
×
786

787
        $query = $config->cacheQueryString
×
788
            ? $uri->getQuery(is_array($config->cacheQueryString) ? ['only' => $config->cacheQueryString] : [])
×
789
            : '';
×
790

791
        return md5((string) $uri->setFragment('')->setQuery($query));
×
792
    }
793

794
    /**
795
     * Replaces the elapsed_time and memory_usage tag.
796
     *
797
     * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used.
798
     */
799
    public function displayPerformanceMetrics(string $output): string
800
    {
801
        return str_replace(
×
802
            ['{elapsed_time}', '{memory_usage}'],
×
803
            [(string) $this->totalTime, number_format(memory_get_peak_usage() / 1024 / 1024, 3)],
×
804
            $output
×
805
        );
×
806
    }
807

808
    /**
809
     * Try to Route It - As it sounds like, works with the router to
810
     * match a route against the current URI. If the route is a
811
     * "redirect route", will also handle the redirect.
812
     *
813
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
814
     *                                              of the config file.
815
     *
816
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
817
     *
818
     * @throws RedirectException
819
     */
820
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
821
    {
822
        $this->benchmark->start('routing');
89✔
823

824
        if (! $routes instanceof RouteCollectionInterface) {
89✔
825
            $routes = service('routes')->loadRoutes();
29✔
826
        }
827

828
        // $routes is defined in Config/Routes.php
829
        $this->router = Services::router($routes, $this->request);
89✔
830

831
        // $uri is URL-encoded.
832
        $uri = $this->request->getPath();
89✔
833

834
        $this->outputBufferingStart();
89✔
835

836
        $this->controller = $this->router->handle($uri);
89✔
837
        $this->method     = $this->router->methodName();
72✔
838

839
        // If a {locale} segment was matched in the final route,
840
        // then we need to set the correct locale on our Request.
841
        if ($this->router->hasLocale()) {
72✔
842
            $this->request->setLocale($this->router->getLocale());
×
843
        }
844

845
        $this->benchmark->stop('routing');
72✔
846

847
        return $this->router->getFilters();
72✔
848
    }
849

850
    /**
851
     * Determines the path to use for us to try to route to, based
852
     * on the CLI/IncomingRequest path.
853
     *
854
     * @return string
855
     *
856
     * @deprecated 4.5.0 No longer used.
857
     */
858
    protected function determinePath()
859
    {
860
        return $this->request->getPath();
×
861
    }
862

863
    /**
864
     * Now that everything has been setup, this method attempts to run the
865
     * controller method and make the script go. If it's not able to, will
866
     * show the appropriate Page Not Found error.
867
     *
868
     * @return ResponseInterface|string|null
869
     */
870
    protected function startController()
871
    {
872
        $this->benchmark->start('controller');
72✔
873
        $this->benchmark->start('controller_constructor');
72✔
874

875
        // Is it routed to a Closure?
876
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
72✔
877
            $controller = $this->controller;
30✔
878

879
            return $controller(...$this->router->params());
30✔
880
        }
881

882
        // No controller specified - we don't know what to do now.
883
        if (! isset($this->controller)) {
42✔
884
            throw PageNotFoundException::forEmptyController();
×
885
        }
886

887
        // Try to autoload the class
888
        if (
889
            ! class_exists($this->controller, true)
42✔
890
            || ($this->method[0] === '_' && $this->method !== '__invoke')
42✔
891
        ) {
892
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
893
        }
894

895
        return null;
42✔
896
    }
897

898
    /**
899
     * Instantiates the controller class.
900
     *
901
     * @return Controller
902
     */
903
    protected function createController()
904
    {
905
        assert(is_string($this->controller));
906

907
        $class = new $this->controller();
44✔
908
        $class->initController($this->request, $this->response, Services::logger());
44✔
909

910
        $this->benchmark->stop('controller_constructor');
44✔
911

912
        return $class;
44✔
913
    }
914

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

933
        // The controller method param types may not be string.
934
        // So cannot set `declare(strict_types=1)` in this file.
935
        $output = method_exists($class, '_remap')
41✔
936
            ? $class->_remap($this->method, ...$params)
×
937
            : $class->{$this->method}(...$params);
41✔
938

939
        $this->benchmark->stop('controller');
41✔
940

941
        return $output;
41✔
942
    }
943

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

954
        // Is there a 404 Override available?
955
        if ($override = $this->router->get404Override()) {
11✔
956
            $returned = null;
4✔
957

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

964
                $this->controller = $override[0];
3✔
965
                $this->method     = $override[1];
3✔
966

967
                $controller = $this->createController();
3✔
968

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

971
                $this->benchmark->stop('controller');
3✔
972
            }
973

974
            unset($override);
4✔
975

976
            $cacheConfig = config(Cache::class);
4✔
977
            $this->gatherOutput($cacheConfig, $returned);
4✔
978

979
            return $this->response;
4✔
980
        }
981

982
        $this->outputBufferingEnd();
7✔
983

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

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

1005
        if ($returned instanceof DownloadResponse) {
74✔
1006
            $this->response = $returned;
1✔
1007

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

1021
        if (is_string($returned)) {
73✔
1022
            $this->output .= $returned;
64✔
1023
        }
1024

1025
        $this->response->setBody($this->output);
73✔
1026
    }
1027

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

1050
        // Ignore unroutable responses
1051
        if ($this->response instanceof DownloadResponse || $this->response instanceof RedirectResponse) {
68✔
1052
            return;
×
1053
        }
1054

1055
        // Ignore non-HTML responses
1056
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
68✔
1057
            return;
12✔
1058
        }
1059

1060
        // This is mainly needed during testing...
1061
        if (is_string($uri)) {
56✔
1062
            $uri = new URI($uri);
×
1063
        }
1064

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

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

1089
        $method = $this->request->getPost('_method');
16✔
1090

1091
        if ($method === null) {
16✔
1092
            return;
14✔
1093
        }
1094

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

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

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

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

1141
        return $this;
39✔
1142
    }
1143

1144
    protected function outputBufferingStart(): void
1145
    {
1146
        $this->bufferLevel = ob_get_level();
89✔
1147
        ob_start();
89✔
1148
    }
1149

1150
    protected function outputBufferingEnd(): string
1151
    {
1152
        $buffer = '';
89✔
1153

1154
        while (ob_get_level() > $this->bufferLevel) {
89✔
1155
            $buffer .= ob_get_contents();
89✔
1156
            ob_end_clean();
89✔
1157
        }
1158

1159
        return $buffer;
89✔
1160
    }
1161
}
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