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

Don't break with Model::shouldBeStrict() enabled #18

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

nick-potts
Copy link

@nick-potts nick-potts changed the title Don't break with Model::shouldBeStrict()` enabled Don't break with Model::shouldBeStrict() enabled Apr 6, 2024
@stancl
Copy link
Member

stancl commented Apr 6, 2024

Does this PR address the same thing as this?

protected static $modelsShouldPreventAccessingMissingAttributes = false;

@nick-potts
Copy link
Author

Yes and no. I have strict enabled project wide in dev, as is somewhat common.

I would prefer strict stays enabled so the model behaves the same as everything else in my project, but there's some personal preference in there.

@stancl
Copy link
Member

stancl commented Apr 6, 2024

Would need a more detailed description of what exactly this PR is solving then, the test doesn't add much info.

@nick-potts
Copy link
Author

Will in the AM

@nick-potts
Copy link
Author

In my case, I have Model::shouldBeStrict() enabled.

Here's a quick explanation of the specific one that's causing this issue:
https://planetscale.com/blog/laravels-safety-mechanisms#attribute-typos-and-renamed-columns

For example, I do have this on my user model, and I would like to ensure that custom columns are indeed loaded. The issue is, I'm not able to do User::pluck('id)->toArray(), as this package tries to decode an unloaded data column.

@stancl
Copy link
Member

stancl commented Apr 8, 2024

I see. The snippet I posted above is what we use in Tenancy for Laravel for use cases where you might do e.g.:

if ($tenant->logo) { ... }

as a sort of "has attribute" check.

The case you're describing here is about models that haven't been saved to the DB yet? Or I guess when they're being created at first. Once they're pulled from the DB, the data column should always be initialized since it should have a default value of an empty JSON object.

@nick-potts
Copy link
Author

nick-potts commented Apr 8, 2024

I'm purely running a Order::query()->pluck('id');

the only attribute that is fetched by the database is the id, but this package is trying to decode the data column and it causes it to break.

The test I wrote demonstrates that behaviour.

@stancl
Copy link
Member

stancl commented Apr 8, 2024

I see now, so some ways of fetching the model completely omit the data column. Could we use hasAttribute() or something similar for an early return instead of the try-catch?

@nick-potts
Copy link
Author

Actually just checking the column exists and returning early in the retrieved event listener works. Would also help stop its current behaviour of silently failing when someone does a pluck('custom') or a get(['custom'])

@stancl
Copy link
Member

stancl commented Apr 8, 2024

Actually just checking the column exists and returning early in the retrieved event listener works. Would also help stop its current behaviour of silently failing when someone does a pluck('custom') or a get(['custom'])

Can you expand on the behavior difference? What you mean by silently failing specifically, compared to the behavior after the change

@nick-potts
Copy link
Author

When you do a pluck('id') call, the current behaviour is to always decode no matter what data came from the database.

So the decodeVirtualColumn will always run, the getAttribute(static::getDataColumn()) will return null and get coalesced to [].

This change now checks if the data column exists before running decodeVirtualColumn.

@nick-potts
Copy link
Author

nick-potts commented Apr 8, 2024

An unintended behaviour of this is that you can accidentally wipe all the custom data from a column without being careful.

        FooModel::create([
            'id' => 1,
            'virtual' => "data I don't want to lose" // in the `data` column
        ]);

        $foo = FooModel::query()->first(['id']);
        $foo->bar = 'baz'; // save to the `data` column
        $foo->save();

        $foo = FooModel::query()->first();
        $this->assertSame('data I don't want to lose', $foo->virtual);
    }

This would actually fail because only bar would be persisted.

I've added a proof of concept that checks if the data was loaded from the database. There might be a better way to do it.

@stancl
Copy link
Member

stancl commented Apr 8, 2024

That makes sense. So in the try-catch implementation, the data attribute got added to Thing::pluck('id'), decodeVirtualColumn() failed, leaving data null, which got cast to [] and would result in the data being lost if save() was called on that model.

Whereas now with the listener not being triggered, the data column doesn't get added at all?

Though then I don't get what the last change is for, I think I'm misunderstanding which implementation (if any) you're saying would prevent the data column from getting set to [].

@nick-potts
Copy link
Author

The last change marks when you choose to selectively pull columns from the database, doesn't decode and prevents you from trying to write when the data column when saving.

@nick-potts
Copy link
Author

I'd be fine with not including the save side of things

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

2 participants