Thanks to visit codestin.com
Credit goes to github.com

Skip to content

refactor: simplify and optimize handleRequest method in CodeIgniter#10369

Open
gr8man wants to merge 1 commit into
codeigniter4:developfrom
gr8man:refactor/codeigniter-handle-request
Open

refactor: simplify and optimize handleRequest method in CodeIgniter#10369
gr8man wants to merge 1 commit into
codeigniter4:developfrom
gr8man:refactor/codeigniter-handle-request

Conversation

@gr8man

@gr8man gr8man commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description
This PR refactors the handleRequest() method in system/CodeIgniter.php.
Originally, handleRequest() was a 109-line method with high complexity, managing routing, filters, controller dispatching, output buffering, and caching within a single block.

This change simplifies the method by delegating these concerns to three smaller, focused helper methods:

  1. applyFilters(string $position, $routeFilters = null): ?ResponseInterface — Runs global and route-specific filters.
  2. serveResponse(Cache $cacheConfig, $returned): void — Creates/runs the controller and gathers the output.
  3. handleCache(Cache $cacheConfig, $returned): void — A wrapper around output gathering and caching.

Optimizations Implemented

  1. Lazy URI Resolution: The $this->request->getPath() call has been moved inside applyFilters(), meaning URI resolution is only performed when filters are enabled ($this->enableFilters === true), avoiding unnecessary string calculations.
  2. Early Response Return: Added an early return; inside serveResponse() if startController() returns a ResponseInterface instance (e.g. from filter attributes or closure routes). This avoids redundant calls to handleCache() / gatherOutput() which ended output buffering and processed the response body twice in the original implementation.
  3. No Redundant Wrappers: We call the existing tryToRouteIt() method directly inside handleRequest() instead of creating a redundant resolveRoute() wrapper.

Performance Comparison (2,000 Iterations)
A realistic benchmark simulating a full request lifecycle (routing to Home::index controller, rendering the welcome_message view, and executing invalidchars before filter plus secureheaders and toolbar after filters) was run to compare the two branches.

Original develop branch:

--- REALISTIC BENCHMARK: Controller + View + Filters ---
Current Branch: develop
Route: GET / -> App\Controllers\Home::index
Active Filters: [before: invalidchars], [after: secureheaders, toolbar]
Iterations: 2,000

Time taken for 2000 runs: 1.1175 seconds
Avg time per run: 0.5587 ms
Peak memory usage: 40.59 MB

Refactored branch (refactor/codeigniter-handle-request):

--- REALISTIC BENCHMARK: Controller + View + Filters ---
Current Branch: refactor/codeigniter-handle-request
Route: GET / -> App\Controllers\Home::index
Active Filters: [before: invalidchars], [after: secureheaders, toolbar]
Iterations: 2,000

Time taken for 2000 runs: 1.0414 seconds
Avg time per run: 0.5207 ms
Peak memory usage: 40.59 MB

The refactoring reduces the average request execution time by ~6.8% while maintaining identical peak memory usage.

Benchmark Script Code

<?php

require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/system/Test/bootstrap.php';

use CodeIgniter\Test\Mock\MockCodeIgniter;
use Config\App;
use Config\Services;

// Reset services.
Services::reset();

// Setup request environment variables on superglobals
$superglobals = service('superglobals');
$superglobals->setServer('argv', ['index.php']);
$superglobals->setServer('argc', 1);
$superglobals->setServer('REQUEST_URI', '/');
$superglobals->setServer('SCRIPT_NAME', '/index.php');
$superglobals->setServer('HTTP_USER_AGENT', 'Mozilla/5.0');

// Load default routes (like the welcome page routing to App\Controllers\Home::index)
$routes = service('routes')->loadRoutes();

// Enable realistic global filters to mimic a real production application
$filtersConfig = new Config\Filters();
$filtersConfig->globals['before'] = ['invalidchars'];
$filtersConfig->globals['after']  = ['secureheaders', 'toolbar'];
Services::injectMock('filtersConfig', $filtersConfig);

$config      = new App();
$codeigniter = new MockCodeIgniter($config);

// Run 2,000 iterations (real controller + view rendering + multiple filters is more resource intensive)
$iterations = 2000;

echo "--- REALISTIC BENCHMARK: Controller + View + Filters ---\n";
echo "Current Branch: " . trim(shell_exec('git rev-parse --abbrev-ref HEAD')) . "\n";
echo "Route: GET / -> App\Controllers\Home::index\n";
echo "Active Filters: [before: invalidchars], [after: secureheaders, toolbar]\n";
echo "Iterations: " . number_format($iterations) . "\n\n";

$start = microtime(true);

ob_start();
for ($i = 0; $i < $iterations; $i++) {
    // Reset properties to simulate clean worker mode run
    $codeigniter->resetForWorkerMode();
    
    // Run the request flow (runs routing, invalidchars filter, Home controller, view rendering, secureheaders/toolbar filters)
    $codeigniter->run($routes);
}
ob_end_clean();

$end = microtime(true);
$endMemory = memory_get_peak_usage();

$time = $end - $start;
echo "Time taken for {$iterations} runs: " . number_format($time, 4) . " seconds\n";
echo "Avg time per run: " . number_format(($time / $iterations) * 1000, 4) . " ms\n";
echo "Peak memory usage: " . number_format($endMemory / 1024 / 1024, 2) . " MB\n";

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

@gr8man gr8man force-pushed the refactor/codeigniter-handle-request branch from 36c2a86 to 76b6aac Compare July 2, 2026 20:50

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree there is one real issue here: when startController() returns a ResponseInterface, the old flow can call gatherOutput() twice. Fixing that redundant call is reasonable.

However, I don't think this refactor should be merged as-is.

  1. Lazy URI Resolution: The $this->request->getPath() call has been moved inside applyFilters(), meaning URI resolution is only performed when filters are enabled ($this->enableFilters === true), avoiding unnecessary string calculations.

I think this is false as an optimization. Routing still calls getPath() unconditionally, and IncomingRequest::getPath() is only a property read. This does not justify the refactor.

  1. Early Response Return: Added an early return; inside serveResponse() if startController() returns a ResponseInterface instance (e.g. from filter attributes or closure routes). This avoids redundant calls to handleCache() / gatherOutput() which ended output buffering and processed the response body twice in the original implementation.

This should be extracted as the only kept change.

  1. No Redundant Wrappers: We call the existing tryToRouteIt() method directly inside handleRequest() instead of creating a redundant resolveRoute() wrapper.

The supposed avoided wrapper did not exist in the original code, while this PR adds a new redundant wrapper: handleCache().

Performance Comparison (2,000 Iterations) A realistic benchmark simulating a full request lifecycle (routing to Home::index controller, rendering the welcome_message view, and executing invalidchars before filter plus secureheaders and toolbar after filters) was run to compare the two branches.
...
The refactoring reduces the average request execution time by ~6.8% while maintaining identical peak memory usage.

I ran the benchmark script locally and couldn't reproduce the ~6.8% improvement. Across 10 runs per branch, the refactored branch was actually a bit slower, both by average and by median.

That matches my earlier doubts about this benchmark. There are also problems with the script itself: as far as I can tell it doesn't actually enable the filters it claims to, and it never triggers the ResponseInterface short-circuit, which is the one code path this PR really changes. The per-iteration reset also doesn't match what the FrankenPHP worker loop does between requests, so it's not measuring the scenario it claims to.

Comment thread system/CodeIgniter.php Outdated
Comment thread system/CodeIgniter.php Outdated
Comment thread system/CodeIgniter.php
Comment thread system/CodeIgniter.php Outdated
@gr8man gr8man force-pushed the refactor/codeigniter-handle-request branch from fddc099 to 6f9842e Compare July 7, 2026 18:54
@gr8man gr8man force-pushed the refactor/codeigniter-handle-request branch from 6f9842e to efcbfae Compare July 7, 2026 18:55

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the PR description so it's not misleading relative to the code changes.

Comment thread system/CodeIgniter.php
Comment on lines +176 to +190
/**
* Cache for Routing configuration
*/
private ?Routing $routingConfig = null;

/**
* Cache for Feature configuration
*/
private ?Feature $featureConfig = null;

/**
* Cache for Cache configuration
*/
private ?Cache $cacheConfig = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove. It gives us nothing here and only causes problems for worker mode.

Comment thread system/CodeIgniter.php
Comment on lines -224 to +245
$csp = service('csp');
$csp = Services::csp();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert everywhere. We prefer the use of service() as it's optimized.

Comment thread system/CodeIgniter.php
Comment on lines -836 to +861
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
protected function tryToRouteIt(?RouteCollectionInterface $routes = null, ?string $uri = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a BC break, without real gain. This is just one getPath() call. Please revert.

Comment thread system/CodeIgniter.php
Comment on lines -891 to +917
// Is it routed to a Closure?
if (is_object($this->controller) && ($this->controller::class === 'Closure')) {
// Is it routed to a Closure? Use instanceof — faster than is_object() + ::class string check.
if ($this->controller instanceof Closure) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's likely true, but please remove the added part of the comment. I see no reason why we should have this in the code.

Comment thread system/CodeIgniter.php
if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) {
// Ignore AJAX requests. Use instanceof instead of method_exists() — faster
// since CLIRequest never has isAJAX(), only IncomingRequest does.
if ($this->request instanceof IncomingRequest && $this->request->isAJAX()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This narrows the previous behavior from "any request with isAJAX()" to only IncomingRequest.

I think I'm okay with this, but I would like to hear what others think.

If this stays, please remove the added comment, as it's not relevant to include.

Comment thread system/CodeIgniter.php
Comment on lines -1135 to +1161
if (in_array($method, [Method::PUT, Method::PATCH, Method::DELETE], true)) {
if (in_array($method, self::SPOOFABLE_METHODS, true)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this gives us any benefits here. This constant will be used only once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants