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

codeigniter4 / CodeIgniter4 / 24779314118

22 Apr 2026 12:52PM UTC coverage: 88.221% (-0.05%) from 88.268%
24779314118

Pull #10128

github

web-flow
Merge 4ac7efc19 into 5f8974055
Pull Request #10128: feat: Add Query, Header and Cookie attributes for direct Controller injection

3 of 17 new or added lines in 2 files covered. (17.65%)

22799 of 25843 relevant lines covered (88.22%)

220.37 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

150
    /**
151
     * Application output buffering level
152
     */
153
    protected int $bufferLevel;
154

155
    /**
156
     * Web Page Caching
157
     */
158
    protected ResponseCache $pageCache;
159

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

168
        $this->pageCache = Services::responsecache();
7,492✔
169
    }
170

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

181
        // Set default timezone on the server
182
        date_default_timezone_set($this->config->appTimezone);
7,492✔
183
    }
184

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

198
        // Reset timing
199
        $this->startTime = null;
1✔
200
        $this->totalTime = 0;
1✔
201
    }
202

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

224
        $this->pageCache->setTtl(0);
110✔
225
        $this->bufferLevel = ob_get_level();
110✔
226

227
        $this->startBenchmark();
110✔
228

229
        $this->getRequestObject();
110✔
230
        $this->getResponseObject();
110✔
231

232
        Events::trigger('pre_system');
110✔
233

234
        $this->benchmark->stop('bootstrap');
110✔
235

236
        $this->benchmark->start('required_before_filters');
110✔
237
        // Start up the filters
238
        $filters = Services::filters();
110✔
239
        // Run required before filters
240
        $possibleResponse = $this->runRequiredBeforeFilters($filters);
110✔
241

242
        // If a ResponseInterface instance is returned then send it back to the client and stop
243
        if ($possibleResponse instanceof ResponseInterface) {
110✔
244
            $this->response = $possibleResponse;
4✔
245
        } else {
246
            try {
247
                $this->response = $this->handleRequest($routes);
109✔
248
            } catch (ResponsableInterface $e) {
22✔
249
                $this->outputBufferingEnd();
10✔
250

251
                $this->response = $e->getResponse();
10✔
252
            } catch (PageNotFoundException $e) {
12✔
253
                $this->response = $this->display404errors($e);
12✔
254
            } catch (Throwable $e) {
×
255
                $this->outputBufferingEnd();
×
256

257
                throw $e;
×
258
            }
259
        }
260

261
        $this->runRequiredAfterFilters($filters);
102✔
262

263
        // Is there a post-system event?
264
        Events::trigger('post_system');
102✔
265

266
        if ($returnResponse) {
102✔
267
            return $this->response;
47✔
268
        }
269

270
        $this->sendResponse();
55✔
271

272
        return null;
55✔
273
    }
274

275
    private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface
276
    {
277
        $possibleResponse = $filters->runRequired('before');
110✔
278
        $this->benchmark->stop('required_before_filters');
110✔
279

280
        // If a ResponseInterface instance is returned then send it back to the client and stop
281
        if ($possibleResponse instanceof ResponseInterface) {
110✔
282
            return $possibleResponse;
4✔
283
        }
284

285
        return null;
109✔
286
    }
287

288
    private function runRequiredAfterFilters(Filters $filters): void
289
    {
290
        $filters->setResponse($this->response);
102✔
291

292
        $this->benchmark->start('required_after_filters');
102✔
293
        $response = $filters->runRequired('after');
102✔
294
        $this->benchmark->stop('required_after_filters');
102✔
295

296
        if ($response instanceof ResponseInterface) {
102✔
297
            $this->response = $response;
102✔
298
        }
299
    }
300

301
    /**
302
     * Invoked via php-cli command?
303
     */
304
    private function isPhpCli(): bool
305
    {
306
        return $this->context === 'php-cli';
72✔
307
    }
308

309
    /**
310
     * Web access?
311
     */
312
    private function isWeb(): bool
313
    {
314
        return $this->context === 'web';
110✔
315
    }
316

317
    /**
318
     * Disables Controller Filters.
319
     */
320
    public function disableFilters(): void
321
    {
322
        $this->enableFilters = false;
1✔
323
    }
324

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

340
        if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'CLI') {
109✔
341
            return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
1✔
342
        }
343

344
        $routeFilters = $this->tryToRouteIt($routes);
108✔
345

346
        // $uri is URL-encoded.
347
        $uri = $this->request->getPath();
91✔
348

349
        if ($this->enableFilters) {
91✔
350
            /** @var Filters $filters */
351
            $filters = service('filters');
90✔
352

353
            // If any filters were specified within the routes file,
354
            // we need to ensure it's active for the current request
355
            if ($routeFilters !== null) {
90✔
356
                $filters->enableFilters($routeFilters, 'before');
90✔
357

358
                $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property
90✔
359
                if (! $oldFilterOrder) {
90✔
360
                    $routeFilters = array_reverse($routeFilters);
90✔
361
                }
362

363
                $filters->enableFilters($routeFilters, 'after');
90✔
364
            }
365

366
            // Run "before" filters
367
            $this->benchmark->start('before_filters');
90✔
368
            $possibleResponse = $filters->run($uri, 'before');
90✔
369
            $this->benchmark->stop('before_filters');
90✔
370

371
            // If a ResponseInterface instance is returned then send it back to the client and stop
372
            if ($possibleResponse instanceof ResponseInterface) {
90✔
373
                $this->outputBufferingEnd();
1✔
374

375
                return $possibleResponse;
1✔
376
            }
377

378
            if ($possibleResponse instanceof IncomingRequest || $possibleResponse instanceof CLIRequest) {
89✔
379
                $this->request = $possibleResponse;
89✔
380
            }
381
        }
382

383
        $returned = $this->startController();
90✔
384

385
        // If startController returned a Response (from an attribute or Closure), use it
386
        if ($returned instanceof ResponseInterface) {
89✔
387
            $this->gatherOutput($returned);
8✔
388
        }
389
        // Closure controller has run in startController() - benchmarks were
390
        // stopped there as well.
391
        elseif (! is_callable($this->controller)) {
82✔
392
            $controller = $this->createController();
59✔
393

394
            if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
59✔
395
                throw PageNotFoundException::forMethodNotFound($this->method);
×
396
            }
397

398
            // Is there a "post_controller_constructor" event?
399
            Events::trigger('post_controller_constructor');
59✔
400

401
            $returned = $this->runController($controller);
59✔
402
        }
403

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

409
        if ($this->enableFilters) {
85✔
410
            /** @var Filters $filters */
411
            $filters = service('filters');
84✔
412
            $filters->setResponse($this->response);
84✔
413

414
            // Run "after" filters
415
            $this->benchmark->start('after_filters');
84✔
416
            $response = $filters->run($uri, 'after');
84✔
417
            $this->benchmark->stop('after_filters');
84✔
418

419
            if ($response instanceof ResponseInterface) {
84✔
420
                $this->response = $response;
84✔
421
            }
422
        }
423

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

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

441
        unset($uri);
85✔
442

443
        return $this->response;
85✔
444
    }
445

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

460
        $this->benchmark = Services::timer();
110✔
461
        $this->benchmark->start('total_execution', $this->startTime);
110✔
462
        $this->benchmark->start('bootstrap');
110✔
463
    }
464

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

480
        return $this;
38✔
481
    }
482

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

493
            return;
40✔
494
        }
495

496
        if ($this->isPhpCli()) {
72✔
497
            Services::createRequest($this->config, true);
×
498
        } else {
499
            Services::createRequest($this->config);
72✔
500
        }
501

502
        $this->request = service('request');
72✔
503

504
        $this->spoofRequestMethod();
72✔
505
    }
506

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

517
        if ($this->isWeb()) {
110✔
518
            $this->response->setProtocolVersion($this->request->getProtocolVersion());
110✔
519
        }
520

521
        // Assume success until proven otherwise.
522
        $this->response->setStatusCode(200);
110✔
523
    }
524

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

533
        return [
×
534
            'startTime' => $this->startTime,
×
535
            'totalTime' => $this->totalTime,
×
536
        ];
×
537
    }
538

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

555
        if (! $routes instanceof RouteCollectionInterface) {
108✔
556
            $routes = service('routes')->loadRoutes();
48✔
557
        }
558

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

562
        // $uri is URL-encoded.
563
        $uri = $this->request->getPath();
108✔
564

565
        $this->outputBufferingStart();
108✔
566

567
        $this->controller = $this->router->handle($uri);
108✔
568
        $this->method     = $this->router->methodName();
91✔
569

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

576
        $this->benchmark->stop('routing');
91✔
577

578
        return $this->router->getFilters();
91✔
579
    }
580

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

593
        // Is it routed to a Closure?
594
        if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
91✔
595
            $controller = $this->controller;
30✔
596

597
            try {
598
                $resolved = $this->resolveCallableParams(new ReflectionFunction($controller), $this->router->params());
30✔
599

600
                return $controller(...$resolved);
30✔
601
            } finally {
602
                $this->benchmark->stop('controller_constructor');
30✔
603
                $this->benchmark->stop('controller');
30✔
604
            }
605
        }
606

607
        // No controller specified - we don't know what to do now.
608
        if (! isset($this->controller)) {
61✔
609
            throw PageNotFoundException::forEmptyController();
×
610
        }
611

612
        // Try to autoload the class
613
        if (
614
            ! class_exists($this->controller, true)
61✔
615
            || ($this->method[0] === '_' && $this->method !== '__invoke')
61✔
616
        ) {
617
            throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
×
618
        }
619

620
        // Execute route attributes' before() methods
621
        // This runs after routing/validation but BEFORE expensive controller instantiation
622
        if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
61✔
623
            $this->benchmark->start('route_attributes_before');
60✔
624
            $attributeResponse = $this->router->executeBeforeAttributes($this->request);
60✔
625
            $this->benchmark->stop('route_attributes_before');
59✔
626

627
            // If attribute returns a Response, short-circuit
628
            if ($attributeResponse instanceof ResponseInterface) {
59✔
629
                $this->benchmark->stop('controller_constructor');
1✔
630
                $this->benchmark->stop('controller');
1✔
631

632
                return $attributeResponse;
1✔
633
            }
634

635
            // If attribute returns a modified Request, use it
636
            if ($attributeResponse instanceof RequestInterface) {
59✔
637
                $this->request = $attributeResponse;
59✔
638
            }
639
        }
640

641
        return null;
60✔
642
    }
643

644
    /**
645
     * Instantiates the controller class.
646
     *
647
     * @return Controller
648
     */
649
    protected function createController()
650
    {
651
        assert(is_string($this->controller));
652

653
        $class = new $this->controller();
62✔
654
        $class->initController($this->request, $this->response, Services::logger());
62✔
655

656
        $this->benchmark->stop('controller_constructor');
62✔
657

658
        return $class;
62✔
659
    }
660

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

679
        // The controller method param types may not be string.
680
        // So cannot set `declare(strict_types=1)` in this file.
681
        try {
682
            if (method_exists($class, '_remap')) {
59✔
683
                // FormRequest injection is not supported for _remap() because its
684
                // signature is fixed to ($method, ...$params). Instantiate the
685
                // FormRequest manually inside _remap() if needed.
686
                $output = $class->_remap($this->method, ...$params);
×
687
            } else {
688
                $resolved = $this->resolveMethodParams($class, $this->method, $params);
59✔
689
                $output   = $class->{$this->method}(...$resolved);
55✔
690
            }
691
        } finally {
692
            $this->benchmark->stop('controller');
59✔
693
        }
694

695
        return $output;
55✔
696
    }
697

698
    /**
699
     * Resolves the final parameter list for a controller method call.
700
     *
701
     * @param list<string> $routeParams URI segments from the router.
702
     *
703
     * @return list<mixed>
704
     */
705
    private function resolveMethodParams(object $class, string $method, array $routeParams): array
706
    {
707
        return $this->resolveCallableParams(new ReflectionMethod($class, $method), $routeParams);
59✔
708
    }
709

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

735
        $request = service('request');
89✔
736

737
        foreach ($reflection->getParameters() as $param) {
89✔
738
            $attrs = $param->getAttributes();
28✔
739

740
            if (count($attrs) > 0) {
28✔
NEW
741
                foreach ($attrs as $attr) {
×
NEW
742
                    $instance = $attr->newInstance();
×
743

NEW
744
                    if (! $instance instanceof BaseParamSourceAttribute) {
×
NEW
745
                        continue;
×
746
                    }
747

NEW
748
                    $key = $instance->getKey($param->getName());
×
749

NEW
750
                    $resolved[] = match (true) {
×
NEW
751
                        $instance instanceof Query  => $request->getGet($key),
×
NEW
752
                        $instance instanceof Header => $request->getHeaderLine($key),
×
NEW
753
                        $instance instanceof Cookie => $request->getCookie($key),
×
NEW
754
                        default                     => throw new LogicException('Unsupported parameter attribute: ' . $instance::class),
×
NEW
755
                    };
×
756

NEW
757
                    continue 2;
×
758
                }
759
            }
760

761
            [$kind, $formRequestClass] = CallableParamClassifier::classify($param);
28✔
762

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

768
                    continue 2;
6✔
769

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

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

782
                        continue 2;
20✔
783
                    }
784

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

795
        return $resolved;
85✔
796
    }
797

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

813
        if ($response !== null) {
10✔
814
            throw new FormRequestException($response);
4✔
815
        }
816

817
        return $formRequest;
6✔
818
    }
819

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

830
        // Is there a 404 Override available?
831
        $override = $this->router->get404Override();
12✔
832

833
        if ($override !== null) {
12✔
834
            $returned = null;
4✔
835

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

842
                $this->controller = $override[0];
3✔
843
                $this->method     = $override[1];
3✔
844

845
                $controller = $this->createController();
3✔
846

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

849
                $this->benchmark->stop('controller');
3✔
850
            }
851

852
            unset($override);
4✔
853

854
            $this->gatherOutput($returned);
4✔
855

856
            return $this->response;
4✔
857
        }
858

859
        $this->outputBufferingEnd();
8✔
860

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

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

879
        if ($returned instanceof NonBufferedResponseInterface) {
89✔
880
            $this->response = $returned;
1✔
881

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

895
        if (is_string($returned)) {
88✔
896
            $this->output .= $returned;
80✔
897
        }
898

899
        $this->response->setBody($this->output);
88✔
900
    }
901

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

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

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

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

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

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

963
        $method = $this->request->getPost('_method');
26✔
964

965
        if ($method === null) {
26✔
966
            return;
24✔
967
        }
968

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

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

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

997
        return $this;
39✔
998
    }
999

1000
    protected function outputBufferingStart(): void
1001
    {
1002
        $this->bufferLevel = ob_get_level();
108✔
1003
        ob_start();
108✔
1004
    }
1005

1006
    protected function outputBufferingEnd(): string
1007
    {
1008
        $buffer = '';
108✔
1009

1010
        while (ob_get_level() > $this->bufferLevel) {
108✔
1011
            $buffer .= ob_get_contents();
108✔
1012
            ob_end_clean();
108✔
1013
        }
1014

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

© 2026 Coveralls, Inc