From 2c61576af5a061c87ab790e5d15bb700e2436e2b Mon Sep 17 00:00:00 2001 From: Thijs Kok Date: Wed, 22 Jul 2026 16:16:41 +0200 Subject: [PATCH] Capturing database default values on initial creation --- src/Concerns/HasRevisions.php | 16 ++++++---------- src/Revisioner.php | 9 ++++++++- tests/CreatingRevisionsTest.php | 29 +++++++++++++++++++++++++++++ tests/RevisionDiffTest.php | 23 +++++++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/Concerns/HasRevisions.php b/src/Concerns/HasRevisions.php index f40148c..aed51eb 100644 --- a/src/Concerns/HasRevisions.php +++ b/src/Concerns/HasRevisions.php @@ -107,9 +107,8 @@ 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. + * Register a listener for the rollingBack event. Return false to abort the rollback; + * mutate the passed revision to change what gets restored. */ public static function rollingBack(Closure $callback): void { @@ -117,8 +116,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. + * Register a listener for the rolledBack event, fired after a rollback with the target revision. */ public static function rolledBack(Closure $callback): void { @@ -163,8 +161,7 @@ public function latestRevision(): MorphOne } /** - * Compare the current model state against the latest revision or a specific revision, or against - * nothing if it has no revisions at all. + * Compare the current model state against the latest revision, a specific revision, or nothing. */ public function diff(?RevisionContract $revision = null): Diff { @@ -468,9 +465,8 @@ protected function revisionToReplace(): ?Revision } /** - * 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. + * Fire an event with the model and revision, bypassing fireModelEvent() so overrides of it + * (e.g. laravel-pivot-events) can't interfere. */ protected function fireRevisionEvent(string $event, bool $halt, RevisionContract $revision): mixed { diff --git a/src/Revisioner.php b/src/Revisioner.php index 5a8f2fc..36ff6df 100644 --- a/src/Revisioner.php +++ b/src/Revisioner.php @@ -274,7 +274,14 @@ protected function buildChanges(Revision $revision, ?Revision $baseline = null): */ protected function buildModelData(): array { - return $this->filterModelData($this->model->getAttributes()); + $attributes = $this->model->getAttributes(); + + if ($this->model->wasRecentlyCreated) { + // Backfill DB-assigned defaults without discarding unsaved in-memory changes. + $attributes += $this->model->fresh()?->getAttributes() ?? []; + } + + return $this->filterModelData($attributes); } /** diff --git a/tests/CreatingRevisionsTest.php b/tests/CreatingRevisionsTest.php index c0dd38a..5de30d4 100644 --- a/tests/CreatingRevisionsTest.php +++ b/tests/CreatingRevisionsTest.php @@ -60,6 +60,35 @@ public function getRevisionOptions(): RevisableOptions $this->assertTrue(Revision::first()->isInitial()); } + #[Test] + public function it_captures_database_default_values_on_the_initial_revision() + { + // Given + $post = new class extends Post + { + public function getRevisionOptions(): RevisableOptions + { + return parent::getRevisionOptions()->enableRevisionOnCreate(); + } + }; + + $author = $this->createAuthor(); + + // When + $post = $post->create([ + 'author_id' => $author->id, + 'name' => 'Post name', + 'slug' => 'post-slug', + ]); + + // Then + $revision = $post->revisions()->firstOrFail(); + + $this->assertEquals(0, $revision->metadata['attributes']['votes']); + $this->assertEquals(0, $revision->metadata['attributes']['views']); + $this->assertNull($revision->metadata['attributes']['content']); + } + #[Test] public function it_only_tags_the_first_revision_as_initial_when_reusing_the_same_instance() { diff --git a/tests/RevisionDiffTest.php b/tests/RevisionDiffTest.php index 102dd56..0b10428 100644 --- a/tests/RevisionDiffTest.php +++ b/tests/RevisionDiffTest.php @@ -285,6 +285,29 @@ public function it_diffs_against_nothing_when_the_model_has_no_revisions() $this->assertEquals(10, $changes['votes']['after']); } + #[Test] + public function it_preserves_unsaved_changes_when_diffing_the_same_instance_right_after_creation() + { + // Given + $author = $this->createAuthor(); + + $post = Post::create([ + 'author_id' => $author->id, + 'name' => 'Post name', + 'slug' => 'post-slug', + 'content' => 'Post content', + 'votes' => 10, + 'views' => 100, + ]); + + // When + $post->votes = 999; + $diff = $post->diff(); + + // Then + $this->assertEquals(999, $diff->all()['votes']['after']); + } + // relations #[Test]