diff --git a/src/Contracts/Revision.php b/src/Contracts/Revision.php index 4a1b201..244da03 100644 --- a/src/Contracts/Revision.php +++ b/src/Contracts/Revision.php @@ -15,6 +15,8 @@ public function revisionable(): MorphTo; public function previous(): ?static; + public function next(): ?static; + public function diff(?self $target = null): Diff; public function toModel(): Model; diff --git a/src/Models/Revision.php b/src/Models/Revision.php index 62e9d70..1df8d53 100644 --- a/src/Models/Revision.php +++ b/src/Models/Revision.php @@ -186,6 +186,37 @@ public function previous(): ?static ->first(); } + /** + * Return the revision that directly follows this one for the same model. + */ + public function next(): ?static + { + return static::query() + ->where([ + ['revisionable_type', $this->revisionable_type], + ['revisionable_id', $this->revisionable_id], + [$this->getKeyName(), '>', $this->getKey()], + ]) + ->oldest($this->getKeyName()) + ->first(); + } + + /** + * Determine whether this is the oldest revision for its model. + */ + public function isFirstRevision(): bool + { + return $this->exists && $this->previous() === null; + } + + /** + * Determine whether this is the most recent revision for its model. + */ + public function isLastRevision(): bool + { + return $this->exists && $this->next() === null; + } + /** * Compare this revision against another revision or its predecessor. */ diff --git a/tests/RevisionNavigationTest.php b/tests/RevisionNavigationTest.php new file mode 100644 index 0000000..9e969d8 --- /dev/null +++ b/tests/RevisionNavigationTest.php @@ -0,0 +1,104 @@ +createPost(); + + $this->modifyPost($post); + $this->modifyPost($post, ['name' => 'Yet another post name']); + + $revisions = $post->revisions()->orderBy('id')->get(); + + // When / Then + $this->assertNull($revisions->first()->previous()); + $this->assertTrue($revisions->last()->previous()->is($revisions->first())); + } + + #[Test] + public function it_returns_the_next_revision() + { + // Given + $post = $this->createPost(); + + $this->modifyPost($post); + $this->modifyPost($post, ['name' => 'Yet another post name']); + + $revisions = $post->revisions()->orderBy('id')->get(); + + // When / Then + $this->assertTrue($revisions->first()->next()->is($revisions->last())); + $this->assertNull($revisions->last()->next()); + } + + #[Test] + public function it_determines_whether_a_revision_is_the_first_revision() + { + // Given + $post = $this->createPost(); + + $this->modifyPost($post); + $this->modifyPost($post, ['name' => 'Yet another post name']); + + $revisions = $post->revisions()->orderBy('id')->get(); + + // When / Then + $this->assertTrue($revisions->first()->isFirstRevision()); + $this->assertFalse($revisions->last()->isFirstRevision()); + } + + #[Test] + public function it_determines_whether_a_revision_is_the_last_revision() + { + // Given + $post = $this->createPost(); + + $this->modifyPost($post); + $this->modifyPost($post, ['name' => 'Yet another post name']); + + $revisions = $post->revisions()->orderBy('id')->get(); + + // When / Then + $this->assertFalse($revisions->first()->isLastRevision()); + $this->assertTrue($revisions->last()->isLastRevision()); + } + + #[Test] + public function it_treats_a_single_revision_as_both_the_first_and_the_last() + { + // Given + $post = $this->createPost(); + + $this->modifyPost($post); + + // When + $revision = $post->revisions()->firstOrFail(); + + // Then + $this->assertTrue($revision->isFirstRevision()); + $this->assertTrue($revision->isLastRevision()); + } + + #[Test] + public function it_treats_an_unsaved_revision_as_neither_the_first_nor_the_last() + { + // Given + $revision = new Revision([ + 'revisionable_type' => Post::class, + 'revisionable_id' => 1, + ]); + + // When / Then + $this->assertFalse($revision->isFirstRevision()); + $this->assertFalse($revision->isLastRevision()); + } +}