Skip to content

[9.x] Keep a copy of a model's $original attributes after updating via $previous #42504

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

Closed
wants to merge 8 commits into from
Closed

[9.x] Keep a copy of a model's $original attributes after updating via $previous #42504

wants to merge 8 commits into from

Conversation

stevebauman
Copy link
Contributor

@stevebauman stevebauman commented May 25, 2022

Description

This PR provides the ability to retrieve model's previous attribute value(s) after it has been successfully updated.

This is super useful, because we can retrieve these values for auditing purposes, or inside of events to determine what a previous value of an attribute was -- after a model has been updated successfully.

Example

Let's say we have an application where we want to track changes made to a user when they have been updated successfully. Without implementing the functionality ourselves by making a copy the model's $original attributes prior to an update, it is not possible to retrieve an attribute's previous value using any built-in attributes. $original is reset, and $changes contains attributes that were modified with their new values only.

However, with $original attributes synchronized to $previous after a successful update, we can use both $changes and $previous to perform this example auditing task:

use Illuminate\Database\Eloquent\TracksPreviousAttributes;

class User extends Model
{
    use TracksPreviousAttributes;
}
// Somewhere in an application, far far away...

$user->save();

event(new UserUpdated($user));
// app/Listeners/TrackUserChanges.php

public function handle(UserUpdated $event)
{
    foreach ($user->getChanges() as $attribute => $newValue) {
        $user->changes()->create([
            'attribute' => $attribute,
            'old_value' => $user->getPrevious($attribute),
            'new_value' => $newValue,
        ]);
    }
}

Notes

I've made a copy of all the "original attribute cast" tests to ensure it handle's casts identically. Let me know if you'd like this adjusted.


I feel like this is the remaining piece to fully built-in attribute tracking in Laravel. I think many would find this useful!

@taylorotwell
Copy link
Member

I don't mind revisiting this, but just linking this here for reference for prior discussion on the topic which I'll need to review as well to get up to speed with all of the caveats people brought up: #37434

@stevebauman
Copy link
Contributor Author

Understood! It seems that PR implemented some controversial helper methods.

This PR is a mirror of getOriginal(), but uses an old copy of the $original attributes. I personally think anything extra is out of scope for Laravel to handle.

@rodrigopedra
Copy link
Contributor

Couldn't this be an opt-in feature by adding a trait?

All pieces could be extracted to a trait, including the $this->syncPrevious(); call which happens just before the updated model event is fired, so it could be registered in a listener on the trait's boot method.

The reason I am suggesting this to be opt-in, is when models have attributes holding large chunks of data, for example a Post model's content attribute.

This concern was first raised on the PR referenced by Taylor here: #37434 (comment)

So instead of providing an opt-out mechanism, for users who don't want the memory penalty, and can make the implementation much harder, it could be an opt-in feature, much like how we already have for email confirmation on the User model.

@DarkGhostHunter
Copy link
Contributor

This should be opt-in. An app could have a huge model, and being this by default could easily make PHP crash because a model is using now double the memory footprint.

I think the best way to approach this is with a TracksChanges trait to avoid someone having their own colliding methods on the model itself.

class Article extends Model
{
    use TracksChanges;

    // ...
}

From there, it would be easier to keep track of the attributes that changed on save, and check which where changed, from what and to what.

$model->attributeChanged('foo');
$model->attributeChangedTo('foo', 'bar');
$model->attributeChangedFrom('foo', 'baz');

But again, it should be opt in through a Trait to avoid collisions.

@stevebauman
Copy link
Contributor Author

I'm totally fine with making it opt-in. If we're worried about memory issues, we could only store values for attributes that were changed via $changes?

@taylorotwell
Copy link
Member

I'm OK with this being a trait as well.

@taylorotwell taylorotwell marked this pull request as draft June 1, 2022 14:23
@stevebauman stevebauman marked this pull request as ready for review June 1, 2022 15:16
@stevebauman
Copy link
Contributor Author

Moved to a TracksPreviousAttributes trait. Let me know if you'd like anything adjusted!

@taylorotwell
Copy link
Member

I may let this exist as a community package for now since it can just be a trait. Little nervous to adopt it in the core at the moment. Thanks 🙏

@stevebauman
Copy link
Contributor Author

Ok no worries! ❤️ Thanks for the reply.

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.

4 participants