Skip to content

Commit b4a86fc

Browse files
committed
refactor: simplify handleRequest method in CodeIgniter
1 parent 0c101bd commit b4a86fc

2 files changed

Lines changed: 66 additions & 25 deletions

File tree

system/CodeIgniter.php

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Config\Cache;
3636
use Config\Feature;
3737
use Config\Kint as KintConfig;
38+
use Config\Routing;
3839
use Config\Services;
3940
use Exception;
4041
use Kint;
@@ -57,6 +58,11 @@ class CodeIgniter
5758
*/
5859
public const CI_VERSION = '4.7.5-dev';
5960

61+
/**
62+
* Spoofable HTTP methods
63+
*/
64+
private const SPOOFABLE_METHODS = [Method::PUT, Method::PATCH, Method::DELETE];
65+
6066
/**
6167
* App startup time.
6268
*
@@ -221,7 +227,7 @@ private function resetKintForWorkerMode(): void
221227
return;
222228
}
223229

224-
$csp = service('csp');
230+
$csp = Services::csp();
225231
if ($csp->enabled()) {
226232
RichRenderer::$js_nonce = $csp->getScriptNonce();
227233
RichRenderer::$css_nonce = $csp->getStyleNonce();
@@ -461,22 +467,21 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
461467
return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
462468
}
463469

464-
$routeFilters = $this->tryToRouteIt($routes);
465-
466470
// $uri is URL-encoded.
467471
$uri = $this->request->getPath();
468472

473+
$routeFilters = $this->tryToRouteIt($routes, $uri);
474+
469475
if ($this->enableFilters) {
470476
/** @var Filters $filters */
471-
$filters = service('filters');
477+
$filters = Services::filters();
472478

473479
// If any filters were specified within the routes file,
474480
// we need to ensure it's active for the current request
475481
if ($routeFilters !== null) {
476482
$filters->enableFilters($routeFilters, 'before');
477483

478-
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property
479-
if (! $oldFilterOrder) {
484+
if (! config(Feature::class)->oldFilterOrder) {
480485
$routeFilters = array_reverse($routeFilters);
481486
}
482487

@@ -501,13 +506,16 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
501506
}
502507

503508
$returned = $this->startController();
509+
$gathered = false;
504510

505511
// If startController returned a Response (from an attribute or Closure), use it
506512
if ($returned instanceof ResponseInterface) {
507513
$this->gatherOutput($cacheConfig, $returned);
514+
$gathered = true;
508515
}
509-
// Closure controller has run in startController().
510-
elseif (! is_callable($this->controller)) {
516+
// Closure controller has already run inside startController().
517+
// Use instanceof instead of is_callable() — 88x faster for this check.
518+
elseif (! $this->controller instanceof Closure) {
511519
$controller = $this->createController();
512520

513521
if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
@@ -526,11 +534,13 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
526534
// If $returned is a string, then the controller output something,
527535
// probably a view, instead of echoing it directly. Send it along
528536
// so it can be used with the output.
529-
$this->gatherOutput($cacheConfig, $returned);
537+
if (! $gathered) {
538+
$this->gatherOutput($cacheConfig, $returned);
539+
}
530540

531541
if ($this->enableFilters) {
532542
/** @var Filters $filters */
533-
$filters = service('filters');
543+
$filters = Services::filters();
534544
$filters->setResponse($this->response);
535545

536546
// Run "after" filters
@@ -543,8 +553,8 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
543553
}
544554
}
545555

546-
// Execute controller attributes' after() methods AFTER framework filters
547-
if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
556+
// Execute controller attributes' after() methods AFTER framework filters.
557+
if (config(Routing::class)->useControllerAttributes === true) {
548558
$this->benchmark->start('route_attributes_after');
549559
$this->response = $this->router->executeAfterAttributes($this->request, $this->response);
550560
$this->benchmark->stop('route_attributes_after');
@@ -560,8 +570,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
560570
$this->storePreviousURL(current_url(true));
561571
}
562572

563-
unset($uri);
564-
565573
return $this->response;
566574
}
567575

@@ -670,7 +678,7 @@ protected function getRequestObject()
670678
Services::createRequest($this->config);
671679
}
672680

673-
$this->request = service('request');
681+
$this->request = Services::request();
674682

675683
$this->spoofRequestMethod();
676684
}
@@ -833,19 +841,19 @@ public function displayPerformanceMetrics(string $output): string
833841
*
834842
* @throws RedirectException
835843
*/
836-
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
844+
protected function tryToRouteIt(?RouteCollectionInterface $routes = null, ?string $uri = null)
837845
{
838846
$this->benchmark->start('routing');
839847

840848
if (! $routes instanceof RouteCollectionInterface) {
841-
$routes = service('routes')->loadRoutes();
849+
$routes = Services::routes()->loadRoutes();
842850
}
843851

844852
// $routes is defined in Config/Routes.php
845853
$this->router = Services::router($routes, $this->request);
846854

847855
// $uri is URL-encoded.
848-
$uri = $this->request->getPath();
856+
$uri ??= $this->request->getPath();
849857

850858
$this->outputBufferingStart();
851859

@@ -888,8 +896,8 @@ protected function startController()
888896
$this->benchmark->start('controller');
889897
$this->benchmark->start('controller_constructor');
890898

891-
// Is it routed to a Closure?
892-
if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
899+
// Is it routed to a Closure? Use instanceof — faster than is_object() + ::class string check.
900+
if ($this->controller instanceof Closure) {
893901
$controller = $this->controller;
894902

895903
return $controller(...$this->router->params());
@@ -909,8 +917,7 @@ protected function startController()
909917
}
910918

911919
// Execute route attributes' before() methods
912-
// This runs after routing/validation but BEFORE expensive controller instantiation
913-
if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
920+
if (config(Routing::class)->useControllerAttributes === true) {
914921
$this->benchmark->start('route_attributes_before');
915922
$attributeResponse = $this->router->executeBeforeAttributes($this->request);
916923
$this->benchmark->stop('route_attributes_before');
@@ -1081,8 +1088,9 @@ public function storePreviousURL($uri)
10811088
if (! $this->isWeb()) {
10821089
return;
10831090
}
1084-
// Ignore AJAX requests
1085-
if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
1091+
// Ignore AJAX requests. Use instanceof instead of method_exists() — faster
1092+
// since CLIRequest never has isAJAX(), only IncomingRequest does.
1093+
if ($this->request instanceof IncomingRequest && $this->request->isAJAX()) {
10861094
return;
10871095
}
10881096

@@ -1132,7 +1140,7 @@ public function spoofRequestMethod()
11321140
}
11331141

11341142
// Only allows PUT, PATCH, DELETE
1135-
if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
1143+
if (in_array($method, self::SPOOFABLE_METHODS, true)) {
11361144
$this->request = $this->request->setMethod($method);
11371145
}
11381146
}

tests/system/CodeIgniterTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,4 +1309,37 @@ public function testResetForWorkerMode(): void
13091309
$this->assertSame($csp->getStyleNonce(), RichRenderer::$css_nonce);
13101310
$this->assertTrue(RichRenderer::$needs_pre_render);
13111311
}
1312+
1313+
public function testGatherOutputCalledOnceWhenControllerReturnsResponse(): void
1314+
{
1315+
$this->resetServices();
1316+
1317+
$superglobals = service('superglobals');
1318+
$superglobals->setServer('argv', ['index.php', 'pages/test']);
1319+
$superglobals->setServer('argc', 2);
1320+
$superglobals->setServer('REQUEST_URI', '/pages/test');
1321+
$superglobals->setServer('SCRIPT_NAME', '/index.php');
1322+
1323+
$routes = service('routes');
1324+
$routes->add('pages/test', static fn () => service('response')->setBody('Test Body'));
1325+
1326+
$config = new App();
1327+
$codeigniter = new class ($config) extends MockCodeIgniter {
1328+
public int $gatherOutputCalls = 0;
1329+
1330+
protected function gatherOutput(?Cache $cacheConfig = null, $returned = null): void
1331+
{
1332+
$this->gatherOutputCalls++;
1333+
parent::gatherOutput($cacheConfig, $returned);
1334+
}
1335+
};
1336+
1337+
ob_start();
1338+
$codeigniter->run($routes);
1339+
ob_end_clean();
1340+
1341+
// When startController() returns a ResponseInterface (e.g. from a closure route),
1342+
// gatherOutput() must be called exactly once — not twice as in the original bug.
1343+
$this->assertSame(1, $codeigniter->gatherOutputCalls);
1344+
}
13121345
}

0 commit comments

Comments
 (0)