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

codeigniter4 / CodeIgniter4 / 25039479136

28 Apr 2026 07:20AM UTC coverage: 88.309%. First build
25039479136

Pull #10147

github

web-flow
Merge e111c5ff3 into 0d51e0015
Pull Request #10147: refactor: keep CSP lazy when resetting Kint in worker mode

5 of 7 new or added lines in 1 file covered. (71.43%)

23273 of 26354 relevant lines covered (88.31%)

217.1 hits per line

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

90.07
/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\Exceptions\FormRequestException;
23
use CodeIgniter\HTTP\Exceptions\RedirectException;
24
use CodeIgniter\HTTP\FormRequest;
25
use CodeIgniter\HTTP\IncomingRequest;
26
use CodeIgniter\HTTP\Method;
27
use CodeIgniter\HTTP\NonBufferedResponseInterface;
28
use CodeIgniter\HTTP\RedirectResponse;
29
use CodeIgniter\HTTP\Request;
30
use CodeIgniter\HTTP\RequestInterface;
31
use CodeIgniter\HTTP\ResponsableInterface;
32
use CodeIgniter\HTTP\ResponseInterface;
33
use CodeIgniter\HTTP\URI;
34
use CodeIgniter\Router\CallableParamClassifier;
35
use CodeIgniter\Router\ParamKind;
36
use CodeIgniter\Router\RouteCollectionInterface;
37
use CodeIgniter\Router\Router;
38
use Config\App;
39
use Config\Cache;
40
use Config\Feature;
41
use Config\Services;
42
use Kint\Kint;
43
use Kint\Renderer\RichRenderer;
44
use Locale;
45
use ReflectionFunction;
46
use ReflectionFunctionAbstract;
47
use ReflectionMethod;
48
use Throwable;
49

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

64
    /**
65
     * App startup time.
66
     *
67
     * @var float|null
68
     */
69
    protected $startTime;
70

71
    /**
72
     * Total app execution time
73
     *
74
     * @var float
75
     */
76
    protected $totalTime;
77

78
    /**
79
     * Main application configuration
80
     *
81
     * @var App
82
     */
83
    protected $config;
84

85
    /**
86
     * Timer instance.
87
     *
88
     * @var Timer
89
     */
90
    protected $benchmark;
91

92
    /**
93
     * Current request.
94
     *
95
     * @var CLIRequest|IncomingRequest|null
96
     */
97
    protected $request;
98

99
    /**
100
     * Current response.
101
     *
102
     * @var ResponseInterface|null
103
     */
104
    protected $response;
105

106
    /**
107
     * Router to use.
108
     *
109
     * @var Router|null
110
     */
111
    protected $router;
112

113
    /**
114
     * Controller to use.
115
     *
116
     * @var Closure|string|null
117
     */
118
    protected $controller;
119

120
    /**
121
     * Controller method to invoke.
122
     *
123
     * @var string|null
124
     */
125
    protected $method;
126

127
    /**
128
     * Output handler to use.
129
     *
130
     * @var string|null
131
     */
132
    protected $output;
133

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

143
    /**
144
     * Whether to enable Control Filters.
145
     */
146
    protected bool $enableFilters = true;
147

148
    /**
149
     * Application output buffering level
150
     */
151
    protected int $bufferLevel;
152

153
    /**
154
     * Web Page Caching
155
     */
156
    protected ResponseCache $pageCache;
157

158
    /**
159
     * Constructor.
160
     */
161
    public function __construct(App $config)
162
    {
163
        $this->startTime = microtime(true);
7,505✔
164
        $this->config    = $config;
7,505✔
165

166
        $this->pageCache = Services::responsecache();
7,505✔
167
    }
168

169
    /**
170
     * Handles some basic app and environment setup.
171
     *
172
     * @return void
173
     */
174
    public function initialize()
175
    {
176
        // Set default locale on the server
177
        Locale::setDefault($this->config->defaultLocale);
7,505✔
178

179
        // Set default timezone on the server
180
        date_default_timezone_set($this->config->appTimezone);
7,505✔
181
    }
182

183
    /**
184
     * Reset request-specific state for worker mode.
185
     * Clears all request/response data to prepare for the next request.
186
     */
187
    public function resetForWorkerMode(): void
188
    {
189
        $this->request    = null;
2✔
190
        $this->response   = null;
2✔
191
        $this->router     = null;
2✔
192
        $this->controller = null;
2✔
193
        $this->method     = null;
2✔
194
        $this->output     = null;
2✔
195

196
        // Reset timing
197
        $this->startTime = null;
2✔
198
        $this->totalTime = 0;
2✔
199

200
        $this->resetKintForWorkerMode();
2✔
201
    }
202

203
    /**
204
     * Resets Kint request-specific state for worker mode.
205
     */
206
    private function resetKintForWorkerMode(): void
207
    {
208
        if (! CI_DEBUG || ! class_exists(Kint::class, false)) {
2✔
209
            return;
×
210
        }
211

212
        // Keep CSP lazy unless it was already initialized or explicitly enabled.
213
        if (Services::has('csp') || config(App::class)->CSPEnabled) {
2✔
214
            $csp = service('csp');
1✔
215

216
            if ($csp->enabled()) {
1✔
217
                RichRenderer::$js_nonce  = $csp->getScriptNonce();
1✔
218
                RichRenderer::$css_nonce = $csp->getStyleNonce();
1✔
219
            } else {
NEW
220
                RichRenderer::$js_nonce  = null;
×
NEW
221
                RichRenderer::$css_nonce = null;
×
222
            }
223
        } else {
224
            RichRenderer::$js_nonce  = null;
1✔
225
            RichRenderer::$css_nonce = null;
1✔
226
        }
227

228
        RichRenderer::$needs_pre_render = true;
2✔
229
    }
230

231
    /**
232
     * Launch the application!
233
     *
234
     * This is "the loop" if you will. The main entry point into the script
235
     * that gets the required class instances, fires off the filters,
236
     * tries to route the response, loads the controller and generally
237
     * makes all the pieces work together.
238
     *
239
     * @param bool $returnResponse Used for testing purposes only.
240
     *
241
     * @return ResponseInterface|null
242
     */
243
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
244
    {
245
        if ($this->context === null) {
110✔
246
            throw new LogicException(
×
247
                'Context must be set before run() is called. If you are upgrading from 4.1.x, '
×
248
                . 'you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.',
×
249
            );
×
250
        }
251

252
        $this->pageCache->setTtl(0);
110✔
253
        $this->bufferLevel = ob_get_level();
110✔
254

255
        $this->startBenchmark();
110✔
256

257
        $this->getRequestObject();
110✔
258
        $this->getResponseObject();
110✔
259

260
        Events::trigger('pre_system');
110✔
261

262
        $this->benchmark->stop('bootstrap');
110✔
263

264
        $this->benchmark->start('required_before_filters');
110✔
265
        // Start up the filters
266
        $filters = Services::filters();
110✔
267
        // Run required before filters
268
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
110✔
269

270
        // If a ResponseInterface instance is returned then send it back to the client and stop
271
        if ($possibleResponse instanceof ResponseInterface) {
110✔
272
            $this->response = $possibleResponse;
4✔
273
        } else {
274
            try {
275
                $this->response = $this->handleRequest($routes);
109✔
276
            } catch (ResponsableInterface $e) {
22✔
277
                $this->outputBufferingEnd();
10✔
278

279
                $this->response = $e->getResponse();
10✔
280
            } catch (PageNotFoundException $e) {
12✔
281
                $this->response = $this->display404errors($e);
12✔
282
            } catch (Throwable $e) {
×
283
                $this->outputBufferingEnd();
×
284

285
                throw $e;
×
286
            }
287
        }
288

289
        $this->runRequiredAfterFilters($filters);
102✔
290

291
        // Is there a post-system event?
292
        Events::trigger('post_system');
102✔
293

294
        if ($returnResponse) {
102✔
295
            return $this->response;
47✔
296
        }
297

298
        $this->sendResponse();
55✔
299

300
        return null;
55✔
301
    }
302

303
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
304
    {
305
        $possibleResponse = $filters->runRequired('before');
110✔
306
        $this->benchmark->stop('required_before_filters');
110✔
307

308
        // If a ResponseInterface instance is returned then send it back to the client and stop
309
        if ($possibleResponse instanceof ResponseInterface) {
110✔
310
            return $possibleResponse;
4✔
311
        }
312

313
        return null;
109✔
314
    }
315

316
    private function runRequiredAfterFilters(Filters $filters): void
317
    {
318
        $filters->setResponse($this->response);
102✔
319

320
        $this->benchmark->start('required_after_filters');
102✔
321
        $response = $filters->runRequired('after');
102✔
322
        $this->benchmark->stop('required_after_filters');
102✔
323

324
        if ($response instanceof ResponseInterface) {
102✔
325
            $this->response = $response;
102✔
326
        }
327
    }
328

329
    /**
330
     * Invoked via php-cli command?
331
     */
332
    private function isPhpCli(): bool
333
    {
334
        return $this->context === 'php-cli';
72✔
335
    }
336

337
    /**
338
     * Web access?
339
     */
340
    private function isWeb(): bool
341
    {
342
        return $this->context === 'web';
110✔
343
    }
344

345
    /**
346
     * Disables Controller Filters.
347
     */
348
    public function disableFilters(): void
349
    {
350
        $this->enableFilters = false;
1✔
351
    }
352

353
    /**
354
     * Handles the main request logic and fires the controller.
355
     *
356
     * @return ResponseInterface
357
     *
358
     * @throws PageNotFoundException
359
     * @throws RedirectException
360
     */
361
    protected function handleRequest(?RouteCollectionInterface $routes, ?Cache $cacheConfig = null)
362
    {
363
        if (func_num_args() > 1) {
109✔
364
            // @todo v4.8.0: Remove this check and the $cacheConfig parameter from the method signature.
365
            @trigger_error(sprintf('Since v4.8.0, the $cacheConfig parameter of %s is deprecated and no longer used.', __METHOD__), E_USER_DEPRECATED);
×
366
        }
367

368
        if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
109✔
369
            return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
1✔
370
        }
371

372
        $routeFilters = $this->tryToRouteIt($routes);
108✔
373

374
        // $uri is URL-encoded.
375
        $uri = $this->request->getPath();
91✔
376

377
        if ($this->enableFilters) {
91✔
378
            /** @var Filters $filters */
379
            $filters = service('filters');
90✔
380

381
            // If any filters were specified within the routes file,
382
            // we need to ensure it's active for the current request
383
            if ($routeFilters !== null) {
90✔
384
                $filters->enableFilters($routeFilters, 'before');
90✔
385

386
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property
90✔
387
                if (! $oldFilterOrder) {
90✔
388
                    $routeFilters = array_reverse($routeFilters);
90✔
389
                }
390

391
                $filters->enableFilters($routeFilters, 'after');
90✔
392
            }
393

394
            // Run "before" filters
395
            $this->benchmark->start('before_filters');
90✔
396
            $possibleResponse = $filters->run($uri, 'before');
90✔
397
            $this->benchmark->stop('before_filters');
90✔
398

399
            // If a ResponseInterface instance is returned then send it back to the client and stop
400
            if ($possibleResponse instanceof ResponseInterface) {
90✔
401
                $this->outputBufferingEnd();
1✔
402

403
                return $possibleResponse;
1✔
404
            }
405

406
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
89✔
407
                $this->request = $possibleResponse;
89✔
408
            }
409
        }
410

411
        $returned = $this->startController();
90✔
412

413
        // If startController returned a Response (from an attribute or Closure), use it
414
        if ($returned instanceof ResponseInterface) {
89✔
415
            $this->gatherOutput($returned);
8✔
416
        }
417
        // Closure controller has run in startController() - benchmarks were
418
        // stopped there as well.
419
        elseif (! is_callable($this->controller)) {
82✔
420
            $controller = $this->createController();
59✔
421

422
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
59✔
423
                throw PageNotFoundException::forMethodNotFound($this->method);
×
424
            }
425

426
            // Is there a "post_controller_constructor" event?
427
            Events::trigger('post_controller_constructor');
59✔
428

429
            $returned = $this->runController($controller);
59✔
430
        }
431

432
        // If $returned is a string, then the controller output something,
433
        // probably a view, instead of echoing it directly. Send it along
434
        // so it can be used with the output.
435
        $this->gatherOutput($returned);
85✔
436

437
        if ($this->enableFilters) {
85✔
438
            /** @var Filters $filters */
439
            $filters = service('filters');
84✔
440
            $filters->setResponse($this->response);
84✔
441

442
            // Run "after" filters
443
            $this->benchmark->start('after_filters');
84✔
444
            $response = $filters->run($uri, 'after');
84✔
445
            $this->benchmark->stop('after_filters');
84✔
446

447
            if ($response instanceof ResponseInterface) {
84✔
448
                $this->response = $response;
84✔
449
            }
450
        }
451

452
        // Execute controller attributes' after() methods AFTER framework filters
453
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
85✔
454
            $this->benchmark->start('route_attributes_after');
84✔
455
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
84✔
456
            $this->benchmark->stop('route_attributes_after');
84✔
457
        }
458

459
        // Skip unnecessary processing for special Responses.
460
        if (
461
            ! $this->response instanceof NonBufferedResponseInterface
85✔
462
            && ! $this->response instanceof RedirectResponse
85✔
463
        ) {
464
            // Save our current URI as the previous URI in the session
465
            // for safer, more accurate use with `previous_url()` helper function.
466
            $this->storePreviousURL(current_url(true));
83✔
467
        }
468

469
        unset($uri);
85✔
470

471
        return $this->response;
85✔
472
    }
473

474
    /**
475
     * Start the Benchmark
476
     *
477
     * The timer is used to display total script execution both in the
478
     * debug toolbar, and potentially on the displayed page.
479
     *
480
     * @return void
481
     */
482
    protected function startBenchmark()
483
    {
484
        if ($this->startTime === null) {
110✔
485
            $this->startTime = microtime(true);
×
486
        }
487

488
        $this->benchmark = Services::timer();
110✔
489
        $this->benchmark->start('total_execution', $this->startTime);
110✔
490
        $this->benchmark->start('bootstrap');
110✔
491
    }
492

493
    /**
494
     * Sets a Request object to be used for this request.
495
     * Used when running certain tests.
496
     *
497
     * @param CLIRequest|IncomingRequest $request
498
     *
499
     * @return $this
500
     *
501
     * @internal Used for testing purposes only.
502
     * @testTag
503
     */
504
    public function setRequest($request)
505
    {
506
        $this->request = $request;
38✔
507

508
        return $this;
38✔
509
    }
510

511
    /**
512
     * Get our Request object, (either IncomingRequest or CLIRequest).
513
     *
514
     * @return void
515
     */
516
    protected function getRequestObject()
517
    {
518
        if ($this->request instanceof Request) {
110✔
519
            $this->spoofRequestMethod();
40✔
520

521
            return;
40✔
522
        }
523

524
        if ($this->isPhpCli()) {
72✔
525
            Services::createRequest($this->config, true);
×
526
        } else {
527
            Services::createRequest($this->config);
72✔
528
        }
529

530
        $this->request = service('request');
72✔
531

532
        $this->spoofRequestMethod();
72✔
533
    }
534

535
    /**
536
     * Get our Response object, and set some default values, including
537
     * the HTTP protocol version and a default successful response.
538
     *
539
     * @return void
540
     */
541
    protected function getResponseObject()
542
    {
543
        $this->response = Services::response($this->config);
110✔
544

545
        if ($this->isWeb()) {
110✔
546
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
110✔
547
        }
548

549
        // Assume success until proven otherwise.
550
        $this->response->setStatusCode(200);
110✔
551
    }
552

553
    /**
554
     * Returns an array with our basic performance stats collected.
555
     */
556
    public function getPerformanceStats(): array
557
    {
558
        // After filter debug toolbar requires 'total_execution'.
559
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
560

561
        return [
×
562
            'startTime' => $this->startTime,
×
563
            'totalTime' => $this->totalTime,
×
564
        ];
×
565
    }
566

567
    /**
568
     * Try to Route It - As it sounds like, works with the router to
569
     * match a route against the current URI. If the route is a
570
     * "redirect route", will also handle the redirect.
571
     *
572
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
573
     *                                              of the config file.
574
     *
575
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
576
     *
577
     * @throws RedirectException
578
     */
579
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
580
    {
581
        $this->benchmark->start('routing');
108✔
582

583
        if (! $routes instanceof RouteCollectionInterface) {
108✔
584
            $routes = service('routes')->loadRoutes();
48✔
585
        }
586

587
        // $routes is defined in Config/Routes.php
588
        $this->router = Services::router($routes, $this->request);
108✔
589

590
        // $uri is URL-encoded.
591
        $uri = $this->request->getPath();
108✔
592

593
        $this->outputBufferingStart();
108✔
594

595
        $this->controller = $this->router->handle($uri);
108✔
596
        $this->method     = $this->router->methodName();
91✔
597

598
        // If a {locale} segment was matched in the final route,
599
        // then we need to set the correct locale on our Request.
600
        if ($this->router->hasLocale()) {
91✔
601
            $this->request->setLocale($this->router->getLocale());
×
602
        }
603

604
        $this->benchmark->stop('routing');
91✔
605

606
        return $this->router->getFilters();
91✔
607
    }
608

609
    /**
610
     * Now that everything has been setup, this method attempts to run the
611
     * controller method and make the script go. If it's not able to, will
612
     * show the appropriate Page Not Found error.
613
     *
614
     * @return ResponseInterface|string|null
615
     */
616
    protected function startController()
617
    {
618
        $this->benchmark->start('controller');
91✔
619
        $this->benchmark->start('controller_constructor');
91✔
620

621
        // Is it routed to a Closure?
622
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
91✔
623
            $controller = $this->controller;
30✔
624

625
            try {
626
                $resolved = $this->resolveCallableParams(new ReflectionFunction($controller), $this->router->params());
30✔
627

628
                return $controller(...$resolved);
30✔
629
            } finally {
630
                $this->benchmark->stop('controller_constructor');
30✔
631
                $this->benchmark->stop('controller');
30✔
632
            }
633
        }
634

635
        // No controller specified - we don't know what to do now.
636
        if (! isset($this->controller)) {
61✔
637
            throw PageNotFoundException::forEmptyController();
×
638
        }
639

640
        // Try to autoload the class
641
        if (
642
            ! class_exists($this->controller, true)
61✔
643
            || ($this->method[0] === '_' && $this->method !== '__invoke')
61✔
644
        ) {
645
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
646
        }
647

648
        // Execute route attributes' before() methods
649
        // This runs after routing/validation but BEFORE expensive controller instantiation
650
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
61✔
651
            $this->benchmark->start('route_attributes_before');
60✔
652
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
60✔
653
            $this->benchmark->stop('route_attributes_before');
59✔
654

655
            // If attribute returns a Response, short-circuit
656
            if ($attributeResponse instanceof ResponseInterface) {
59✔
657
                $this->benchmark->stop('controller_constructor');
1✔
658
                $this->benchmark->stop('controller');
1✔
659

660
                return $attributeResponse;
1✔
661
            }
662

663
            // If attribute returns a modified Request, use it
664
            if ($attributeResponse instanceof RequestInterface) {
59✔
665
                $this->request = $attributeResponse;
59✔
666
            }
667
        }
668

669
        return null;
60✔
670
    }
671

672
    /**
673
     * Instantiates the controller class.
674
     *
675
     * @return Controller
676
     */
677
    protected function createController()
678
    {
679
        assert(is_string($this->controller));
680

681
        $class = new $this->controller();
62✔
682
        $class->initController($this->request, $this->response, Services::logger());
62✔
683

684
        $this->benchmark->stop('controller_constructor');
62✔
685

686
        return $class;
62✔
687
    }
688

689
    /**
690
     * Runs the controller, allowing for _remap methods to function.
691
     *
692
     * CI4 supports three types of requests:
693
     *  1. Web: URI segments become parameters, sent to Controllers via Routes,
694
     *      output controlled by Headers to browser
695
     *  2. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments,
696
     *      sent to Controllers via Routes, output varies
697
     *
698
     * @param Controller $class
699
     *
700
     * @return false|ResponseInterface|string|void
701
     */
702
    protected function runController($class)
703
    {
704
        // This is a Web request or PHP CLI request
705
        $params = $this->router->params();
59✔
706

707
        // The controller method param types may not be string.
708
        // So cannot set `declare(strict_types=1)` in this file.
709
        try {
710
            if (method_exists($class, '_remap')) {
59✔
711
                // FormRequest injection is not supported for _remap() because its
712
                // signature is fixed to ($method, ...$params). Instantiate the
713
                // FormRequest manually inside _remap() if needed.
714
                $output = $class->_remap($this->method, ...$params);
×
715
            } else {
716
                $resolved = $this->resolveMethodParams($class, $this->method, $params);
59✔
717
                $output   = $class->{$this->method}(...$resolved);
55✔
718
            }
719
        } finally {
720
            $this->benchmark->stop('controller');
59✔
721
        }
722

723
        return $output;
55✔
724
    }
725

726
    /**
727
     * Resolves the final parameter list for a controller method call.
728
     *
729
     * @param list<string> $routeParams URI segments from the router.
730
     *
731
     * @return list<mixed>
732
     */
733
    private function resolveMethodParams(object $class, string $method, array $routeParams): array
734
    {
735
        return $this->resolveCallableParams(new ReflectionMethod($class, $method), $routeParams);
59✔
736
    }
737

738
    /**
739
     * Shared FormRequest resolver for both controller methods and closures.
740
     *
741
     * Builds a sequential positional argument list for the call site.
742
     * The supported signature shape is: required scalar route params first,
743
     * then the FormRequest, then optional scalar params.
744
     *
745
     * - FormRequest subclasses are instantiated, authorized, and validated
746
     *   before being injected.
747
     * - Variadic non-FormRequest parameters consume all remaining URI segments.
748
     * - Scalar non-FormRequest parameters consume one URI segment each.
749
     * - When route segments run out, a required non-FormRequest parameter stops
750
     *   iteration so PHP throws an ArgumentCountError on the call site.
751
     * - Optional non-FormRequest parameters with no remaining segment are omitted
752
     *   from the list; PHP then applies their declared default values.
753
     *
754
     * @param list<string> $routeParams URI segments from the router.
755
     *
756
     * @return list<mixed>
757
     */
758
    private function resolveCallableParams(ReflectionFunctionAbstract $reflection, array $routeParams): array
759
    {
760
        $resolved   = [];
89✔
761
        $routeIndex = 0;
89✔
762

763
        foreach ($reflection->getParameters() as $param) {
89✔
764
            [$kind, $formRequestClass] = CallableParamClassifier::classify($param);
28✔
765

766
            switch ($kind) {
767
                case ParamKind::FormRequest:
28✔
768
                    // Inject FormRequest subclasses regardless of position.
769
                    $resolved[] = $this->resolveFormRequest($formRequestClass);
10✔
770

771
                    continue 2;
6✔
772

773
                case ParamKind::Variadic:
22✔
774
                    // Consume all remaining route segments.
775
                    while (array_key_exists($routeIndex, $routeParams)) {
1✔
776
                        $resolved[] = $routeParams[$routeIndex++];
1✔
777
                    }
778
                    break 2;
1✔
779

780
                case ParamKind::Scalar:
21✔
781
                    // Consume the next route segment if one is available.
782
                    if (array_key_exists($routeIndex, $routeParams)) {
21✔
783
                        $resolved[] = $routeParams[$routeIndex++];
20✔
784

785
                        continue 2;
20✔
786
                    }
787

788
                    // No more route segments. Required params stop iteration so
789
                    // that PHP throws an ArgumentCountError on the call site.
790
                    // Optional params are omitted - PHP then applies their
791
                    // declared default value.
792
                    if (! $param->isOptional()) {
3✔
793
                        break 2;
×
794
                    }
795
            }
796
        }
797

798
        return $resolved;
85✔
799
    }
800

801
    /**
802
     * Instantiates, authorizes, and validates a FormRequest class.
803
     *
804
     * If authorization or validation fails, the FormRequest returns a
805
     * ResponseInterface. The framework wraps it in a FormRequestException
806
     * (which implements ResponsableInterface) so the response is sent
807
     * without reaching the controller method.
808
     *
809
     * @param class-string<FormRequest> $className
810
     */
811
    private function resolveFormRequest(string $className): FormRequest
812
    {
813
        $formRequest = new $className($this->request);
10✔
814
        $response    = $formRequest->resolveRequest();
10✔
815

816
        if ($response !== null) {
10✔
817
            throw new FormRequestException($response);
4✔
818
        }
819

820
        return $formRequest;
6✔
821
    }
822

823
    /**
824
     * Displays a 404 Page Not Found error. If set, will try to
825
     * call the 404Override controller/method that was set in routing config.
826
     *
827
     * @return ResponseInterface|void
828
     */
829
    protected function display404errors(PageNotFoundException $e)
830
    {
831
        $this->response->setStatusCode($e->getCode());
12✔
832

833
        // Is there a 404 Override available?
834
        $override = $this->router->get404Override();
12✔
835

836
        if ($override !== null) {
12✔
837
            $returned = null;
4✔
838

839
            if ($override instanceof Closure) {
4✔
840
                echo $override($e->getMessage());
1✔
841
            } elseif (is_array($override)) {
3✔
842
                $this->benchmark->start('controller');
3✔
843
                $this->benchmark->start('controller_constructor');
3✔
844

845
                $this->controller = $override[0];
3✔
846
                $this->method     = $override[1];
3✔
847

848
                $controller = $this->createController();
3✔
849

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

852
                $this->benchmark->stop('controller');
3✔
853
            }
854

855
            unset($override);
4✔
856

857
            $this->gatherOutput($returned);
4✔
858

859
            return $this->response;
4✔
860
        }
861

862
        $this->outputBufferingEnd();
8✔
863

864
        // Throws new PageNotFoundException and remove exception message on production.
865
        throw PageNotFoundException::forPageNotFound(
8✔
866
            (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : null,
8✔
867
        );
8✔
868
    }
869

870
    /**
871
     * Gathers the script output from the buffer, replaces some execution
872
     * time tag in the output and displays the debug toolbar, if required.
873
     *
874
     * @param ResponseInterface|string|null $returned
875
     *
876
     * @return void
877
     */
878
    protected function gatherOutput($returned = null)
879
    {
880
        $this->output = $this->outputBufferingEnd();
89✔
881

882
        if ($returned instanceof NonBufferedResponseInterface) {
89✔
883
            $this->response = $returned;
1✔
884

885
            return;
1✔
886
        }
887
        // If the controller returned a response object,
888
        // we need to grab the body from it so it can
889
        // be added to anything else that might have been
890
        // echoed already.
891
        // We also need to save the instance locally
892
        // so that any status code changes, etc, take place.
893
        if ($returned instanceof ResponseInterface) {
88✔
894
            $this->response = $returned;
28✔
895
            $returned       = $returned->getBody();
28✔
896
        }
897

898
        if (is_string($returned)) {
88✔
899
            $this->output .= $returned;
80✔
900
        }
901

902
        $this->response->setBody($this->output);
88✔
903
    }
904

905
    /**
906
     * If we have a session object to use, store the current URI
907
     * as the previous URI. This is called just prior to sending the
908
     * response to the client, and will make it available next request.
909
     *
910
     * This helps provider safer, more reliable previous_url() detection.
911
     *
912
     * @param string|URI $uri
913
     *
914
     * @return void
915
     */
916
    public function storePreviousURL($uri)
917
    {
918
        // Ignore CLI requests
919
        if (! $this->isWeb()) {
83✔
920
            return;
×
921
        }
922
        // Ignore AJAX requests
923
        if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
83✔
924
            return;
×
925
        }
926

927
        // Ignore unroutable responses
928
        if ($this->response instanceof NonBufferedResponseInterface || $this->response instanceof RedirectResponse) {
83✔
929
            return;
×
930
        }
931

932
        // Ignore non-HTML responses
933
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
83✔
934
            return;
12✔
935
        }
936

937
        // This is mainly needed during testing...
938
        if (is_string($uri)) {
71✔
939
            $uri = new URI($uri);
×
940
        }
941

942
        if (isset($_SESSION)) {
71✔
943
            session()->set('_ci_previous_url', URI::createURIString(
71✔
944
                $uri->getScheme(),
71✔
945
                $uri->getAuthority(),
71✔
946
                $uri->getPath(),
71✔
947
                $uri->getQuery(),
71✔
948
                $uri->getFragment(),
71✔
949
            ));
71✔
950
        }
951
    }
952

953
    /**
954
     * Modifies the Request Object to use a different method if a POST
955
     * variable called _method is found.
956
     *
957
     * @return void
958
     */
959
    public function spoofRequestMethod()
960
    {
961
        // Only works with POSTED forms
962
        if ($this->request->getMethod() !== Method::POST) {
110✔
963
            return;
85✔
964
        }
965

966
        $method = $this->request->getPost('_method');
26✔
967

968
        if ($method === null) {
26✔
969
            return;
24✔
970
        }
971

972
        // Only allows PUT, PATCH, DELETE
973
        if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
2✔
974
            $this->request = $this->request->setMethod($method);
1✔
975
        }
976
    }
977

978
    /**
979
     * Sends the output of this request back to the client.
980
     * This is what they've been waiting for!
981
     *
982
     * @return void
983
     */
984
    protected function sendResponse()
985
    {
986
        $this->response->send();
55✔
987
    }
988

989
    /**
990
     * Sets the app context.
991
     *
992
     * @param 'php-cli'|'web' $context
993
     *
994
     * @return $this
995
     */
996
    public function setContext(string $context)
997
    {
998
        $this->context = $context;
39✔
999

1000
        return $this;
39✔
1001
    }
1002

1003
    protected function outputBufferingStart(): void
1004
    {
1005
        $this->bufferLevel = ob_get_level();
108✔
1006
        ob_start();
108✔
1007
    }
1008

1009
    protected function outputBufferingEnd(): string
1010
    {
1011
        $buffer = '';
108✔
1012

1013
        while (ob_get_level() > $this->bufferLevel) {
108✔
1014
            $buffer .= ob_get_contents();
108✔
1015
            ob_end_clean();
108✔
1016
        }
1017

1018
        return $buffer;
108✔
1019
    }
1020
}
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