From 515b70d63f5d5d6ff7b188422c683c8dcf03a2b2 Mon Sep 17 00:00:00 2001 From: Thijs Kok Date: Tue, 21 Jul 2026 22:50:58 +0200 Subject: [PATCH 1/2] Enhance rollback event listeners to include target revision for mutation and verification --- src/Concerns/HasRevisions.php | 25 ++++++++++++- tests/RevisionEventsTest.php | 69 ++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/Concerns/HasRevisions.php b/src/Concerns/HasRevisions.php index 824b6cb..8908162 100644 --- a/src/Concerns/HasRevisions.php +++ b/src/Concerns/HasRevisions.php @@ -108,6 +108,7 @@ public static function revisioned(Closure $callback): void /** * Register a listener for the rollingBack event, which fires before a rollback is performed. + * Receives the model and the target revision; mutating the revision here affects the restore. * Return false from the callback to abort the rollback. */ public static function rollingBack(Closure $callback): void @@ -117,6 +118,7 @@ public static function rollingBack(Closure $callback): void /** * Register a listener for the rolledBack event, which fires after a rollback is performed. + * Receives the model and the revision it was rolled back to. */ public static function rolledBack(Closure $callback): void { @@ -280,7 +282,7 @@ public function rollback(): bool */ public function rollbackToRevision(RevisionContract $revision): bool { - if ($this->fireModelEvent('rollingBack') === false) { + if ($this->fireRevisionEvent('rollingBack', true, $revision) === false) { return false; } @@ -299,7 +301,7 @@ public function rollbackToRevision(RevisionContract $revision): bool $this->saveAsRollbackRevision($options, $revision); } - $this->fireModelEvent('rolledBack', false); + $this->fireRevisionEvent('rolledBack', false, $revision); return $result; } @@ -465,6 +467,25 @@ protected function revisionToReplace(): ?Revision return $this->latestRevision()->first(); } + /** + * Fire an observable event with the model and revision, dispatching directly instead of + * via fireModelEvent() so third-party overrides of that method (e.g. laravel-pivot-events) + * don't interfere. + */ + protected function fireRevisionEvent(string $event, bool $halt, RevisionContract $revision): mixed + { + $dispatcher = static::getEventDispatcher(); + + if (! $dispatcher) { + return true; + } + + return $dispatcher->{$halt ? 'until' : 'dispatch'}( + "eloquent.{$event}: ".static::class, + [$this, $revision] + ); + } + /** * Save a rollback revision, always as a new record and marked as a rollback. */ diff --git a/tests/RevisionEventsTest.php b/tests/RevisionEventsTest.php index a195efe..01f97bb 100644 --- a/tests/RevisionEventsTest.php +++ b/tests/RevisionEventsTest.php @@ -75,12 +75,33 @@ public function it_fires_the_rolling_back_event_before_a_rollback() } #[Test] - public function it_fires_the_rolled_back_event_after_a_rollback() + public function it_passes_the_target_revision_to_the_rolling_back_listener() { // Given - $fired = false; - Post::rolledBack(function (Post $post) use (&$fired) { - $fired = true; + $capturedRevision = null; + Post::rollingBack(function (Post $post, Revision $revision) use (&$capturedRevision) { + $capturedRevision = $revision; + }); + $post = $this->createPost(); + $this->modifyPost($post); + + $revision = $post->revisions()->firstOrFail(); + + // When + $post->rollbackToRevision($revision); + + // Then + $this->assertTrue($revision->is($capturedRevision)); + } + + #[Test] + public function it_allows_the_rolling_back_listener_to_mutate_the_revision_before_restore() + { + // Given + Post::rollingBack(function (Post $post, Revision $revision) { + $metadata = $revision->metadata; + $metadata['attributes']['name'] = 'Overridden by listener'; + $revision->metadata = $metadata; }); $post = $this->createPost(); $this->modifyPost($post); @@ -89,7 +110,7 @@ public function it_fires_the_rolled_back_event_after_a_rollback() $post->rollbackToRevision($post->revisions()->firstOrFail()); // Then - $this->assertTrue($fired); + $this->assertEquals('Overridden by listener', $post->fresh()->name); } #[Test] @@ -107,4 +128,42 @@ public function it_does_not_roll_back_when_the_rolling_back_event_returns_false( $this->assertFalse($result); $this->assertEquals('Another post name', $post->fresh()->name); } + + #[Test] + public function it_fires_the_rolled_back_event_after_a_rollback() + { + // Given + $fired = false; + Post::rolledBack(function (Post $post) use (&$fired) { + $fired = true; + }); + $post = $this->createPost(); + $this->modifyPost($post); + + // When + $post->rollbackToRevision($post->revisions()->firstOrFail()); + + // Then + $this->assertTrue($fired); + } + + #[Test] + public function it_passes_the_target_revision_to_the_rolled_back_listener() + { + // Given + $capturedRevision = null; + Post::rolledBack(function (Post $post, Revision $revision) use (&$capturedRevision) { + $capturedRevision = $revision; + }); + $post = $this->createPost(); + $this->modifyPost($post); + + $revision = $post->revisions()->firstOrFail(); + + // When + $post->rollbackToRevision($revision); + + // Then + $this->assertTrue($revision->is($capturedRevision)); + } } From ac8fae1e36b10ccc64640fc9c2baa1d426dc430c Mon Sep 17 00:00:00 2001 From: Thijs Kok Date: Tue, 21 Jul 2026 22:52:07 +0200 Subject: [PATCH 2/2] Fix formatting in event dispatch string for clarity --- src/Concerns/HasRevisions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Concerns/HasRevisions.php b/src/Concerns/HasRevisions.php index 8908162..f40148c 100644 --- a/src/Concerns/HasRevisions.php +++ b/src/Concerns/HasRevisions.php @@ -481,7 +481,7 @@ protected function fireRevisionEvent(string $event, bool $halt, RevisionContract } return $dispatcher->{$halt ? 'until' : 'dispatch'}( - "eloquent.{$event}: ".static::class, + "eloquent.{$event}: " . static::class, [$this, $revision] ); }