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

codeigniter4 / CodeIgniter4 / 24312698318

12 Apr 2026 05:50PM UTC coverage: 88.242% (+0.004%) from 88.238%
24312698318

Pull #10087

github

web-flow
Merge 0ae2d370d into 399983bff
Pull Request #10087: feat: add `FormRequest` for encapsulating validation and authorization

72 of 81 new or added lines in 6 files covered. (88.89%)

21 existing lines in 2 files now uncovered.

22702 of 25727 relevant lines covered (88.24%)

221.49 hits per line

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

90.67
/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\RouteCollectionInterface;
35
use CodeIgniter\Router\Router;
36
use Config\App;
37
use Config\Cache;
38
use Config\Feature;
39
use Config\Services;
40
use Locale;
41
use ReflectionFunction;
42
use ReflectionFunctionAbstract;
43
use ReflectionMethod;
44
use Throwable;
45

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

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

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

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

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

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

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

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

109
    /**
110
     * Controller to use.
111
     *
112
     * @var Closure|string|null
113
     */
114
    protected $controller;
115

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

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

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

139
    /**
140
     * Whether to enable Control Filters.
141
     */
142
    protected bool $enableFilters = true;
143

144
    /**
145
     * Application output buffering level
146
     */
147
    protected int $bufferLevel;
148

149
    /**
150
     * Web Page Caching
151
     */
152
    protected ResponseCache $pageCache;
153

154
    /**
155
     * Constructor.
156
     */
157
    public function __construct(App $config)
158
    {
159
        $this->startTime = microtime(true);
7,480✔
160
        $this->config    = $config;
7,480✔
161

162
        $this->pageCache = Services::responsecache();
7,480✔
163
    }
164

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

175
        // Set default timezone on the server
176
        date_default_timezone_set($this->config->appTimezone);
7,480✔
177
    }
178

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

192
        // Reset timing
193
        $this->startTime = null;
1✔
194
        $this->totalTime = 0;
1✔
195
    }
196

197
    /**
198
     * Launch the application!
199
     *
200
     * This is "the loop" if you will. The main entry point into the script
201
     * that gets the required class instances, fires off the filters,
202
     * tries to route the response, loads the controller and generally
203
     * makes all the pieces work together.
204
     *
205
     * @param bool $returnResponse Used for testing purposes only.
206
     *
207
     * @return ResponseInterface|null
208
     */
209
    public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false)
210
    {
211
        if ($this->context === null) {
110✔
UNCOV
212
            throw new LogicException(
×
UNCOV
213
                'Context must be set before run() is called. If you are upgrading from 4.1.x, '
×
214
                . 'you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.',
×
215
            );
×
216
        }
217

218
        $this->pageCache->setTtl(0);
110✔
219
        $this->bufferLevel = ob_get_level();
110✔
220

221
        $this->startBenchmark();
110✔
222

223
        $this->getRequestObject();
110✔
224
        $this->getResponseObject();
110✔
225

226
        Events::trigger('pre_system');
110✔
227

228
        $this->benchmark->stop('bootstrap');
110✔
229

230
        $this->benchmark->start('required_before_filters');
110✔
231
        // Start up the filters
232
        $filters = Services::filters();
110✔
233
        // Run required before filters
234
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
110✔
235

236
        // If a ResponseInterface instance is returned then send it back to the client and stop
237
        if ($possibleResponse instanceof ResponseInterface) {
110✔
238
            $this->response = $possibleResponse;
4✔
239
        } else {
240
            try {
241
                $this->response = $this->handleRequest($routes);
109✔
242
            } catch (ResponsableInterface $e) {
22✔
243
                $this->outputBufferingEnd();
10✔
244

245
                $this->response = $e->getResponse();
10✔
246
            } catch (PageNotFoundException $e) {
12✔
247
                $this->response = $this->display404errors($e);
12✔
UNCOV
248
            } catch (Throwable $e) {
×
UNCOV
249
                $this->outputBufferingEnd();
×
250

251
                throw $e;
×
252
            }
253
        }
254

255
        $this->runRequiredAfterFilters($filters);
102✔
256

257
        // Is there a post-system event?
258
        Events::trigger('post_system');
102✔
259

260
        if ($returnResponse) {
102✔
261
            return $this->response;
47✔
262
        }
263

264
        $this->sendResponse();
55✔
265

266
        return null;
55✔
267
    }
268

269
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
270
    {
271
        $possibleResponse = $filters->runRequired('before');
110✔
272
        $this->benchmark->stop('required_before_filters');
110✔
273

274
        // If a ResponseInterface instance is returned then send it back to the client and stop
275
        if ($possibleResponse instanceof ResponseInterface) {
110✔
276
            return $possibleResponse;
4✔
277
        }
278

279
        return null;
109✔
280
    }
281

282
    private function runRequiredAfterFilters(Filters $filters): void
283
    {
284
        $filters->setResponse($this->response);
102✔
285

286
        $this->benchmark->start('required_after_filters');
102✔
287
        $response = $filters->runRequired('after');
102✔
288
        $this->benchmark->stop('required_after_filters');
102✔
289

290
        if ($response instanceof ResponseInterface) {
102✔
291
            $this->response = $response;
102✔
292
        }
293
    }
294

295
    /**
296
     * Invoked via php-cli command?
297
     */
298
    private function isPhpCli(): bool
299
    {
300
        return $this->context === 'php-cli';
72✔
301
    }
302

303
    /**
304
     * Web access?
305
     */
306
    private function isWeb(): bool
307
    {
308
        return $this->context === 'web';
110✔
309
    }
310

311
    /**
312
     * Disables Controller Filters.
313
     */
314
    public function disableFilters(): void
315
    {
316
        $this->enableFilters = false;
1✔
317
    }
318

319
    /**
320
     * Handles the main request logic and fires the controller.
321
     *
322
     * @return ResponseInterface
323
     *
324
     * @throws PageNotFoundException
325
     * @throws RedirectException
326
     */
327
    protected function handleRequest(?RouteCollectionInterface $routes, ?Cache $cacheConfig = null)
328
    {
329
        if (func_num_args() > 1) {
109✔
330
            // @todo v4.8.0: Remove this check and the $cacheConfig parameter from the method signature.
UNCOV
331
            @trigger_error(sprintf('Since v4.8.0, the $cacheConfig parameter of %s is deprecated and no longer used.', __METHOD__), E_USER_DEPRECATED);
×
332
        }
333

334
        if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
109✔
335
            return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
1✔
336
        }
337

338
        $routeFilters = $this->tryToRouteIt($routes);
108✔
339

340
        // $uri is URL-encoded.
341
        $uri = $this->request->getPath();
91✔
342

343
        if ($this->enableFilters) {
91✔
344
            /** @var Filters $filters */
345
            $filters = service('filters');
90✔
346

347
            // If any filters were specified within the routes file,
348
            // we need to ensure it's active for the current request
349
            if ($routeFilters !== null) {
90✔
350
                $filters->enableFilters($routeFilters, 'before');
90✔
351

352
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property
90✔
353
                if (! $oldFilterOrder) {
90✔
354
                    $routeFilters = array_reverse($routeFilters);
90✔
355
                }
356

357
                $filters->enableFilters($routeFilters, 'after');
90✔
358
            }
359

360
            // Run "before" filters
361
            $this->benchmark->start('before_filters');
90✔
362
            $possibleResponse = $filters->run($uri, 'before');
90✔
363
            $this->benchmark->stop('before_filters');
90✔
364

365
            // If a ResponseInterface instance is returned then send it back to the client and stop
366
            if ($possibleResponse instanceof ResponseInterface) {
90✔
367
                $this->outputBufferingEnd();
1✔
368

369
                return $possibleResponse;
1✔
370
            }
371

372
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
89✔
373
                $this->request = $possibleResponse;
89✔
374
            }
375
        }
376

377
        $returned = $this->startController();
90✔
378

379
        // If startController returned a Response (from an attribute or Closure), use it
380
        if ($returned instanceof ResponseInterface) {
89✔
381
            $this->gatherOutput($returned);
8✔
382
        }
383
        // Closure controller has run in startController().
384
        elseif (! is_callable($this->controller)) {
82✔
385
            $controller = $this->createController();
59✔
386

387
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
59✔
UNCOV
388
                throw PageNotFoundException::forMethodNotFound($this->method);
×
389
            }
390

391
            // Is there a "post_controller_constructor" event?
392
            Events::trigger('post_controller_constructor');
59✔
393

394
            $returned = $this->runController($controller);
59✔
395
        } else {
396
            $this->benchmark->stop('controller_constructor');
23✔
397
            $this->benchmark->stop('controller');
23✔
398
        }
399

400
        // If $returned is a string, then the controller output something,
401
        // probably a view, instead of echoing it directly. Send it along
402
        // so it can be used with the output.
403
        $this->gatherOutput($returned);
85✔
404

405
        if ($this->enableFilters) {
85✔
406
            /** @var Filters $filters */
407
            $filters = service('filters');
84✔
408
            $filters->setResponse($this->response);
84✔
409

410
            // Run "after" filters
411
            $this->benchmark->start('after_filters');
84✔
412
            $response = $filters->run($uri, 'after');
84✔
413
            $this->benchmark->stop('after_filters');
84✔
414

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

420
        // Execute controller attributes' after() methods AFTER framework filters
421
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
85✔
422
            $this->benchmark->start('route_attributes_after');
84✔
423
            $this->response = $this->router->executeAfterAttributes($this->request, $this->response);
84✔
424
            $this->benchmark->stop('route_attributes_after');
84✔
425
        }
426

427
        // Skip unnecessary processing for special Responses.
428
        if (
429
            ! $this->response instanceof NonBufferedResponseInterface
85✔
430
            && ! $this->response instanceof RedirectResponse
85✔
431
        ) {
432
            // Save our current URI as the previous URI in the session
433
            // for safer, more accurate use with `previous_url()` helper function.
434
            $this->storePreviousURL(current_url(true));
83✔
435
        }
436

437
        unset($uri);
85✔
438

439
        return $this->response;
85✔
440
    }
441

442
    /**
443
     * Start the Benchmark
444
     *
445
     * The timer is used to display total script execution both in the
446
     * debug toolbar, and potentially on the displayed page.
447
     *
448
     * @return void
449
     */
450
    protected function startBenchmark()
451
    {
452
        if ($this->startTime === null) {
110✔
453
            $this->startTime = microtime(true);
×
454
        }
455

456
        $this->benchmark = Services::timer();
110✔
457
        $this->benchmark->start('total_execution', $this->startTime);
110✔
458
        $this->benchmark->start('bootstrap');
110✔
459
    }
460

461
    /**
462
     * Sets a Request object to be used for this request.
463
     * Used when running certain tests.
464
     *
465
     * @param CLIRequest|IncomingRequest $request
466
     *
467
     * @return $this
468
     *
469
     * @internal Used for testing purposes only.
470
     * @testTag
471
     */
472
    public function setRequest($request)
473
    {
474
        $this->request = $request;
38✔
475

476
        return $this;
38✔
477
    }
478

479
    /**
480
     * Get our Request object, (either IncomingRequest or CLIRequest).
481
     *
482
     * @return void
483
     */
484
    protected function getRequestObject()
485
    {
486
        if ($this->request instanceof Request) {
110✔
487
            $this->spoofRequestMethod();
40✔
488

489
            return;
40✔
490
        }
491

492
        if ($this->isPhpCli()) {
72✔
493
            Services::createRequest($this->config, true);
×
494
        } else {
495
            Services::createRequest($this->config);
72✔
496
        }
497

498
        $this->request = service('request');
72✔
499

500
        $this->spoofRequestMethod();
72✔
501
    }
502

503
    /**
504
     * Get our Response object, and set some default values, including
505
     * the HTTP protocol version and a default successful response.
506
     *
507
     * @return void
508
     */
509
    protected function getResponseObject()
510
    {
511
        $this->response = Services::response($this->config);
110✔
512

513
        if ($this->isWeb()) {
110✔
514
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
110✔
515
        }
516

517
        // Assume success until proven otherwise.
518
        $this->response->setStatusCode(200);
110✔
519
    }
520

521
    /**
522
     * Returns an array with our basic performance stats collected.
523
     */
524
    public function getPerformanceStats(): array
525
    {
526
        // After filter debug toolbar requires 'total_execution'.
527
        $this->totalTime = $this->benchmark->getElapsedTime('total_execution');
×
528

529
        return [
×
530
            'startTime' => $this->startTime,
×
531
            'totalTime' => $this->totalTime,
×
532
        ];
×
533
    }
534

535
    /**
536
     * Try to Route It - As it sounds like, works with the router to
537
     * match a route against the current URI. If the route is a
538
     * "redirect route", will also handle the redirect.
539
     *
540
     * @param RouteCollectionInterface|null $routes A collection interface to use in place
541
     *                                              of the config file.
542
     *
543
     * @return list<string>|string|null Route filters, that is, the filters specified in the routes file
544
     *
545
     * @throws RedirectException
546
     */
547
    protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
548
    {
549
        $this->benchmark->start('routing');
108✔
550

551
        if (! $routes instanceof RouteCollectionInterface) {
108✔
552
            $routes = service('routes')->loadRoutes();
48✔
553
        }
554

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

558
        // $uri is URL-encoded.
559
        $uri = $this->request->getPath();
108✔
560

561
        $this->outputBufferingStart();
108✔
562

563
        $this->controller = $this->router->handle($uri);
108✔
564
        $this->method     = $this->router->methodName();
91✔
565

566
        // If a {locale} segment was matched in the final route,
567
        // then we need to set the correct locale on our Request.
568
        if ($this->router->hasLocale()) {
91✔
569
            $this->request->setLocale($this->router->getLocale());
×
570
        }
571

572
        $this->benchmark->stop('routing');
91✔
573

574
        return $this->router->getFilters();
91✔
575
    }
576

577
    /**
578
     * Now that everything has been setup, this method attempts to run the
579
     * controller method and make the script go. If it's not able to, will
580
     * show the appropriate Page Not Found error.
581
     *
582
     * @return ResponseInterface|string|null
583
     */
584
    protected function startController()
585
    {
586
        $this->benchmark->start('controller');
91✔
587
        $this->benchmark->start('controller_constructor');
91✔
588

589
        // Is it routed to a Closure?
590
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
91✔
591
            $controller = $this->controller;
30✔
592
            $resolved   = $this->resolveCallableParams(new ReflectionFunction($controller), $this->router->params());
30✔
593

594
            return $controller(...$resolved);
30✔
595
        }
596

597
        // No controller specified - we don't know what to do now.
598
        if (! isset($this->controller)) {
61✔
599
            throw PageNotFoundException::forEmptyController();
×
600
        }
601

602
        // Try to autoload the class
603
        if (
604
            ! class_exists($this->controller, true)
61✔
605
            || ($this->method[0] === '_' && $this->method !== '__invoke')
61✔
606
        ) {
UNCOV
607
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
608
        }
609

610
        // Execute route attributes' before() methods
611
        // This runs after routing/validation but BEFORE expensive controller instantiation
612
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
61✔
613
            $this->benchmark->start('route_attributes_before');
60✔
614
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
60✔
615
            $this->benchmark->stop('route_attributes_before');
59✔
616

617
            // If attribute returns a Response, short-circuit
618
            if ($attributeResponse instanceof ResponseInterface) {
59✔
619
                $this->benchmark->stop('controller_constructor');
1✔
620
                $this->benchmark->stop('controller');
1✔
621

622
                return $attributeResponse;
1✔
623
            }
624

625
            // If attribute returns a modified Request, use it
626
            if ($attributeResponse instanceof RequestInterface) {
59✔
627
                $this->request = $attributeResponse;
59✔
628
            }
629
        }
630

631
        return null;
60✔
632
    }
633

634
    /**
635
     * Instantiates the controller class.
636
     *
637
     * @return Controller
638
     */
639
    protected function createController()
640
    {
641
        assert(is_string($this->controller));
642

643
        $class = new $this->controller();
62✔
644
        $class->initController($this->request, $this->response, Services::logger());
62✔
645

646
        $this->benchmark->stop('controller_constructor');
62✔
647

648
        return $class;
62✔
649
    }
650

651
    /**
652
     * Runs the controller, allowing for _remap methods to function.
653
     *
654
     * CI4 supports three types of requests:
655
     *  1. Web: URI segments become parameters, sent to Controllers via Routes,
656
     *      output controlled by Headers to browser
657
     *  2. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments,
658
     *      sent to Controllers via Routes, output varies
659
     *
660
     * @param Controller $class
661
     *
662
     * @return false|ResponseInterface|string|void
663
     */
664
    protected function runController($class)
665
    {
666
        // This is a Web request or PHP CLI request
667
        $params = $this->router->params();
59✔
668

669
        // The controller method param types may not be string.
670
        // So cannot set `declare(strict_types=1)` in this file.
671
        if (method_exists($class, '_remap')) {
59✔
672
            // FormRequest injection is not supported for _remap() because its
673
            // signature is fixed to ($method, ...$params). Instantiate the
674
            // FormRequest manually inside _remap() if needed.
NEW
UNCOV
675
            $output = $class->_remap($this->method, ...$params);
×
676
        } else {
677
            $resolved = $this->resolveMethodParams($class, $this->method, $params);
59✔
678
            $output   = $class->{$this->method}(...$resolved);
55✔
679
        }
680

681
        $this->benchmark->stop('controller');
55✔
682

683
        return $output;
55✔
684
    }
685

686
    /**
687
     * Resolves the final parameter list for a controller method call.
688
     *
689
     * @param list<string> $routeParams URI segments from the router.
690
     *
691
     * @return list<mixed>
692
     */
693
    private function resolveMethodParams(object $class, string $method, array $routeParams): array
694
    {
695
        return $this->resolveCallableParams(new ReflectionMethod($class, $method), $routeParams);
59✔
696
    }
697

698
    /**
699
     * Shared FormRequest resolver for both controller methods and closures.
700
     *
701
     * Builds a sequential positional argument list for the call site.
702
     * The supported signature shape is: required scalar route params first,
703
     * then the FormRequest, then optional scalar params.
704
     *
705
     * - FormRequest subclasses are instantiated, authorized, and validated
706
     *   before being injected.
707
     * - Variadic non-FormRequest parameters consume all remaining URI segments.
708
     * - Scalar non-FormRequest parameters consume one URI segment each.
709
     * - When route segments run out, a required non-FormRequest parameter stops
710
     *   iteration so PHP throws an ArgumentCountError on the call site.
711
     * - Optional non-FormRequest parameters with no remaining segment are omitted
712
     *   from the list; PHP then applies their declared default values.
713
     *
714
     * @param list<string> $routeParams URI segments from the router.
715
     *
716
     * @return list<mixed>
717
     */
718
    private function resolveCallableParams(ReflectionFunctionAbstract $reflection, array $routeParams): array
719
    {
720
        $resolved   = [];
89✔
721
        $routeIndex = 0;
89✔
722

723
        foreach ($reflection->getParameters() as $param) {
89✔
724
            // Inject FormRequest subclasses regardless of position.
725
            $formRequestClass = FormRequest::getFormRequestClass($param);
28✔
726

727
            if ($formRequestClass !== null) {
28✔
728
                $resolved[] = $this->resolveFormRequest($formRequestClass);
10✔
729

730
                continue;
6✔
731
            }
732

733
            // Variadic parameter - consume all remaining route segments.
734
            if ($param->isVariadic()) {
22✔
735
                while (array_key_exists($routeIndex, $routeParams)) {
1✔
736
                    $resolved[] = $routeParams[$routeIndex++];
1✔
737
                }
738

739
                break;
1✔
740
            }
741

742
            // Consume the next route segment if one is available.
743
            if (array_key_exists($routeIndex, $routeParams)) {
21✔
744
                $resolved[] = $routeParams[$routeIndex++];
20✔
745

746
                continue;
20✔
747
            }
748

749
            // No more route segments. Required params stop iteration so that
750
            // PHP throws an ArgumentCountError on the call site. Optional
751
            // params are omitted - PHP then applies their declared default value.
752
            if (! $param->isOptional()) {
3✔
NEW
753
                break;
×
754
            }
755
        }
756

757
        return $resolved;
85✔
758
    }
759

760
    /**
761
     * Instantiates, authorizes, and validates a FormRequest class.
762
     *
763
     * If authorization or validation fails, the FormRequest returns a
764
     * ResponseInterface. The framework wraps it in a FormRequestException
765
     * (which implements ResponsableInterface) so the response is sent
766
     * without reaching the controller method.
767
     *
768
     * @param class-string<FormRequest> $className
769
     */
770
    private function resolveFormRequest(string $className): FormRequest
771
    {
772
        $formRequest = new $className($this->request);
10✔
773
        $response    = $formRequest->resolveRequest();
10✔
774

775
        if ($response !== null) {
10✔
776
            throw new FormRequestException($response);
4✔
777
        }
778

779
        return $formRequest;
6✔
780
    }
781

782
    /**
783
     * Displays a 404 Page Not Found error. If set, will try to
784
     * call the 404Override controller/method that was set in routing config.
785
     *
786
     * @return ResponseInterface|void
787
     */
788
    protected function display404errors(PageNotFoundException $e)
789
    {
790
        $this->response->setStatusCode($e->getCode());
12✔
791

792
        // Is there a 404 Override available?
793
        $override = $this->router->get404Override();
12✔
794

795
        if ($override !== null) {
12✔
796
            $returned = null;
4✔
797

798
            if ($override instanceof Closure) {
4✔
799
                echo $override($e->getMessage());
1✔
800
            } elseif (is_array($override)) {
3✔
801
                $this->benchmark->start('controller');
3✔
802
                $this->benchmark->start('controller_constructor');
3✔
803

804
                $this->controller = $override[0];
3✔
805
                $this->method     = $override[1];
3✔
806

807
                $controller = $this->createController();
3✔
808

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

811
                $this->benchmark->stop('controller');
3✔
812
            }
813

814
            unset($override);
4✔
815

816
            $this->gatherOutput($returned);
4✔
817

818
            return $this->response;
4✔
819
        }
820

821
        $this->outputBufferingEnd();
8✔
822

823
        // Throws new PageNotFoundException and remove exception message on production.
824
        throw PageNotFoundException::forPageNotFound(
8✔
825
            (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : null,
8✔
826
        );
8✔
827
    }
828

829
    /**
830
     * Gathers the script output from the buffer, replaces some execution
831
     * time tag in the output and displays the debug toolbar, if required.
832
     *
833
     * @param ResponseInterface|string|null $returned
834
     *
835
     * @return void
836
     */
837
    protected function gatherOutput($returned = null)
838
    {
839
        $this->output = $this->outputBufferingEnd();
89✔
840

841
        if ($returned instanceof NonBufferedResponseInterface) {
89✔
842
            $this->response = $returned;
1✔
843

844
            return;
1✔
845
        }
846
        // If the controller returned a response object,
847
        // we need to grab the body from it so it can
848
        // be added to anything else that might have been
849
        // echoed already.
850
        // We also need to save the instance locally
851
        // so that any status code changes, etc, take place.
852
        if ($returned instanceof ResponseInterface) {
88✔
853
            $this->response = $returned;
28✔
854
            $returned       = $returned->getBody();
28✔
855
        }
856

857
        if (is_string($returned)) {
88✔
858
            $this->output .= $returned;
80✔
859
        }
860

861
        $this->response->setBody($this->output);
88✔
862
    }
863

864
    /**
865
     * If we have a session object to use, store the current URI
866
     * as the previous URI. This is called just prior to sending the
867
     * response to the client, and will make it available next request.
868
     *
869
     * This helps provider safer, more reliable previous_url() detection.
870
     *
871
     * @param string|URI $uri
872
     *
873
     * @return void
874
     */
875
    public function storePreviousURL($uri)
876
    {
877
        // Ignore CLI requests
878
        if (! $this->isWeb()) {
83✔
UNCOV
879
            return;
×
880
        }
881
        // Ignore AJAX requests
882
        if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
83✔
UNCOV
883
            return;
×
884
        }
885

886
        // Ignore unroutable responses
887
        if ($this->response instanceof NonBufferedResponseInterface || $this->response instanceof RedirectResponse) {
83✔
888
            return;
×
889
        }
890

891
        // Ignore non-HTML responses
892
        if (! str_contains($this->response->getHeaderLine('Content-Type'), 'text/html')) {
83✔
893
            return;
12✔
894
        }
895

896
        // This is mainly needed during testing...
897
        if (is_string($uri)) {
71✔
UNCOV
898
            $uri = new URI($uri);
×
899
        }
900

901
        if (isset($_SESSION)) {
71✔
902
            session()->set('_ci_previous_url', URI::createURIString(
71✔
903
                $uri->getScheme(),
71✔
904
                $uri->getAuthority(),
71✔
905
                $uri->getPath(),
71✔
906
                $uri->getQuery(),
71✔
907
                $uri->getFragment(),
71✔
908
            ));
71✔
909
        }
910
    }
911

912
    /**
913
     * Modifies the Request Object to use a different method if a POST
914
     * variable called _method is found.
915
     *
916
     * @return void
917
     */
918
    public function spoofRequestMethod()
919
    {
920
        // Only works with POSTED forms
921
        if ($this->request->getMethod() !== Method::POST) {
110✔
922
            return;
85✔
923
        }
924

925
        $method = $this->request->getPost('_method');
26✔
926

927
        if ($method === null) {
26✔
928
            return;
24✔
929
        }
930

931
        // Only allows PUT, PATCH, DELETE
932
        if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
2✔
933
            $this->request = $this->request->setMethod($method);
1✔
934
        }
935
    }
936

937
    /**
938
     * Sends the output of this request back to the client.
939
     * This is what they've been waiting for!
940
     *
941
     * @return void
942
     */
943
    protected function sendResponse()
944
    {
945
        $this->response->send();
55✔
946
    }
947

948
    /**
949
     * Sets the app context.
950
     *
951
     * @param 'php-cli'|'web' $context
952
     *
953
     * @return $this
954
     */
955
    public function setContext(string $context)
956
    {
957
        $this->context = $context;
39✔
958

959
        return $this;
39✔
960
    }
961

962
    protected function outputBufferingStart(): void
963
    {
964
        $this->bufferLevel = ob_get_level();
108✔
965
        ob_start();
108✔
966
    }
967

968
    protected function outputBufferingEnd(): string
969
    {
970
        $buffer = '';
108✔
971

972
        while (ob_get_level() > $this->bufferLevel) {
108✔
973
            $buffer .= ob_get_contents();
108✔
974
            ob_end_clean();
108✔
975
        }
976

977
        return $buffer;
108✔
978
    }
979
}
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