feat(core): endpointUrl() helper — query-safe URLs for module routes

Introduce a helper that returns the registered route TARGET for a given
source path, so URLs built for endpoints that carry query parameters do
not rely on MintyPHP's applyRoutes() rewrite layer — that layer compares
the full request URI (including `?query`) against source paths and
silently misses modules where path ≠ target.

- core/Support/helpers/app.php: endpointUrl($path) consults
  ModuleRegistry::getRoutes(), returns lurl(target) when the path is a
  registered module source path, otherwise falls through to lurl($path).
  Safe fallback when the container or registry is unavailable.
- modules/audit/pages/audit/system-audit/index(default).phtml: replace
  the ad-hoc target-path workaround with endpointUrl('admin/system-audit/
  export'). Callers can now write the natural source path without
  knowing about the rewrite trap.
- Apply the same helper to modules/helpdesk/.../domains/index and
  pages/admin/users/index for consistency — a no-op where path already
  equals target, but establishes the convention: every export/data
  endpoint URL in page configs goes through endpointUrl().
- tests/Support/Helpers/EndpointUrlTest: 5 cases covering source→target
  resolution, idempotence when path == target, fall-through for
  unregistered paths, leading-slash normalization, and graceful
  degradation when the registry is missing. Uses
  AppContainerIsolationTrait per the contract test in
  tests/Architecture/AppContainerIsolationContractTest.

Gates: PHPUnit 1894 OK, PHPStan 0 errors, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 22:39:36 +02:00
parent 7c67d273aa
commit 0e82e00495
5 changed files with 119 additions and 7 deletions

View File

@@ -46,11 +46,7 @@ $pageConfig = [
'filterSchema' => $clientFilterSchema,
'filterChipMeta' => $filterChipMeta,
'gridLang' => $gridLang,
// Uses the page target path, not the registered route path: the Router's
// applyRoutes() rewrite does not strip query strings before matching, so
// "admin/system-audit/export?format=csv" would miss the rewrite and 404.
// Matches the convention already used for `dataUrl: 'audit/system-audit/data'`.
'exportUrl' => lurl('audit/system-audit/export'),
'exportUrl' => endpointUrl('admin/system-audit/export'),
'labels' => [
'created' => t('Created'),
'status' => t('Status'),