[5.9] Add ability to define models with 'strict property' getter#29089
[5.9] Add ability to define models with 'strict property' getter#29089garygreen wants to merge 1 commit intolaravel:masterfrom
Conversation
cc2d615 to
914a6aa
Compare
|
In practice this has problems with not-yet persisted models. Unless you also define all default values for attributes in But this may be legit, e.g. I feel at this stage this might be better a package with a trait (if technically possible). It sure sounds intriguing though 😄 |
|
Btw, I've never had such problems due to the following (I know they can all be counter-argued):
|
This doesn't have any impact on not yet persisted models - it's only when you go to access properties that aren't defined it will error.
Yes exactly, it will "bomb" and that's the intended behaviour here. If you've attempted to access a property that hasn't been defined then you could run into weird obscure bugs as outlined in original post. "boming" is exactly how PHP handles accessing undefined properties on a object. Also consider the fact that trying to call an undefined function on your model also "bombs" - let's flip the coin and suggest that calling undefined functions just silently fails - that too could lead to weird and unexpected behaviour. IMO properties should be just as strict.
That's the real trade off here, for nullable columns there will be a bit of overhead to make sure they are defined before being accessed, but the safety here far outweighs this marginal overhead, coupled with the fact that this is an opt-in feature.
👍 😄 |
|
As for the comments about php stan and other static analysis tools etc, those are definitely interesting, been meaning to check them out - though I suspect it's very much tied with your IDE/a very particular workflow, and I do question why is it nesscary to have extra tooling for some sensible safety that can be provided out the box? |
914a6aa to
61bb672
Compare
|
@mfn can you expand on your comment that this causes problems with un-persisted models? |
|
Assume the $user = new User();
echo $user->internal_comment; // 💥 That access will throw the exception. Basically all attribute access on models not loaded with all columns from the DB are prone to this. To "fix" this, realistically you have to use FTR I'm not rallying against this feature but it should be known how it behaves in all scenarios. This needs to be added to the docs too. |
|
It breaks the following code: // Assume we've $relation (another model) loaded _from_ the database with all fields)
$model = new Model();
$model->relation()->associate($relation); // 💥 The expected behaviour is that after the call to
However it throws: Must be due to some internal access to |
|
In @mfn's example, |
|
@mfn I'm not sure I follow you, but if I do I guess what you're describing is still desired behavior. This seems to have one caveat though: Eloquent is unable to cast attributes to Value Object, which limits the capability of the Nullable Pattern. |
associate() problemI'm not sure why Throwing in
|
|
It also breaks Comment::with('commentable')->get();
Country::has('posts')->get();These cases and @mfn's example could be fixed quite easily. |
IMO, the code is perfectly valid: $post = new Post;
$post->associate(User::find($id));
$post->save(); |
| } | ||
|
|
||
| return $this->getRelationValue($key); | ||
| if ($this->relationLoaded($key) || method_exists($this, $key)) { |
There was a problem hiding this comment.
Can we find a better solution for this? We shouldn't have to run the same checks twice.
Can we throw a PropertyNotFoundException in getRelationValue() and catch it here?
There was a problem hiding this comment.
I agree it's not ideal, was trying to find a way of avoiding it doing the check twice so any suggestions welcome. Moving the throw to getRelationValue wouldn't be an option for a few reasons - 1. that method is public so it changes the behaviour of that method when used in isolation, 2. if your extending & overriding that method again it changes the behaviour, 3. it's a bit quirky to me to throw a PropertyNotFoundException in a method that's specifically related to handling getting a relation's value and has nothing to do with attributes on the model.
I would imagine that these checks relationLoaded and method_exists are pretty quick anyway.
There was a problem hiding this comment.
- that method is public so it changes the behaviour of that method when used in isolation
I think it would be desirable to have getRelationValue() respect $strictProperties. After all, the setting is called $strictProperties, not $strictAttributes.
|
Honestly since the risk of this breaking code seems quite high and I personally don't feel like chasing all of that down, I suggest releasing this as a package which ships a base model people can extend instead of the regular Eloquent base model. |
|
@garygreen do you have any further plans following up on the problematic cases far? I might have come over as counter-interested but in fact I am interested. But as we found out, basic use with eloquent relations is problematic. |
Yeah the main issue was Laravel internally relies on the functionality of models to come back with As Taylor mentioned, we would have to chase down all of those cases in the framework. We would have to decide upon a consistent coding pattern in Laravel which has strict properties in mind. I outlined a few suggested behaviours in a comment above, so Another technical issue was with unit testing. We should test how the model behaves across all unit tests with So yeah, there's still a few technical challenges to address before can benefit from stricter property accessors, I still think it would be a nice addition to the framework. |
Background
By default PHP will throw an exception when you attempt to access an unknown/undefined property:
The current problem
Models however will allow this behaviour which can sometimes cause some nasty bugs in your code. Take for example this rather innocent looking example:
See anything wrong with this? On the face of it looks like this code is pretty flawless but let's see what happens when someone actually get's logged in:
Although this may seem like a bit of a construed example, as in reality you wouldn't do a partial select on some fields when logging in, but it goes to show that some innocent looking code can go massively wrong just by silently returning
nullfrom models for properties that haven't been defined at all.Another example
A simple mistake that goes unnoticed because of silent errors can make it into production:
Yet another example
Let's say your database sets a default value for a certain column in MySQL (maybe not considered best practise, but it's still a common paradigm)
When you create a new model:
Solution with this PR
With this PR, you can opt-in to strict properties on your model:
Upon accessing any undefined properties it will now throw a
PropertyNotFoundException. In the case of the above examples, you would of instantly caught any of the above issues early during development.I've implemented this in production and have been suprised how many coding obscurities this has revealed.
I understand this can be implemented in your own userland code by overloading the
getAttributemethod, but I think this strict opt-in approach to accessing properties would benefit the community overall, so hope you will consider this PR for inclusion in Laravel.