Skip to content

feat: support Laravel :column binding hint in localized route URLs - #960

Open
jordyvanderhaegen wants to merge 2 commits into
masterfrom
feature/column-binding-support
Open

feat: support Laravel :column binding hint in localized route URLs#960
jordyvanderhaegen wants to merge 2 commits into
masterfrom
feature/column-binding-support

Conversation

@jordyvanderhaegen

@jordyvanderhaegen jordyvanderhaegen commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Problem

Routes defined with Laravel's {param:column} explicit binding syntax (e.g. {post:slug}) were not resolved correctly when generating localized URLs. The binding hint was treated as part of the parameter name, so lookups against the attributes array always failed and the placeholder was left unresolved in the URL.

Solution

Refactor substituteAttributesInRoute to use a single preg_replace_callback that strips the :column hint before looking up the attribute value — mirroring what Laravel's RouteUri::parse() does during route compilation.

This also fixes a pre-existing bug in the optional-param cleanup regex ([^)][^}]).

What's changed

  • substituteAttributesInRoute now handles {param:column} and {param:column?} placeholders correctly
  • Plain string values, UrlRoutable models, and LocalizedUrlRoutable models all work as before
  • forceDefaultLocation continues to work with column-bound routes

Tests

Added ModelWithCustomRouteKey fixture and tests covering:

  • Single {post:slug} binding with a data provider of 10 slug variants (hyphens, underscores, dots, numbers, unicode, accented characters)
  • Multiple bindings: {category:slug}/{post:slug}
  • Optional binding provided: {post:slug}/{comment:slug?}
  • Optional binding omitted: placeholder is cleanly stripped from the URL
  • Plain string value passed directly with forceDefaultLocation = true

Summary by CodeRabbit

  • New Features

    • Improved localized URL generation with custom route keys, including slug-based values.
    • Added support for route placeholders with binding hints and optional parameters.
    • Supports localized route keys for translatable models and standard route keys for other values.
    • Preserves missing required placeholders while removing omitted optional segments.
  • Tests

    • Added coverage for custom slugs, multiple and optional route parameters, column bindings, and diverse slug formats.

Routes defined with {param:column} syntax (e.g. {post:slug}) were not
resolved correctly when generating localized URLs, as the binding hint
was treated as part of the parameter name.

Refactor substituteAttributesInRoute to use a single preg_replace_callback
that strips the :column hint before looking up the attribute, mirroring
how Laravel's RouteUri::parse() normalises the URI during route compilation.
Also fixes a pre-existing bug in the optional-param cleanup regex
([^)] → [^}]).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

LaravelLocalization now resolves binding-aware route placeholders from localized or standard routable keys, while tests cover slug-based routes, optional parameters, column binding, and diverse slug formats in English and Spanish.

Changes

Custom route key localization

Layer / File(s) Summary
Placeholder substitution and route-key resolution
src/Mcamara/LaravelLocalization/LaravelLocalization.php
Route placeholders support binding hints, routable object keys, raw values, and optional-parameter cleanup.
Custom binding routes and validation
tests/ModelWithCustomRouteKey.php, tests/LaravelLocalizationTest.php, tests/lang/*/routes.php
Translated slug routes, a custom-key model fixture, and tests cover required, optional, multiple, column-bound, Unicode, accented, dotted, numeric, hyphenated, and underscored values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LaravelLocalization
  participant substituteAttributesInRoute
  participant UrlRoutable
  LaravelLocalization->>substituteAttributesInRoute: localized route template and attributes
  substituteAttributesInRoute->>UrlRoutable: request route key
  UrlRoutable-->>substituteAttributesInRoute: localized or standard key
  substituteAttributesInRoute-->>LaravelLocalization: generated localized URL
Loading

Poem

A rabbit hops through slugs so neat,
With optional paths beneath its feet.
Spanish trails and English signs,
Bindings bloom in route designs.
“Hop!” says the test, “each key is right!” 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: support for Laravel column binding hints in localized route URLs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/column-binding-support

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.

Registers view_with_slug and view_post_comment routes in setRoutes() and
adds HTTP dispatch tests to verify that Laravel correctly matches routes
using the {param:slug} syntax and extracts the parameter value from the URL:

- Single binding: {post:slug} parameter is correctly extracted
- Optional binding provided: {post:slug}/{comment:slug?} both extracted
- Optional binding omitted: route still matches with optional segment absent

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/LaravelLocalizationTest.php`:
- Around line 686-691: Update
testRouteWithOptionalColumnBindingMatchesWhenOmitted to verify the
view_post_comment route is matched, either by asserting the matched route name
or by changing the request URI to one that cannot match view_with_slug, while
preserving validation of the omitted optional comment segment.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 412a8210-11d8-47d1-a3e8-55414f20c2ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0559059 and 46ddd93.

📒 Files selected for processing (5)
  • src/Mcamara/LaravelLocalization/LaravelLocalization.php
  • tests/LaravelLocalizationTest.php
  • tests/ModelWithCustomRouteKey.php
  • tests/lang/en/routes.php
  • tests/lang/es/routes.php

Comment on lines +686 to +691
public function testRouteWithOptionalColumnBindingMatchesWhenOmitted(): void
{
$response = $this->get(self::TEST_URL.'posts/my-post');
$response->assertStatus(200);
$this->assertEquals('my-post', $response->getContent());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)LaravelLocalizationTest\.php$|route|routes|webpack|migrations|phpunit|composer' || true

echo
echo "Relevant test section:"
if [ -f tests/LaravelLocalizationTest.php ]; then
  wc -l tests/LaravelLocalizationTest.php
  sed -n '50,100p' tests/LaravelLocalizationTest.php
  sed -n '660,705p' tests/LaravelLocalizationTest.php
fi

echo
echo "Search route definitions in tests:"
rg -n "view_with_slug|view_post_comment|optional|comment|posts/my-post|Route::|fallback" tests -S || true

if command -v git >/dev/null 2>&1; then
  echo
  echo "Git diff summary (if available):"
  git diff --stat
  echo
  echo "Git diff for file (if available):"
  git diff -- tests/LaravelLocalizationTest.php | sed -n '1,240p'
fi

Repository: mcamara/laravel-localization

Length of output: 9483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Candidate source files:"
git ls-files 'src/*.php' | sed -n '1,120p'

echo
echo "Search translateRoutes/transRoute:"
rg -n "function (translateRoutes|transRoute)|translateRoutes\(|transRoute\(" src -S

echo
echo "Relevant route/translation method bodies:"
for f in $(rg -l "translateRoutes|transRoute" src); do
  echo "--- $f"
  ast-grep outline "$f" --match 'translateRoutes' --view expanded || true
  ast-grep outline "$f" --match 'transRoute' --view expanded || true
done

echo
echo "Route collection refresh definition:"
rg -n "refresh_router_lookups|class LaravelLocalizationRouter|function getDefaultRouterLoader" tests src -S

Repository: mcamara/laravel-localization

Length of output: 2442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "LaravelLocalization.php sections:"
wc -l src/Mcamara/LaravelLocalization/LaravelLocalization.php
sed -n '1,120p' src/Mcamara/LaravelLocalization/LaravelLocalization.php
sed -n '640,735p' src/Mcamara/LaravelLocalization/LaravelLocalization.php
sed -n '735,820p' src/Mcamara/LaravelLocalization/LaravelLocalization.php

echo
echo "Tests/LaravelLocalizationTest.php header/setup:"
sed -n '1,100p' tests/LaravelLocalizationTest.php

echo
echo "Language stub route generation/transRoute implementation searches:"
rg -n "translateRoutes|transRoute|cachedRoutes|translate|Route::group|Locale::make|routes\[" src/Mcamara/LaravelLocalization/LaravelLocalization.php src -S || true

echo
echo "Composer deps:"
sed -n '1,120p' composer.json

Repository: mcamara/laravel-localization

Length of output: 24487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "setLocale definitions/usages:"
rg -n "function setLocale|public function setLocale|protected function setLocale|Locale:make|getNonLocalizedURL|getURLFromRouteNameTranslated|translations" src/Mcamara/LaravelLocalization/LaravelLocalization.php src/Mcamara/LaravelLocalization tests/tests src -S || true

echo
echo "Search route name assertions near optional binding tests:"
rg -n "assertStatus|assertRouteMatches|assertEquals\('my-post'|getContent\(\)|view_post_comment" tests/LaravelLocalizationTest.php -S || true

echo
echo "Framework doc evidence maybe via local docs if present:"
[ -d vendor ] && (find vendor/laravel/framework -maxdepth 4 -type f | rg 'Router|RouteCollection|Route' | head -20 || true) || true

Repository: mcamara/laravel-localization

Length of output: 8862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant Laravel routing source sections:"
for f in \
  vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteUri.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteParameterBinder.php \
  vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php
do
  echo "--- $f"
  rg -n "function find|findRoute|getRoute|parameter|binding|slug|dependencies|dependency|RouteParameterBinder|resolveDependencies|dependencies" "$f" || true
done

echo
echo "Extract route matching/resolution snippets:"
python3 - <<'PY'
from pathlib import Path
targets = {
    "AbstractRouteCollection.php": [1, 180],
    "RouteCollection.php": [1, 180],
    "RouteUri.php": [1, 220],
    "RouteParameterBinder.php": [1, 220],
    "ImplicitRouteBinding.php": [1, 220],
    "RouteDependencyResolverTrait.php": [1, 180],
}
for path, (start, end) in targets.items():
    f = Path(f"vendor/laravel/framework/src/Illuminate/Routing/{path}")
    lines = f.read_text(errors="replace").splitlines()
    print(f"--- vendor/laravel/framework/src/Illuminate/Routing/{path}:{start}-{end}")
    for i in range(start-1, min(end, len(lines))):
        if any(tok in lines[i] for tok in ["function find", "function getRoute", "function resolveDependencies", "function getRouteKey", "function match", "parameter=", "dependency", "slug"]):
            print(f"{i+1}: {lines[i]}")
PY

Repository: mcamara/laravel-localization

Length of output: 6537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "AbstractRouteCollection matching bodies:"
sed -n '60,140p' vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php
sed -n '180,230p' vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php

echo
echo "RouteCollection/CompiledRouteCollection dispatch body bodies:"
rg -n "function dispatch|function dispatchToRoute|protected function dispatch|public function dispatch" vendor/laravel/framework/src/Illuminate/Routing -S
case "$(rg -l "function dispatch\(" vendor/laravel/framework/src/Illuminate/Routing -S | head -1)" in
  vendor/laravel/framework/src/Illuminate/Routing/Router.php) sed -n '200,280p' vendor/laravel/framework/src/Illuminate/Routing/Router.php ;;
esac
sed -n '190,230p' vendor/laravel/framework/src/Illuminate/Routing/CompiledRouteCollection.php
sed -n '180,230p' vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php

echo
echo "ImplicitRouteBinding and dependencies bodies:"
sed -n '1,110p' vendor/laravel/apimachinery/src/Illuminate/Routing/ ... 2>/dev/null || sed -n '1,130p' vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php
sed -n '1,180p' vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php

echo
echo "Standalone Laravel RouteParameterBinder dependency/parameter probe if testbench autoload is available:"
php -r '
if (!file_exists("vendor/laravel/framework/src/Illuminate/Routing/Route.php")) exit("NO ROUTE CLASS");
$suffix = basename(getcwd());
if ($suffix !== "vendor") exit("In sandbox, avoiding actual request; source read-only inspection was requested.");
' || true

Repository: mcamara/laravel-localization

Length of output: 15144


Make the omitted-optional binding test unambiguous.

/posts/my-post can match both view_with_slug and view_post_comment. This test currently asserts the response body from either matching route, so it does not prove the optional comment segment is handled by view_post_comment; assert the matched route name or use a URI that only targets the optional route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/LaravelLocalizationTest.php` around lines 686 - 691, Update
testRouteWithOptionalColumnBindingMatchesWhenOmitted to verify the
view_post_comment route is matched, either by asserting the matched route name
or by changing the request URI to one that cannot match view_with_slug, while
preserving validation of the omitted optional comment segment.

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