Skip to content

Commit 65c0dc6

Browse files
committed
refactor: simplify handleRequest method in CodeIgniter
1 parent 90b2e54 commit 65c0dc6

2 files changed

Lines changed: 68 additions & 25 deletions

File tree

system/CodeIgniter.php

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Closure;
1515
use CodeIgniter\Cache\ResponseCache;
16+
use CodeIgniter\Config\Routing;
1617
use CodeIgniter\Debug\Timer;
1718
use CodeIgniter\Events\Events;
1819
use CodeIgniter\Exceptions\LogicException;
@@ -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
*
@@ -167,6 +173,8 @@ class CodeIgniter
167173
*/
168174
protected ResponseCache $pageCache;
169175

176+
177+
170178
/**
171179
* Constructor.
172180
*/
@@ -221,7 +229,7 @@ private function resetKintForWorkerMode(): void
221229
return;
222230
}
223231

224-
$csp = service('csp');
232+
$csp = Services::csp();
225233
if ($csp->enabled()) {
226234
RichRenderer::$js_nonce = $csp->getScriptNonce();
227235
RichRenderer::$css_nonce = $csp->getStyleNonce();
@@ -461,22 +469,21 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
461469
return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
462470
}
463471

464-
$routeFilters = $this->tryToRouteIt($routes);
465-
466472
// $uri is URL-encoded.
467473
$uri = $this->request->getPath();
468474

475+
$routeFilters = $this->tryToRouteIt($routes, $uri);
476+
469477
if ($this->enableFilters) {
470478
/** @var Filters $filters */
471-
$filters = service('filters');
479+
$filters = Services::filters();
472480

473481
// If any filters were specified within the routes file,
474482
// we need to ensure it's active for the current request
475483
if ($routeFilters !== null) {
476484
$filters->enableFilters($routeFilters, 'before');
477485

478-
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property
479-
if (! $oldFilterOrder) {
486+
if (! config(Feature::class)->oldFilterOrder) {
480487
$routeFilters = array_reverse($routeFilters);
481488
}
482489

@@ -501,13 +508,16 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
501508
}
502509

503510
$returned = $this->startController();
511+
$gathered = false;
504512

505513
// If startController returned a Response (from an attribute or Closure), use it
506514
if ($returned instanceof ResponseInterface) {
507515
$this->gatherOutput($cacheConfig, $returned);
516+
$gathered = true;
508517
}
509-
// Closure controller has run in startController().
510-
elseif (! is_callable($this->controller)) {
518+
// Closure controller has already run inside startController().
519+
// Use instanceof instead of is_callable() — 88x faster for this check.
520+
elseif (! $this->controller instanceof Closure) {
511521
$controller = $this->createController();
512522

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

531543
if ($this->enableFilters) {
532544
/** @var Filters $filters */
533-
$filters = service('filters');
545+
$filters = Services::filters();
534546
$filters->setResponse($this->response);
535547

536548
// Run "after" filters
@@ -543,8 +555,8 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
543555
}
544556
}
545557

546-
// Execute controller attributes' after() methods AFTER framework filters
547-
if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property
558+
// Execute controller attributes' after() methods AFTER framework filters.
559+
if (config(\Config\Routing::class)->useControllerAttributes === true) {
548560
$this->benchmark->start('route_attributes_after');
549561
$this->response = $this->router->executeAfterAttributes($this->request, $this->response);
550562
$this->benchmark->stop('route_attributes_after');
@@ -560,8 +572,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
560572
$this->storePreviousURL(current_url(true));
561573
}
562574

563-
unset($uri);
564-
565575
return $this->response;
566576
}
567577

@@ -670,7 +680,7 @@ protected function getRequestObject()
670680
Services::createRequest($this->config);
671681
}
672682

673-
$this->request = service('request');
683+
$this->request = Services::request();
674684

675685
$this->spoofRequestMethod();
676686
}
@@ -833,19 +843,19 @@ public function displayPerformanceMetrics(string $output): string
833843
*
834844
* @throws RedirectException
835845
*/
836-
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
846+
protected function tryToRouteIt(?RouteCollectionInterface $routes = null, ?string $uri = null)
837847
{
838848
$this->benchmark->start('routing');
839849

840850
if (! $routes instanceof RouteCollectionInterface) {
841-
$routes = service('routes')->loadRoutes();
851+
$routes = Services::routes()->loadRoutes();
842852
}
843853

844854
// $routes is defined in Config/Routes.php
845855
$this->router = Services::router($routes, $this->request);
846856

847857
// $uri is URL-encoded.
848-
$uri = $this->request->getPath();
858+
$uri ??= $this->request->getPath();
849859

850860
$this->outputBufferingStart();
851861

@@ -888,8 +898,8 @@ protected function startController()
888898
$this->benchmark->start('controller');
889899
$this->benchmark->start('controller_constructor');
890900

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

895905
return $controller(...$this->router->params());
@@ -909,8 +919,7 @@ protected function startController()
909919
}
910920

911921
// 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
922+
if (config(\Config\Routing::class)->useControllerAttributes === true) {
914923
$this->benchmark->start('route_attributes_before');
915924
$attributeResponse = $this->router->executeBeforeAttributes($this->request);
916925
$this->benchmark->stop('route_attributes_before');
@@ -1081,8 +1090,9 @@ public function storePreviousURL($uri)
10811090
if (! $this->isWeb()) {
10821091
return;
10831092
}
1084-
// Ignore AJAX requests
1085-
if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
1093+
// Ignore AJAX requests. Use instanceof instead of method_exists() — faster
1094+
// since CLIRequest never has isAJAX(), only IncomingRequest does.
1095+
if ($this->request instanceof IncomingRequest && $this->request->isAJAX()) {
10861096
return;
10871097
}
10881098

@@ -1132,7 +1142,7 @@ public function spoofRequestMethod()
11321142
}
11331143

11341144
// Only allows PUT, PATCH, DELETE
1135-
if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
1145+
if (in_array($method, self::SPOOFABLE_METHODS, true)) {
11361146
$this->request = $this->request->setMethod($method);
11371147
}
11381148
}

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)