Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.x] Prevent implicit route model binding parameter name mismatches in controllers #51380

Draft
wants to merge 4 commits into
base: 11.x
Choose a base branch
from

Conversation

stevebauman
Copy link
Contributor

@stevebauman stevebauman commented May 10, 2024

This PR is based on a question I asked on X, where I somewhat frequently scratch my head as to why a route model binding isn't working and a new model instance is tossed in via dependency injection. It's obvious when you show the routes together with the controller method (as you'll see below), but it's an easy mistake to make when you're in one file working with models named two or more words (PhoneNumber, UserInvitation, ActivityLog, etc.):

https://twitter.com/ste_bau/status/1788976209943986419

Route::name('show')->get( 
    'phone-numbers/{phoneNumber}', 
    [PhoneNumberController::class, 'show'] 
); 

// ... 

public function show(PhoneNumber $number) // Named incorrectly - new instance created
{ 
    $number->exists; // false ❌
} 

public function show(PhoneNumber $phoneNumber) // Named correctly - existing model instance given
{ 
    $phoneNumber->exists; // true ✅
} 

This PR implements a way to enable throwing an exception when a model (similarly to Model::shouldBeStrict()) binding is missed:

\Illuminate\Routing\ImplicitRouteBinding::preventModelParameterMismatch();

Then, when the first scenario above occurs, developers will immediately see the exception with clarity:

public function show(PhoneNumber $number) // ❌ throws ModelParameterMismatchException
{ 
    // ...
}
ModelParameterMismatchException: Route parameter name 'number' does not match expected model binding name 'phoneNumber'.

This PR doesn't affect nullable controller parameters:

public function show(PhoneNumber $number = null); // ✅

It also does not affect explicit model bindings (as these are performed prior to implicit):

Route::model('number', PhoneNumber::class); // ✅

Let me know your thoughts! No hard feelings on closure ❤️ .

*
* @var bool
*/
protected static $shouldPreventModelParameterMismatch = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There may be a better name for this property, as well as the method below it.

@ralphjsmit
Copy link
Contributor

ralphjsmit commented May 10, 2024

I like the PR, nice work! It can definitely be very confusing that the container sometimes generates empty Eloquent models that don't exist in some places that are implicit (like route model binding in your case). I would even argue that this setting could go on the container, as a way of telling the container "never instantiate an empty non existent Eloquent model". The chances that instantiating such a model is erroneous are much higher than that a developer is actually doing app(User::class)->fill(...)->save() to name sth. Everyone would call Eloquent models statically User::create(...).

@sharryy-s
Copy link

I loved this PR. I have personally bitten by it several times.

@megasteve19
Copy link

This should definitely exist. Sometimes, you get trapped in this situation, and after hours of debugging, you realize it was just a wrong naming..

@ashikhasnat
Copy link

It should be added. At first, I didn’t know the name needed to be the same! I thought the model name would be enough. After a few incidents, I just stopped using route model bindings.

{
if (
! $parameter->isDefaultValueAvailable() &&
static::$shouldPreventModelParameterMismatch &&

Choose a reason for hiding this comment

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

Maybe better to start the if with static::$shouldPreventModelParameterMismatch to avoid performance hits if this is functionality is disabled

@hailwood
Copy link
Contributor

Should this be tied in with Model::shouldBeStrict() I feel like it's related.

@taylorotwell taylorotwell marked this pull request as draft May 13, 2024 18:09
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.

None yet

7 participants