Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/Illuminate/Routing/RouteUrlGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ protected function formatParameters(Route $route, $parameters)
$bindingField = $route->bindingFieldFor($name);
$defaultParameterKey = $bindingField ? "$name:$bindingField" : $name;

if (! isset($this->defaultParameters[$defaultParameterKey]) && ! isset($optionalParameters[$name])) {
if (! isset($this->defaultParameters[$defaultParameterKey]) && ! isset($this->defaultParameters[$name]) && ! isset($optionalParameters[$name])) {
// No named parameter or default value for a required parameter, try to match to positional parameter below...
array_push($requiredRouteParametersWithoutDefaultsOrNamedParameters, $name);
}
Expand Down Expand Up @@ -284,8 +284,12 @@ protected function formatParameters(Route $route, $parameters)
$bindingField = $route->bindingFieldFor($key);
$defaultParameterKey = $bindingField ? "$key:$bindingField" : $key;

if ($value === '' && isset($this->defaultParameters[$defaultParameterKey])) {
$namedParameters[$key] = $this->defaultParameters[$defaultParameterKey];
$defaultParameter = $this->defaultParameters[$defaultParameterKey]
?? $this->defaultParameters[$key]
?? null;

if ($value === '' && isset($defaultParameter)) {
$namedParameters[$key] = $defaultParameter;
}
}

Expand Down
167 changes: 167 additions & 0 deletions tests/Routing/RoutingUrlGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2000,6 +2000,173 @@ public function testUrlGenerationWithOptionalParameters(): void
$url->route('tenantPostUserOptionalMethod', ['concreteTenant', 'concretePost', 'concreteUser', 'concreteMethod']),
);
}

/**
* Test the fix for route parameter resolution with URL::defaults() and model key binding fields.
*/
public function testRouteParameterResolutionWithDefaultsAndBindingFields(): void
{
$url = new UrlGenerator(
$routes = new RouteCollection,
Request::create('https://www.foo.com/')
);

/**
* Test case 1: Route with optional parameter with binding field.
*/
$url->defaults([
'team' => 'example-team',
]);

$route = new Route(['GET'], '{team:slug?}/posts/{post}', ['as' => 'posts.show', fn () => '']);
$routes->add($route);

// Test with positional parameters
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show', [123]),
);

// Test with named parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show', ['post' => 123]),
);

// Test with explicit team parameter
$this->assertSame(
'https://www.foo.com/another-team/posts/123',
$url->route('posts.show', ['team' => 'another-team', 'post' => 123]),
);

/**
* Test case 2: Route with required parameter with binding field.
*/
$route = new Route(['GET'], '{team:slug}/posts/{post}', ['as' => 'posts.show.required', fn () => '']);
$routes->add($route);

// Test with positional parameters
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show.required', [123]),
);

// Test with named parameters
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show.required', ['post' => 123]),
);

/**
* Test case 3: Route with multiple parameters with binding fields.
*/
$url->defaults([
'team' => 'example-team',
'team:slug' => 'example-team',
'post' => '123',
]);

$route = new Route(['GET'], '{team:slug?}/posts/{post:id}/comments/{comment}', ['as' => 'posts.show.multi', fn () => '']);
$routes->add($route);

// Test with positional parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123/comments/456',
$url->route('posts.show.multi', [456]),
);

// Test with named parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123/comments/456',
$url->route('posts.show.multi', ['comment' => 456]),
);

/**
* Test case 4: Route with mixed parameter types (some with defaults, some without).
*/
$route = new Route(['GET'], '{team:slug?}/posts/{post}/comments/{comment}', ['as' => 'comments.show', fn () => '']);
$routes->add($route);

// Test with positional parameters
$this->assertSame(
'https://www.foo.com/example-team/posts/123/comments/456',
$url->route('comments.show', [123, 456]),
);

// Test with named parameters
$this->assertSame(
'https://www.foo.com/example-team/posts/123/comments/456',
$url->route('comments.show', ['post' => 123, 'comment' => 456]),
);

/**
* Test case 5: Route with only binding field default (no regular parameter default).
*/
$url->defaults([
'team:slug' => 'example-team',
]);

$route = new Route(['GET'], '{team:slug?}/posts/{post}', ['as' => 'posts.show.binding-only', fn () => '']);
$routes->add($route);

// Test with positional parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show.binding-only', [123]),
);

// Test with named parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show.binding-only', ['post' => 123]),
);

/**
* Test case 6: Route with only regular parameter default (no binding field default).
*/
$url->defaults([
'team' => 'example-team',
]);

$route = new Route(['GET'], '{team:slug?}/posts/{post}', ['as' => 'posts.show.regular-only', fn () => '']);
$routes->add($route);

// Test with positional parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show.regular-only', [123]),
);

// Test with named parameter
$this->assertSame(
'https://www.foo.com/example-team/posts/123',
$url->route('posts.show.regular-only', ['post' => 123]),
);

/**
* Test case 7: Route without any defaults to ensure existing functionality works.
*/
$route = new Route(['GET'], '{team:slug}/posts/{post}', ['as' => 'posts.show.no-defaults', fn () => '']);
$routes->add($route);

// Test with positional parameters
$this->assertSame(
'https://www.foo.com/another-team/posts/123',
$url->route('posts.show.no-defaults', ['another-team', 123]),
);

// Test with named parameters
$this->assertSame(
'https://www.foo.com/another-team/posts/123',
$url->route('posts.show.no-defaults', ['team' => 'another-team', 'post' => 123]),
);

// Test with mixed parameters
$this->assertSame(
'https://www.foo.com/another-team/posts/123',
$url->route('posts.show.no-defaults', ['team' => 'another-team', 123]),
);
}
}

class RoutableInterfaceStub implements UrlRoutable
Expand Down
Loading