Use of getOriginal() on model in observer doesn't return original value, when using transactions and ShouldHandleEventsAfterCommit. #52552
Replies: 5 comments 29 replies
-
|
It seems that you're dealing with a situation where the model object being tracked within a transaction is modified, which affects the behavior of the callbacks assigned to that transaction. This can lead to issues, especially when working with models in the context of a transaction, particularly in conjunction with events like updated. In short, when a transaction is registered, Laravel creates callbacks that will be executed after the transaction is committed. If the model is modified during the transaction and Laravel uses the same object for the registered callbacks, this can lead to a scenario where the callbacks operate on outdated or inconsistent data. In PHP, objects are passed by reference, meaning that if you modify an object in one place, those changes will be reflected everywhere that object is used unless you specifically clone the object before making changes. |
Beta Was this translation helpful? Give feedback.
-
|
This is a big issue when building a logging mechanism for registering model changes, imagine this pretty common example: Model to watch class Order extends Model{
protected $fillable = [
'state_id', // ...
];
}Model to log changes #[ObservedBy(OrderObserver::class)]
class OrderStates extends Model{
protected $fillable = [
'order_id', 'old_state_id', 'new_state_id', // ...
];
public function order(): BelongsTo
{
return $this->belongsTo(Order::class, 'order_id', 'id');
}
}Observer code class OrderObserver implements ShouldHandleEventsAfterCommit
{
public function updated(Order $model) // tried also updating with no luck
{
$new_state_id = $model->state_id;
$previous_state_id = $model->getOriginal('state_id'));
// here I would like to log the change in a OrderStates entry
// but real previous state_id is lost when wrapping the change in a transaction...
}
}Example code DB::beginTransaction;
$order = Order::find(1);
$order->update(['state_id' => 2]);
// other relevant code...
DB::commit();Is there any other clean way to do this? |
Beta Was this translation helpful? Give feedback.
-
|
Solution
Thank you for this. We had thoughts of using this ShouldHandleEventsAfterCommit but we never used it. Lucky us. |
Beta Was this translation helpful? Give feedback.
-
|
I have the same issue. Are there any updates? |
Beta Was this translation helpful? Give feedback.
-
|
I believe I've found a working solution for this! Avoid using ShouldHandleEventsAfterCommit and instead wrap any observer actions within manual DB::afterCommit() callbacks. Using your example, modify the provided Observer like the following; <?php
namespace Tests;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Assert;
class Observer
{
public function updated(Test $model)
{
$original = $model->getOriginal('test');
DB::afterCommit(function () use ($model, $original) {
Assert::assertEquals('updated', $model->test);
Assert::assertEquals('original', $original);
});
}
}This maintains proper event firing order (saving --> updating --> SQL executed --> updated) for both transactions & non-transactions (these will be executed immediately) and retains original model values, all while ensuring that any external actions still only happen after the SQL is committed to the database. I was building something similar to what @yurik94 had suggested, an outbox-style logging system that was triggered from both transactions and non-transactions within my codebase. Interestingly (or frustratingly depending on how you look at it), assuming your observer is firing from a transaction AND you're only making DB changes, since the observer is ran on the same PHP thread the transaction rollback will properly undo any db changes. This flow retains atomicity, however if the observer can also be fired from outside of a transaction and the changes fail to commit to your database you will introduce ghost actions that shouldn't have occurred, breaking the atomicity. By not using ShouldHandleEventsAfterCommit and instead using manual DB::afterCommit we are able to have the best of both worlds, atomicity on the save and atomicity on the executed observer actions. Here's a real world example; Situation: When a user's address is updated I want to send an email to them indicating changes were made while including the original address in the email. <?php
namespace App\Observers;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Mail\AddressChangedEmail;
class Observer
{
public function updated(User $user)
{
if ($user->wasChanged('address')) {
$originalAddress = $user->getOriginal('address');
DB::afterCommit(function() use ($user, $originalAddress) {
Mail::to([$user->email])->send(new AddressChangedEmail($user, $originalAddress));
});
}
}
}This solved the problem for me! |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Laravel Version
11.20
PHP Version
8.3
Database Driver & Version
sqlite
Description
When using and observer to subscribe to
updatedevents on models, you can access the original value in a few different ways, including thegetOriginal(...)method.If the observer implements the
ShouldHandleEventsAfterCommitand the changes to the model is wrapped in a databas transaction, the events should be processed after the transaction has commited. This works as expected.However. When using transactions and
ShouldHandleEventsAfterCommitthegetOriginal(...)method no longer returns the original value.I did not expect this, and can't find anything about it in the documentation. Is this a bug?
Steps To Reproduce
Below are test cases for this. It consists of:
updated, asserts that the value of$model->testisupdatedand the value of$model->getOriginal('test')isoriginaltestset tooriginaland then updates that field toupdated.I would expect both of these test cases to pass, but the test case that uses transactions fails.
Beta Was this translation helpful? Give feedback.
All reactions