-
Notifications
You must be signed in to change notification settings - Fork 11.1k
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
[9.x] Fix isset()
throwing when strict mode is enabled
#44642
Conversation
I'm honestly getting a bit worried about strict mode as a concept. 🫤 @inxilpro thoughts? |
@axlon can you PR a test for this? |
@taylorotwell I would be OK with this solution as a compromise if most folks agree with it. But I'm not certain most folks do. I know @aarondfrancis and @donnysim chimed in on the other PR. I wonder who else has an opinion. I also would like to clarify, this will not throw an exception:
The bug referenced in #44558 comes down to the fact that the mailable is trying to access (As an aside, I generally dislike the current trend towards strict types, private final readonly nonsense everywhere. I think that this particular feature is a case where "strictness" is useful—it's guarding against an actual, real-world problem, rather than protecting you from theoretically passing a string to an
Models are arguably different from other objects. They represent data stored elsewhere. They come with the ability to only retrieve a portion of that data. This means that the semantics of I can say from my experience with a very large Laravel app (100,000's of lines of code, 10,000's of lines of tests), turning |
@taylorotwell I'll send in additional tests (and fix the current one because it contains a false positive) today @inxilpro in my tiredness (it was almost 12AM when I wrote all this), I seem to have oversimplified my example. But in reality it doesn't matter, the |
@axlon Your sayHello(new User());
sayHello(User::create(['email' => 'e@mail.com']));
sayHello(User::find(1));
sayHello(User::inRandomOrder()->first()); The only case that would break is: sayHello(User::select('email')->first()); And in this case, it should break (when strict mode is on), because your function is interacting with the |
I agree with @inxilpro on this one. First, I should say that this PR is an ok tradeoff and if it makes the Laravel team's jobs easier, let's merge it. I don't agree that it's technically correct though. Strict mode is opt in. And when you opt in there are going to be some behaviors you have to work around. The thing I keep coming back to is that if you call Is the attribute set? We don't know, because it wasn't selected from the database. And you may say "well it doesn't matter, we can just treat it as null!" but what if it isn't really null? The entire point of the If it helps, let's forget not selecting from the database altogether. Let's consider a typo or a renamed attribute. If you had an attribute named // Oops, used the old name. No warning!
if (isset($user->blocked)) {
//
} Or if you misspell it: // Oops, typo. No warning!
if (isset($user->is_blokced)) {
//
} I also think the argument that Change the example to function calculateTax(User $user, $amount)
{
if (!isset($user->tax_rate)) {
return 0;
}
return $user->tax_rate * $amount;
}
$user = User::select('id')->first();
calculateTax($user, 100);
At the end of the day I think the |
I'll be honest I'm more confused why people use isset on an attribute when you can just compare with null or if you into them functions - is_null, you get the same result, though I use custom hasAttribute function. As I mentioned in original PR I've used strict mode for couple years now and it's either you agree that it throws or you don't enable it, there should be no in between for what it tries to achieve or solve. I even leave it turned on on production without any problems. I think it's either, the framework commits to it and adds explicit checks for attribute existence in codebase or doesn't and should remove this to avoid further problems. But hey, for me strictness was never a problem, even in production, heck I would love to have a super lightweight model without all the shenanigans that current implementation has, and I especially dislike not removing outdated features (old way of attribute mutators etc). You just have to adapt to it, the same way I've never used ==, always strict comparison, casts etc., never has been a root cause of a problem if you ensure what you work with is what you expect. I think people are too stubborn to adjust their code to work with it, for what it's goal is. |
I'm glad to see this has been merged. The whole purpose behind When using the null-coalescing operator, this uses $user = new stdClass;
$name = $user->name; // PHP throws
$name = $user->name ?? 'Blah'; // PHP doesn't throw I think PHP's default behaviour here is reasonable given the purpose of the null-coalescing operator. Maybe in an ideal world when defining A middle ground here is to explicitly use $user = User::select(['id'])->first();
$name = $user->getAttribute('name') ?? 'blah'; // MissingAttributeException
$user->name = null;
$name = $user->getAttribute('name') ?? 'blah'; // blah It's a welcome sight seeing stricter attributes and lazy loading in Laravel! 😀 |
So put more thought into it, yeah I can see why you wouldn't want to throw on isset, but at the same time it's just avoiding the point that null can still mean that it exists. I think the big culprit here might be offsetExists checking for null value here instead if it actually exists or not - null doesn't mean it doesn't exist. Also mutators returning null should never be held as non-existing but throw if they access an attribute that is not present without ensuring it's existance. So I think the whole offset exists concept here is incorrect at the fundamental level that prevents proper overall behavior. I kind of don't know now, it might be that the model is too bloated to properly support both scenarios and will have to take a stance whether it throws on isset or silently hides mutator etc. problems with try/catch, because what most people are talking is about direct attribute access, there's also mutators etc. that also access those attributes and this change hides those errors if used with isset etc.. The isset might fail not because your attribute is missing, but because the mutator accessed other attributes that are not present. |
And as a side note, if the offset does exist, in current situation you're eating a performance penalty because offsetGet executes the same logic all over again. |
That's the way $a = null;
dd(isset($a)); // false I think what your describing is
Eloquent models aren't proper objects with defined properties, it's all magically handled in So to handle $user = new User;
$user->name = null;
array_key_exists('name', $user->getAttributes()); // true
array_key_exists('email', $user->getAttributes()); // false This PR doesn't change the way it's worked in Laravel for many years, it only prevents |
Well, you're just confirming my point - people are only thinking about direct attributes, there's more than just attributes we're dealing here - it includes mutators. public function getTitleAttribute()
{
return $this->name ?: $this->email;
}
$user = User::query()->first(['id', 'email']);
// what do you actually expect here from?
isset($user->title); It returns false because |
Am currently checking out a recent bug in our app code, and while debugging it, I traced it back to this PR being the breaking change 😬 TL;DR: Very unexpected that the opt-in strict mode setting suddenly started returning Our case: In a resource class, we want to use the distance if it's defined/set: return [
'name' => $this->resource->name,
'distance' => $this->resource->distance ?? null,
]; On staging and production, we report these to Sentry using a custom callback ( With
The
|
Yeah, it's definitely a bummer that this got so contentious. As far as I can tell, Taylor isn't interested in touching strict mode anymore, so I don't think it's likely that this will get fixed. I think the only option would be to implement |
It's indeed super unfortunate that so many got hit by changing behavior. We're now at a point where we don't want to modify or touch anything about this anymore to stop just that. |
Fixes #44558
Previously discussed in #44283 and #44558
Trying to describe the problem
Recently a new feature was introduced to prevent developers from accidentally accessing attributes that don't exist on their models. While I think this is a really great feature to have, I currently find it unusable due to how it handles
isset()
. Isset allows developers to 'test' a variable or property before actually using it, preventing errors. For example:Strict models changes this up, by suddenly having models act differently from all other objects (any I've come across anyway):
Since Laravel presents models like normal objects with normal properties, it feels very weird when they suddenly don't act like them.
It is still possible to work around all this, but you end up with weird constructions because you have to be sure you're dealing with a model, e.g.:
But if
isset()
still worked like normal, this code could have been a lot shorter:I think its also noteworthy to point out that
??
and??=
also make use ofisset()
under the hood. This means they can no longer be used on models freely, because it opens you up to potential errors when strict mode is enabled (as is evident by #44558)What does this PR do?
This PR makes
isset()
returnfalse
when testing an uninitialized model attribute in strict-mode. This creates parity between strict-mode models, and 'regular' objects with typed properties; accessing an uninitialized property will throw an error, testing a uninitialized property will not.This also immediately fixes the issue linked above, because the framework (and packages) can go back to treating models like regular objects.