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
* @param string $position 'before' or 'after'
* @param list<string>|string|null $routeFilters
*/
protected function applyFilters(string $position, $routeFilters = null): ?ResponseInterface

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 don't think this helper should be added. It turns the existing inline before/after flow into a new protected abstraction, but the two branches still have different contracts.

Comment thread system/CodeIgniter.php Outdated
// we need to ensure it's active for the current request
if ($position === 'before') {
if ($routeFilters !== null) {
if (is_string($routeFilters)) {

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 should be removed with the refactor. Router::getFilters() returns array / list<string>.

Comment thread system/CodeIgniter.php
$this->handleCache($cacheConfig, $returned);

return;
}

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 the only behavior change here that I think is worth keeping: avoid the previous duplicate gatherOutput() call when startController() returns a ResponseInterface. Please extract this as a minimal targeted change in the original flow rather than keeping the surrounding refactor.

Comment thread system/CodeIgniter.php Outdated
*
* @param mixed $returned
*/
protected function handleCache(Cache $cacheConfig, $returned): void

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 does not handle caching. It only forwards to gatherOutput(), whose $cacheConfig parameter is already deprecated.

@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
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