Skip to content

fix(routes): mint Laravel routes written without a leading slash - #2471

Merged
DeusData merged 2 commits into
mainfrom
fix/laravel-slashless-route-paths
Oct 7, 2026
Merged

DeusData merged 2 commits into
mainfrom
fix/laravel-slashless-route-paths

Conversation

@DeusData

@DeusData DeusData commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Why: Laravel accepts a route URI with or without its leading slash and trims
'/' from both ends, so Route::get('users', ...) serves /users and
Route::get('', ...) the group root. Most Laravel apps write it that way. Both
route passes only minted a Route for a first argument starting with '/', so
Firefly III's API registrations (246 distinct routes in routes/api.php) had
no Route at all, and neither did laravel.io's PUT/DELETE API routes or 62 of
its 67 registrations.

Fix (extract_calls.c, PHP only; other languages' path gates are untouched):

  • A recognised Laravel registration gets its literal URI normalised to the
    slashed form before the gates: 'users' -> /users, '' -> /, 'users/' ->
    /users. It is recognised by an HTTP verb (get/post/put/patch/delete)
    called on the Route facade - directly, or at the root of a registrar chain
    such as Route::middleware('auth')->get(...) - or on $router. The literal
    Route scope is the same one the PHP/Laravel: routes inside Route::prefix()->group() produce no Route nodes #952 callee qualification accepts. Any
    other call keeps its argument: $request->get('name'), Cache::get('key'),
    config(), a collection's or session's get(), $this->router->get(), and a
    test client's $this->get('users').
  • The normalised path composes with the enclosing in-file prefix groups the
    same way as PHP/Laravel: routes inside Route::prefix()->group() produce no Route nodes #952. The attribute-array group form
    Route::group(['prefix' => 'v1/autocomplete', ...], function () {...}),
    which Firefly III's whole API uses, now contributes its prefix too;
    without it those routes would mint as /accounts instead of
    /v1/autocomplete/accounts.
  • A slashless registration whose action is a string ('index' inside a
    Route::controller() group, 'UserController@store') is minted without a
    handler. The string does not say which class owns the method, and
    resolving it could only guess: on krayin/laravel-crm, 'update' and
    'destroy' bound functions in a bundled chart.js, and 'store' bound
    ActivityController for /login.

Proof (release CLI built from this tree vs origin/main 1f9b4db):

  • Routes minted by route registration: firefly-iii 30 -> 651,
    laravel.io 39 -> 81, krayin/laravel-crm 5 -> 132. An independent
    recomputation of every composed path from the route files matches
    exactly: firefly routes/api.php 246/246 and routes/web.php 390/390,
    laravel.io web 64/64 and api 3/3, with 0 missing and 0 extra.
  • Samples checked against source: GET /v1/autocomplete/accounts and
    DELETE /v1/accounts/{account} (firefly), PUT /articles/{article} (laravel.io
    api), PUT /admin/articles/{article}/approve (laravel.io web), and
    PUT /leads/edit/{id} (krayin).
  • No false Routes: every new route-registration edge comes from a Route::
    registrar in a route file. The pre-existing test-client and JS-client
    route edges are identical to main. HANDLES stays at 0 on all three
    corpora, so no handler is guessed.

Tests (edge_types_probe): routes_laravel_slashless_issue1146 and
routes_laravel_slashless_parallel_issue1146 assert the exact method+path set:
facade, facade chain, $router, chain and array prefix groups, '' and a
trailing slash. routes_laravel_slashless_no_junk_issue1146 covers the
non-route string calls. routes_laravel_slashless_no_guessed_handlers_issue1146
checks string actions against JS and PHP decoys. RED on origin/main 3x,
green, RED on revert (the guessed-handler test is also RED with only the
handler gate removed). edge_types_probe, php_lsp, pipeline, parallel,
route_canon and cross_repo pass (731); make lint-ci passes.

Refs #1146

Why: Laravel accepts a route URI with or without its leading slash and trims
'/' from both ends, so Route::get('users', ...) serves /users and
Route::get('', ...) the group root. Most Laravel apps write it that way. Both
route passes only minted a Route for a first argument starting with '/', so
Firefly III's API registrations (246 distinct routes in routes/api.php) had
no Route at all, and neither did laravel.io's PUT/DELETE API routes or 62 of
its 67 registrations.

Fix (extract_calls.c, PHP only; other languages' path gates are untouched):
- A recognised Laravel registration gets its literal URI normalised to the
  slashed form before the gates: 'users' -> /users, '' -> /, 'users/' ->
  /users. It is recognised by an HTTP verb (get/post/put/patch/delete)
  called on the Route facade - directly, or at the root of a registrar chain
  such as Route::middleware('auth')->get(...) - or on $router. The literal
  `Route` scope is the same one the #952 callee qualification accepts. Any
  other call keeps its argument: $request->get('name'), Cache::get('key'),
  config(), a collection's or session's get(), $this->router->get(), and a
  test client's $this->get('users').
- The normalised path composes with the enclosing in-file prefix groups the
  same way as #952. The attribute-array group form
  Route::group(['prefix' => 'v1/autocomplete', ...], function () {...}),
  which Firefly III's whole API uses, now contributes its prefix too;
  without it those routes would mint as /accounts instead of
  /v1/autocomplete/accounts.
- A slashless registration whose action is a string ('index' inside a
  Route::controller() group, 'UserController@store') is minted without a
  handler. The string does not say which class owns the method, and
  resolving it could only guess: on krayin/laravel-crm, 'update' and
  'destroy' bound functions in a bundled chart.js, and 'store' bound
  ActivityController for /login.

Proof (release CLI built from this tree vs origin/main 1f9b4db):
- Routes minted by route registration: firefly-iii 30 -> 651,
  laravel.io 39 -> 81, krayin/laravel-crm 5 -> 132. An independent
  recomputation of every composed path from the route files matches
  exactly: firefly routes/api.php 246/246 and routes/web.php 390/390,
  laravel.io web 64/64 and api 3/3, with 0 missing and 0 extra.
- Samples checked against source: GET /v1/autocomplete/accounts and
  DELETE /v1/accounts/{account} (firefly), PUT /articles/{article} (laravel.io
  api), PUT /admin/articles/{article}/approve (laravel.io web), and
  PUT /leads/edit/{id} (krayin).
- No false Routes: every new route-registration edge comes from a Route::
  registrar in a route file. The pre-existing test-client and JS-client
  route edges are identical to main. HANDLES stays at 0 on all three
  corpora, so no handler is guessed.

Tests (edge_types_probe): routes_laravel_slashless_issue1146 and
routes_laravel_slashless_parallel_issue1146 assert the exact method+path set:
facade, facade chain, $router, chain and array prefix groups, '' and a
trailing slash. routes_laravel_slashless_no_junk_issue1146 covers the
non-route string calls. routes_laravel_slashless_no_guessed_handlers_issue1146
checks string actions against JS and PHP decoys. RED on origin/main 3x,
green, RED on revert (the guessed-handler test is also RED with only the
handler gate removed). edge_types_probe, php_lsp, pipeline, parallel,
route_canon and cross_repo pass (731); make lint-ci passes.

Refs #1146

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData force-pushed the fix/laravel-slashless-route-paths branch from 20703a3 to e066515 Compare October 4, 2026 16:48
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit bf93f0b into main Oct 7, 2026
27 checks passed
DeusData added a commit that referenced this pull request Oct 7, 2026
…batch

Conflict in tests/test_edge_types_probe.c: both sides added a self-contained helper + tests before the Rails test (this branch: et_handles_exact_routes and the class-handler tests; main/#2471: et_route_set_exact and the slashless-route tests) and their RUN_TEST lines; both blocks kept whole, this branch's first.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Oct 7, 2026
Conflict in internal/cbm/extract_calls.c with main's #1146 slashless Laravel routes (#2471): both helper sets kept (this branch's route_prefix_compose/route_path_with_prefix, main's php_plain_string_value/php_first_arg_value/php_group_array_prefix). At the call site main's slashless-URI step runs first, then this branch's shared route_path_with_group_prefix replaces main's inline #952 composition (identical composition and guard), then main's string-action handler rule.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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.

1 participant