Skip to content

[#462] Normalize router helper surface through Request proxies - #466

Merged
armanist merged 2 commits into
quantum-php:masterfrom
armanist:issue/462-normalize-helper-surface
Apr 22, 2026
Merged

armanist merged 2 commits into
quantum-php:masterfrom
armanist:issue/462-normalize-helper-surface

Conversation

@armanist

@armanist armanist commented Apr 21, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #462

Move router metadata access behind Request proxy methods, migrate core internal usages from router helpers to request()-> methods, and remove dead helper functions while keeping public helper wrappers for DX compatibility.

Summary by CodeRabbit

  • Refactor
    • Centralized request routing/context accessors for more consistent route/module/controller/action metadata across the app.
  • Behavioral
    • View caching and pagination URL generation now use the current request URI (may affect cached views and page links).
    • Localization now derives language and translation resolution from the current request/module context.
  • Tests
    • Added unit tests covering request route metadata and route-lookup helpers.

…xies

Move router metadata access behind Request proxy methods, migrate core internal usages from router helpers to request()-> methods, and remove dead helper functions while keeping public helper wrappers for DX compatibility.

Made-with: Cursor
@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Centralize router metadata access behind Request proxy methods, remove two unused caller helpers, and update core usages to call request() accessors (replacing global router helpers) across adapters, loaders, middleware, view caching, routing, and tests.

Changes

Cohort / File(s) Summary
Helper removal
src/App/Helpers/misc.php
Removed dead helpers get_caller_class() and get_caller_function().
Request routing proxies
src/Http/Traits/Request/Route.php
Added public accessors for matched-route metadata (middlewares, module, controller, action, callback, patterns, params, cache settings, name, prefix), route lookup (findRouteByName, routeGroupExists), and getModuleBaseNamespace().
Router helper replacements
src/Router/Helpers/router.php
Replaced direct route-inspection helpers with delegations to request() accessors; adjusted route_pattern() return type to ?string.
Core migrations (usage updates)
src/App/Adapters/WebAppAdapter.php, src/Http/Request.php, src/Console/Commands/OpenApiCommand.php, src/Lang/Factories/LangFactory.php, src/Lang/Translator.php, src/Loader/Setup.php, src/Middleware/MiddlewareManager.php, src/Module/ModuleManager.php, src/Paginator/Traits/PaginatorTrait.php, src/Renderer/Adapters/... (HtmlAdapter.php,TwigAdapter.php), src/ResourceCache/ViewCache.php, src/Router/RouteBuilder.php, src/View/View.php
Switched usages of global helpers (current_module(), route_prefix(), route_group_exists(), module_base_namespace(), route_uri()) to request() proxy methods; minor docblock @throws updates where DI/Reflection can bubble up.
Tests
tests/Unit/Http/RequestTest.php, tests/Unit/Router/Helpers/RouteHelpersTest.php
Added tests validating request() route metadata proxies and route-collection lookups; adjusted expectation for route_pattern() to return null when no route matched.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

help wanted

Suggested reviewers

  • andrey-smaelov
  • grigoryanmartin20
  • live-soft

Poem

🐰 I thumped my foot and cleared the lawn,
Helpers trimmed at break of dawn,
Request now holds the routing key,
Neat paths hop home, all tidy, free,
A carrot cheer for code made brawn 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main change: moving router metadata access behind Request proxy methods and normalizing the helper surface through this normalization.
Linked Issues check ✅ Passed The PR fully implements the issue #462 acceptance criteria: removes dead helpers (get_caller_class/get_caller_function), adds Request proxy methods for router metadata, migrates core usage from helpers to request() methods, and preserves public helpers.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #462 objectives: dead helper removal, Request proxy additions, internal migration to request() methods, and docblock updates. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Apr 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.44%. Comparing base (7d33a8c) to head (b88a907).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
src/Console/Commands/OpenApiCommand.php 0.00% 2 Missing ⚠️
src/Middleware/MiddlewareManager.php 0.00% 1 Missing ⚠️
src/Router/Helpers/router.php 93.33% 1 Missing ⚠️
src/View/View.php 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #466      +/-   ##
============================================
- Coverage     82.83%   82.44%   -0.40%     
- Complexity     2857     2894      +37     
============================================
  Files           250      250              
  Lines          7614     7798     +184     
============================================
+ Hits           6307     6429     +122     
- Misses         1307     1369      +62     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Module/ModuleManager.php (1)

200-211: ⚠️ Potential issue | 🟠 Major

Verify request()->getModuleBaseNamespace() behavior in CLI scaffolding context.

ModuleManager.replacePlaceholders() calls request()->getModuleBaseNamespace() to set the {{MODULE_NAMESPACE}} placeholder in generated module files. The method returns different namespaces based on environment()->isTesting(): it returns 'Quantum\Tests\_root\modules' when testing, and 'Modules' otherwise.

This is problematic: the test suite itself (ModuleManagerTest) runs module scaffolding under environment()->isTesting() === true, creating test fixtures with the test namespace. If module scaffolding is invoked via php qt module:generate while the test environment is active (whether intentionally or by mistake), generated production modules would be created with the Quantum\Tests\_root\modules\* namespace instead of Modules\*, making them unusable.

Please confirm:

  • (a) Is php qt module:generate protected from running when environment()->isTesting() is true, or
  • (b) Should the testing branch in getModuleBaseNamespace() not apply to scaffolding operations?

If (b), ModuleManager needs a different resolver than the one used by MiddlewareManager and RouteBuilder for runtime namespace resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Module/ModuleManager.php` around lines 200 - 211,
ModuleManager::replacePlaceholders currently uses
request()->getModuleBaseNamespace() (which branches on
environment()->isTesting()) to populate {{MODULE_NAMESPACE}}, causing CLI
scaffolding (module generation) to pick up the test namespace; change
ModuleManager to not rely on request()->getModuleBaseNamespace() for
scaffolding: detect CLI scaffolding context (PHP_SAPI === 'cli' or the
module:generate command flag) and resolve the production module base namespace
instead (e.g., use a dedicated resolver or config value such as a
ModuleNamespaceResolver or config('modules.base_namespace') rather than
request()), or add an explicit parameter to replacePlaceholders()/the generator
call to pass the intended namespace; update references in replacePlaceholders
and any callers so scaffolding always uses the production namespace while
runtime classes (MiddlewareManager/RouteBuilder) can continue to use
request()->getModuleBaseNamespace().
🧹 Nitpick comments (7)
src/Paginator/Traits/PaginatorTrait.php (1)

220-233: Pagination param stripping works, but is fragile to URL shape changes.

The regex /([?&](page|per_page)=\d+)/ followed by preg_replace('/&/', '?', ..., 1) handles common layouts, but:

  • Non-numeric values (?page=abc, empty, URL-encoded) won't be stripped and will leak into generated links.
  • Order-sensitive: ?page=1&foo=bar → strips ?page=1 → leaves &foo=bar → the &→? fixup rescues it, but any stray leading & before other logic touches $routeUrl would break.
  • The first preg_replace can return null on failure; it's guarded by ?? '' on the next line, but $url at line 230 is only checked with ?? '' inside strpos, not consistently elsewhere.

Consider parsing with parse_url + parse_str, unsetting page/per_page, and rebuilding with http_build_query. That's both more robust and self-documenting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Paginator/Traits/PaginatorTrait.php` around lines 220 - 233, The getUri
method currently uses fragile regexes to strip page/per_page; replace that logic
by parsing the current request URI with parse_url and parse_str to extract query
parameters, unset 'page' and 'per_page' (and handle non-numeric/encoded values),
rebuild the query with http_build_query, reconstruct the route part (path +
rebuilt query if non-empty), then prepend $this->baseUrl when $withBaseUrl is
true and finally compute the delimiter ('?' vs '&') consistently before
returning; update references in getUri to avoid preg_replace and null-return
pitfalls from preg functions and ensure empty-query cases are handled uniformly.
src/Renderer/Adapters/TwigAdapter.php (1)

79-79: Null getCurrentModule() produces a malformed path.

When no route is matched (CLI, early errors, direct render() calls), request()->getCurrentModule() returns null, producing a path like .../modules//Views/.... $this->fs->exists() will return false and the code falls through to the shared view path, so behavior is preserved — but the intent is unclear and identical to the old current_module() foot-gun.

Consider early-returning the shared path when the module is null, or guarding the concat:

-        $moduleViewPath = modules_dir() . DS . request()->getCurrentModule() . DS . 'Views' . DS . $view . '.php';
+        $module = request()->getCurrentModule();
+        $moduleViewPath = $module
+            ? modules_dir() . DS . $module . DS . 'Views' . DS . $view . '.php'
+            : null;

Same comment applies to src/Renderer/Adapters/HtmlAdapter.php line 86.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Renderer/Adapters/TwigAdapter.php` at line 79,
request()->getCurrentModule() can be null and causes a malformed
$moduleViewPath; update TwigAdapter (and similarly HtmlAdapter) to guard that
call by checking request()->getCurrentModule() before concatenating into
$moduleViewPath (or early-return/resolve to the shared view path when it is
null) so $this->fs->exists() is only called with a valid path; locate the
$moduleViewPath construction in TwigAdapter::... (and HtmlAdapter::...) and
either skip building the module-specific path when getCurrentModule() === null
or replace it with the shared view path.
src/Http/Traits/Request/Route.php (3)

159-164: getModuleBaseNamespace() doesn't belong on the Request/Route trait and hardcodes the namespace.

This method never touches request/route state — it only branches on environment()->isTesting(). Putting it on the Request\Route trait couples unrelated concerns, and hardcoding 'Modules' removes the configurability the prior module_base_namespace() helper likely provided (e.g., via config). If a consumer ever customizes the root namespace of modules, this proxy silently ignores that.

Consider (a) moving this to a dedicated module/namespace resolver service and (b) sourcing the default from config rather than a literal, e.g. config()->get('modules.base_namespace', 'Modules').

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Traits/Request/Route.php` around lines 159 - 164,
getModuleBaseNamespace() is misplaced on the Request/Route trait and hardcodes
the namespace; remove it from the Request\Route trait and implement a dedicated
module namespace resolver (e.g., ModuleNamespaceResolver or
ModuleNamespaceService) that encapsulates the environment check
(environment()->isTesting()) and reads the default from
config()->get('modules.base_namespace', 'Modules') instead of the literal
'Modules'; update any callers of getModuleBaseNamespace() to use the new
resolver (or a module_base_namespace() helper that proxies to it) so
customization via config is respected.

138-157: routeGroupExists performs a full scan on every call.

For apps with large route tables this is O(n) per call and is invoked from hot paths like OpenApiCommand and previously route_group_exists(). Consider caching a group => true set on first access (invalidated when RouteCollection changes), or exposing a hasGroup() helper on RouteCollection itself that maintains an index at add() time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Traits/Request/Route.php` around lines 138 - 157, routeGroupExists
currently scans the entire RouteCollection each call causing O(n) hot-path
overhead; instead add a hasGroup(string $name, string $module): bool method to
RouteCollection that maintains an index (e.g., map[module][group] = true)
updated when routes are added/removed/loaded (update in RouteCollection::add(),
remove(), and any bulk load/replace methods) and then change
Request::routeGroupExists to delegate to RouteCollection::hasGroup (or use the
new index cache) so lookups become O(1); also ensure the index is rebuilt or
invalidated when the collection is replaced/cleared and adjust callers like
OpenApiCommand/route_group_exists() to use the new helper if they call the
collection directly.

117-157: findRouteByName() and routeGroupExists() duplicate the same lookup loop.

Both methods differ only in which Route field they compare (getName() vs getGroup()) and what they return. Extracting a private iterator keeps the DI/collection guard and the case-insensitive module match in one place.

Sketch
private function findRouteBy(callable $matcher): ?RouterRoute
{
    if (!Di::isRegistered(RouteCollection::class)) {
        return null;
    }
    foreach (Di::get(RouteCollection::class)->all() as $route) {
        if ($matcher($route)) {
            return $route;
        }
    }
    return null;
}

Then findRouteByName/routeGroupExists become thin predicates over findRouteBy(...).

Also worth noting: these two methods reach into a DI-registered RouteCollection from the Request object, which blurs the HTTP request abstraction with the router registry. A dedicated RouteRegistry service injected where needed would be a cleaner long-term home than piggy-backing on Request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Traits/Request/Route.php` around lines 117 - 157, Extract the
duplicated loop into a private helper like private function findRouteBy(callable
$matcher): ?RouterRoute that performs the DI guard
(Di::isRegistered(RouteCollection::class)), retrieves the collection via
Di::get(RouteCollection::class)->all(), iterates routes and returns the first
RouterRoute where $matcher($route) is true (keep case-insensitive module
comparison by comparing (string)$route->getModule()), then update
findRouteByName(string $name, string $module) to call findRouteBy(fn($route) =>
$route->getName() !== null && strcasecmp($route->getName(), $name) === 0 &&
strcasecmp((string)$route->getModule(), $module) === 0) and update
routeGroupExists(string $name, string $module) to return (findRouteBy(fn($route)
=> $route->getGroup() !== null && strcasecmp($route->getGroup(), $name) === 0 &&
strcasecmp((string)$route->getModule(), $module) === 0) !== null); ensure unique
symbols referenced: findRouteByName, routeGroupExists, findRouteBy,
RouteCollection, Di, RouterRoute, getName, getGroup, getModule.
src/Router/Helpers/router.php (1)

35-42: current_module() guard is inconsistent with the other route helpers.

current_module() still special-cases Di::has(Request::class) and returns null, but every other helper here (current_controller(), current_action(), route_callback(), current_route(), route_pattern(), route_params(), route_param(), route_cache_settings(), route_name(), route_prefix(), etc.) unconditionally calls request()->.... Either all helpers should short-circuit when Request is not registered, or none should — the asymmetry is easy to misread as an intentional nullability contract for module only.

Since request() itself will register/resolve Request in DI in normal flows, the simplest direction is to drop the guard and let current_module() delegate directly like the others; otherwise, extract a small helper and apply it consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Router/Helpers/router.php` around lines 35 - 42, The current_module()
helper is inconsistently guarding DI with Di::has(Request::class) while all
other helpers call request() unconditionally; remove that special-case by
deleting the Di::has(...) check and have current_module() simply delegate to
request()->getCurrentModule() like the other helpers (or alternatively implement
a small shared helper used by all route helpers), ensuring the function
signature stays function current_module(): ?string and the unique symbol
current_module() is the only location changed.
src/Console/Commands/OpenApiCommand.php (1)

127-138: Cache the routeGroupExists result to avoid redundant lookups.

request()->routeGroupExists('openapi', $module) is now invoked twice with identical arguments. Each call iterates the full RouteCollection. Storing the result once keeps intent clearer and avoids the repeated scan.

♻️ Proposed refactor
-        if (request()->routeGroupExists('openapi', $module) && $this->fs->exists($modulePath . DS . 'resources' . DS . 'openApi' . DS . 'spec.json')) {
+        $openapiGroupExists = request()->routeGroupExists('openapi', $module);
+
+        if ($openapiGroupExists && $this->fs->exists($modulePath . DS . 'resources' . DS . 'openApi' . DS . 'spec.json')) {
             $this->error('The Open API specifications already installed for `' . ucfirst($module) . '` module');
             return;
         }
 
-        if (!request()->routeGroupExists('openapi', $module)) {
+        if (!$openapiGroupExists) {
             $routeContent = $this->fs->get($routes);

Also note the path casing inconsistency: line 127 checks resources/openApi/spec.json while line 140-141 creates resources/openapi (lowercase). This predates the PR but is worth a follow-up since case-sensitive filesystems will treat them as distinct directories.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Console/Commands/OpenApiCommand.php` around lines 127 - 138, Cache the
result of request()->routeGroupExists('openapi', $module) into a local variable
(e.g. $hasOpenapiGroup) and use that variable in both checks instead of calling
request()->routeGroupExists twice to avoid scanning the RouteCollection twice;
update the conditional that currently checks $modulePath . DS . 'resources' . DS
. 'openApi' . DS . 'spec.json' to use a consistent lowercase 'openapi' directory
name to match where resources are created, and then use $hasOpenapiGroup when
deciding to early-return and when deciding whether to insert the openapi routes
with openapiRoutes($module) into $routes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Http/Traits/Request/Route.php`:
- Around line 76-79: Change getCompiledRoutePattern() to match the other
accessors by returning null when there is no current route or when the route's
compiled pattern is missing; update the method signature from
getCompiledRoutePattern(): string to allow null (e.g., ?string) and return null
instead of '' in both the "no route" branch and when getCompiledPattern() yields
null/empty, so callers can distinguish "no route matched" from "route has no
compiled pattern"; modify the implementation around
$this->route->getRoute()->getCompiledPattern() accordingly and keep behavior
consistent with getCurrentRoutePattern(), getRouteName(), and getRoutePrefix().

In `@src/Router/RouteBuilder.php`:
- Line 317: Update the docblock for the build() method to reflect that it can
throw the same exceptions as addRoute() and the public verb helpers: add
DiException and ReflectionException (or their union type) alongside
RouteException, since build() invokes the route DSL closures and can propagate
request/DI reflection errors encountered during route registration; reference
the build() method and the related addRoute()/verb helper behavior when making
the docblock change so callers see the accurate throws contract.

In `@tests/Unit/Http/RequestTest.php`:
- Around line 95-112: The test is reusing a DI-registered RouteCollection across
runs which causes test pollution; fix it by always creating a fresh
RouteCollection and overwriting the DI entry instead of conditionally setting
it: remove the Di::isRegistered(RouteCollection::class) conditional and
unconditionally create $collection = new RouteCollection();
$collection->add($route); then call Di::set(RouteCollection::class, $collection)
so request()->findRouteByName(...) and request()->routeGroupExists(...) always
read from a fresh collection (refer to RouteCollection, Di::set,
request()->findRouteByName, request()->routeGroupExists).

---

Outside diff comments:
In `@src/Module/ModuleManager.php`:
- Around line 200-211: ModuleManager::replacePlaceholders currently uses
request()->getModuleBaseNamespace() (which branches on
environment()->isTesting()) to populate {{MODULE_NAMESPACE}}, causing CLI
scaffolding (module generation) to pick up the test namespace; change
ModuleManager to not rely on request()->getModuleBaseNamespace() for
scaffolding: detect CLI scaffolding context (PHP_SAPI === 'cli' or the
module:generate command flag) and resolve the production module base namespace
instead (e.g., use a dedicated resolver or config value such as a
ModuleNamespaceResolver or config('modules.base_namespace') rather than
request()), or add an explicit parameter to replacePlaceholders()/the generator
call to pass the intended namespace; update references in replacePlaceholders
and any callers so scaffolding always uses the production namespace while
runtime classes (MiddlewareManager/RouteBuilder) can continue to use
request()->getModuleBaseNamespace().

---

Nitpick comments:
In `@src/Console/Commands/OpenApiCommand.php`:
- Around line 127-138: Cache the result of
request()->routeGroupExists('openapi', $module) into a local variable (e.g.
$hasOpenapiGroup) and use that variable in both checks instead of calling
request()->routeGroupExists twice to avoid scanning the RouteCollection twice;
update the conditional that currently checks $modulePath . DS . 'resources' . DS
. 'openApi' . DS . 'spec.json' to use a consistent lowercase 'openapi' directory
name to match where resources are created, and then use $hasOpenapiGroup when
deciding to early-return and when deciding whether to insert the openapi routes
with openapiRoutes($module) into $routes.

In `@src/Http/Traits/Request/Route.php`:
- Around line 159-164: getModuleBaseNamespace() is misplaced on the
Request/Route trait and hardcodes the namespace; remove it from the
Request\Route trait and implement a dedicated module namespace resolver (e.g.,
ModuleNamespaceResolver or ModuleNamespaceService) that encapsulates the
environment check (environment()->isTesting()) and reads the default from
config()->get('modules.base_namespace', 'Modules') instead of the literal
'Modules'; update any callers of getModuleBaseNamespace() to use the new
resolver (or a module_base_namespace() helper that proxies to it) so
customization via config is respected.
- Around line 138-157: routeGroupExists currently scans the entire
RouteCollection each call causing O(n) hot-path overhead; instead add a
hasGroup(string $name, string $module): bool method to RouteCollection that
maintains an index (e.g., map[module][group] = true) updated when routes are
added/removed/loaded (update in RouteCollection::add(), remove(), and any bulk
load/replace methods) and then change Request::routeGroupExists to delegate to
RouteCollection::hasGroup (or use the new index cache) so lookups become O(1);
also ensure the index is rebuilt or invalidated when the collection is
replaced/cleared and adjust callers like OpenApiCommand/route_group_exists() to
use the new helper if they call the collection directly.
- Around line 117-157: Extract the duplicated loop into a private helper like
private function findRouteBy(callable $matcher): ?RouterRoute that performs the
DI guard (Di::isRegistered(RouteCollection::class)), retrieves the collection
via Di::get(RouteCollection::class)->all(), iterates routes and returns the
first RouterRoute where $matcher($route) is true (keep case-insensitive module
comparison by comparing (string)$route->getModule()), then update
findRouteByName(string $name, string $module) to call findRouteBy(fn($route) =>
$route->getName() !== null && strcasecmp($route->getName(), $name) === 0 &&
strcasecmp((string)$route->getModule(), $module) === 0) and update
routeGroupExists(string $name, string $module) to return (findRouteBy(fn($route)
=> $route->getGroup() !== null && strcasecmp($route->getGroup(), $name) === 0 &&
strcasecmp((string)$route->getModule(), $module) === 0) !== null); ensure unique
symbols referenced: findRouteByName, routeGroupExists, findRouteBy,
RouteCollection, Di, RouterRoute, getName, getGroup, getModule.

In `@src/Paginator/Traits/PaginatorTrait.php`:
- Around line 220-233: The getUri method currently uses fragile regexes to strip
page/per_page; replace that logic by parsing the current request URI with
parse_url and parse_str to extract query parameters, unset 'page' and 'per_page'
(and handle non-numeric/encoded values), rebuild the query with
http_build_query, reconstruct the route part (path + rebuilt query if
non-empty), then prepend $this->baseUrl when $withBaseUrl is true and finally
compute the delimiter ('?' vs '&') consistently before returning; update
references in getUri to avoid preg_replace and null-return pitfalls from preg
functions and ensure empty-query cases are handled uniformly.

In `@src/Renderer/Adapters/TwigAdapter.php`:
- Line 79: request()->getCurrentModule() can be null and causes a malformed
$moduleViewPath; update TwigAdapter (and similarly HtmlAdapter) to guard that
call by checking request()->getCurrentModule() before concatenating into
$moduleViewPath (or early-return/resolve to the shared view path when it is
null) so $this->fs->exists() is only called with a valid path; locate the
$moduleViewPath construction in TwigAdapter::... (and HtmlAdapter::...) and
either skip building the module-specific path when getCurrentModule() === null
or replace it with the shared view path.

In `@src/Router/Helpers/router.php`:
- Around line 35-42: The current_module() helper is inconsistently guarding DI
with Di::has(Request::class) while all other helpers call request()
unconditionally; remove that special-case by deleting the Di::has(...) check and
have current_module() simply delegate to request()->getCurrentModule() like the
other helpers (or alternatively implement a small shared helper used by all
route helpers), ensuring the function signature stays function current_module():
?string and the unique symbol current_module() is the only location changed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec92b5ae-2794-4103-888a-d32245916560

📥 Commits

Reviewing files that changed from the base of the PR and between 7d33a8c and 7d39184.

📒 Files selected for processing (18)
  • src/App/Adapters/WebAppAdapter.php
  • src/App/Helpers/misc.php
  • src/Console/Commands/OpenApiCommand.php
  • src/Http/Request.php
  • src/Http/Traits/Request/Route.php
  • src/Lang/Factories/LangFactory.php
  • src/Lang/Translator.php
  • src/Loader/Setup.php
  • src/Middleware/MiddlewareManager.php
  • src/Module/ModuleManager.php
  • src/Paginator/Traits/PaginatorTrait.php
  • src/Renderer/Adapters/HtmlAdapter.php
  • src/Renderer/Adapters/TwigAdapter.php
  • src/ResourceCache/ViewCache.php
  • src/Router/Helpers/router.php
  • src/Router/RouteBuilder.php
  • src/View/View.php
  • tests/Unit/Http/RequestTest.php
💤 Files with no reviewable changes (1)
  • src/App/Helpers/misc.php

Comment thread src/Http/Traits/Request/Route.php Outdated
Comment thread src/Router/RouteBuilder.php
Comment thread tests/Unit/Http/RequestTest.php

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d391843b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/Unit/Http/RequestTest.php
@armanist
armanist requested a review from andrey-smaelov April 22, 2026 08:28
@armanist armanist added the enhancement New feature or request label Apr 22, 2026
@armanist armanist added this to the 3.0.0 milestone Apr 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/Router/Helpers/router.php (1)

34-41: Inconsistent Di::has(Request::class) guard in current_module() only.

Every other helper in this file (current_middlewares(), current_controller(), current_action(), route_callback(), current_route(), route_pattern(), route_params(), route_param(), route_cache_settings(), route_name(), route_prefix(), find_route_by_name(), route_group_exists(), module_base_namespace()) unconditionally calls request()->... with no guard. Meanwhile, current_module() alone checks Di::has(Request::class) before delegating.

The request() helper (in src/Http/Helpers/http.php) auto-registers Request on first call if not already registered, so all siblings will safely trigger registration if needed. This guard in current_module() prevents that auto-registration and returns null instead. Two paths forward:

  • Add the same guard to all other helpers for uniform defensive behavior, or
  • Remove the guard from current_module() to align with siblings' unconditional delegation pattern.

Pick one to ensure the helper surface behaves uniformly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Router/Helpers/router.php` around lines 34 - 41, Remove the special-case
Di::has(Request::class) guard from the current_module() helper so it
unconditionally calls request()->getCurrentModule() like the other helpers;
update the current_module() function to delegate directly to
request()->getCurrentModule() (relying on the request() helper's
auto-registration behavior) to achieve consistent behavior across helpers such
as current_middlewares(), current_controller(), current_action(),
route_callback(), current_route(), route_pattern(), route_params(),
route_param(), route_cache_settings(), route_name(), route_prefix(),
find_route_by_name(), route_group_exists(), and module_base_namespace().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Router/Helpers/router.php`:
- Around line 34-41: Remove the special-case Di::has(Request::class) guard from
the current_module() helper so it unconditionally calls
request()->getCurrentModule() like the other helpers; update the
current_module() function to delegate directly to request()->getCurrentModule()
(relying on the request() helper's auto-registration behavior) to achieve
consistent behavior across helpers such as current_middlewares(),
current_controller(), current_action(), route_callback(), current_route(),
route_pattern(), route_params(), route_param(), route_cache_settings(),
route_name(), route_prefix(), find_route_by_name(), route_group_exists(), and
module_base_namespace().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62f4ab89-1eb7-4a13-a7ee-5a7e23e1c5c5

📥 Commits

Reviewing files that changed from the base of the PR and between 7d39184 and b88a907.

📒 Files selected for processing (4)
  • src/Http/Traits/Request/Route.php
  • src/Router/Helpers/router.php
  • tests/Unit/Http/RequestTest.php
  • tests/Unit/Router/Helpers/RouteHelpersTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Unit/Http/RequestTest.php
  • src/Http/Traits/Request/Route.php

@armanist
armanist merged commit fdfede2 into quantum-php:master Apr 22, 2026
6 of 7 checks passed
@armanist
armanist deleted the issue/462-normalize-helper-surface branch April 22, 2026 12:15
@coderabbitai coderabbitai Bot mentioned this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Normalize helper surface: remove dead helpers, move router metadata access into Request, and trim redundant proxies

2 participants