[9.x] Use model route key when route parameter does not specifiy custom binding field but a different parameter does #42425
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Note: I'm unsure to which branch I should send this bugfix since the implementation of
RouteUri
has changed between8.x
and9.x
.9.x
usesstr_contains
instead ofstrpos
. I might have to send two separate PRs to each respective branch? Help? 😅Summary
This PR fixes a special circumstance where the wrong model route key would get used when substituting route parameters. For a tl;dr of what the bug is, check out the test I added to
RoutingUrlGeneratorTest.php
that reproduces the error. For the longer explanation, keep reading.Reproducing the bug
Let's say I have two models,
User
andPost
. TheUser
model overrides thegetRouteKeyName
method so the user'susername
field gets used during route generation instead of theid
. ThePost
model is kept vanilla.Now, let's define a route like this.
We specify an explicit binding field for the
post
so it gets scoped on the user. For the user, we don't specify an explicit binding key, so we expect it to use theusername
like we've configured. This works as expected if we generate the route url and pass the parameters as an associative array.However, if we pass the parameters without explicit keys it breaks.
If we explicitly define the binding field for the
user
parameter as well, it works again as expected.I have added a test to the
RoutingUrlGeneratorTest
file that reproduces this error.Why the bug happens
When the
RouteUri
class parses the URI, it extracts all binding fields from the URI. This array then gets saved on theRoute
object's$bindingFields
property. So our URI ofgets turned into
So far so good, but note that the
user
parameter is missing since it doesn't define a an explicit binding field (no spoilers, but this is important).When the
UrlGenerator
tries to resolve the route parameters, it loops of the parameters and calls thebindingFieldFor
method of theRoute
object for each of them (https://github.com/laravel/framework/blob/9.x/src/Illuminate/Routing/UrlGenerator.php#L483).This method is passed the key of the current parameter we're looping over. It then checks if that key is an integer, and if so, extracts the binding field from the route's
bindingFields
array by index.Going back to our route above, the
bindingFields
array currently looks like this.But since it's trying to look up the binding field by index, it ends doing this instead.
Since
user
is the first parameter we passed to theroute
call, it tries to look up the binding field with index0
in this array. Which is why theid
field gets mistakenly used for theuser
parameter.Also, at this point "binding fields" has stopped sounding like a word.
Changes
To fix this bug, I changed the
parse
method of theRouteUri
class to keep track of all parameters, instead of only parameters with an explicit binding field. So when we first parse our URI, we get back this instead:Before returning, I check if this array contains any values that are not
null
, meaning we have at least one explicit binDinG fIeLd. In that case, we keep the whole array—includingnull
values—so the index positions line up as expected when callingarray_values
on it. In any other case, we simply return an empty array (like before).Other changes
I've refactored the tests for the
RouteUri
class to use a data provider instead of having all assertions in a single test. That made figuring out which test case actually broke quite a bit easier.