diff --git a/src/Entries/Entry.php b/src/Entries/Entry.php index b1113005624..ca3f06fe95a 100644 --- a/src/Entries/Entry.php +++ b/src/Entries/Entry.php @@ -281,6 +281,8 @@ public function delete() Facades\Entry::delete($this); + $withEvents ? $this->deleteRevisions() : $this->deleteRevisionsQuietly(); + if ($withEvents) { EntryDeleted::dispatch($this); } diff --git a/src/Revisions/Revisable.php b/src/Revisions/Revisable.php index 8091eaa1d73..17595a375db 100644 --- a/src/Revisions/Revisable.php +++ b/src/Revisions/Revisable.php @@ -81,6 +81,20 @@ public function deleteWorkingCopy() return optional($this->workingCopy())->delete(); } + public function deleteRevisions() + { + $this->revisions()->each->delete(); + + $this->deleteWorkingCopy(); + } + + public function deleteRevisionsQuietly() + { + $this->revisions()->each->deleteQuietly(); + + optional($this->workingCopy())->deleteQuietly(); + } + public function publishWorkingCopy($options = []) { $item = $this->fromWorkingCopy(); diff --git a/src/Revisions/Revision.php b/src/Revisions/Revision.php index 94b660815d4..0e3f9b298de 100644 --- a/src/Revisions/Revision.php +++ b/src/Revisions/Revision.php @@ -30,6 +30,7 @@ class Revision implements Arrayable, ContainsQueryableValues, Contract protected $message; protected $action = 'revision'; protected $attributes = []; + protected $withEvents = true; public function id() { @@ -147,11 +148,23 @@ public function save() return true; } + public function deleteQuietly() + { + $this->withEvents = false; + + return $this->delete(); + } + public function delete() { + $withEvents = $this->withEvents; + $this->withEvents = true; + Revisions::delete($this); - RevisionDeleted::dispatch($this); + if ($withEvents) { + RevisionDeleted::dispatch($this); + } } public function isWorkingCopy(): bool diff --git a/src/Revisions/RevisionRepository.php b/src/Revisions/RevisionRepository.php index 295417e112f..d9119fece2c 100644 --- a/src/Revisions/RevisionRepository.php +++ b/src/Revisions/RevisionRepository.php @@ -58,6 +58,12 @@ public function save(RevisionContract $revision) public function delete(RevisionContract $revision) { $this->store->delete($revision); + + $directory = $this->directory().'/'.$revision->key(); + + if (File::exists($directory) && File::isEmpty($directory)) { + File::delete($directory); + } } public function query() diff --git a/tests/Feature/Entries/EntryRevisionsTest.php b/tests/Feature/Entries/EntryRevisionsTest.php index 8953729b239..9c7280922bc 100644 --- a/tests/Feature/Entries/EntryRevisionsTest.php +++ b/tests/Feature/Entries/EntryRevisionsTest.php @@ -5,10 +5,17 @@ use Facades\Statamic\Fields\BlueprintRepository; use Facades\Tests\Factories\EntryFactory; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\Test; +use Statamic\Events\EntryDeleted; +use Statamic\Events\EntryDeleting; +use Statamic\Events\RevisionDeleted; use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Facades\Folder; +use Statamic\Facades\Path; +use Statamic\Facades\Revision as Revisions; +use Statamic\Facades\Stache; use Statamic\Facades\User; use Statamic\Fields\Blueprint; use Statamic\Revisions\Revision; @@ -868,6 +875,137 @@ public function revision_localizations_only_includes_authorized_sites() $this->assertEquals(['en', 'fr'], array_column($localizations, 'handle')); } + #[Test] + public function it_deletes_revisions_and_the_working_copy_when_the_entry_is_deleted() + { + Event::fake([RevisionDeleted::class]); + + [$entry, $revisions, $workingCopy] = $this->entryWithRevisions('1'); + + $this->assertRevisionFilesExist($entry, $revisions, $workingCopy); + + $entry->delete(); + + $this->assertRevisionFilesAreGone($entry, $revisions, $workingCopy); + $this->assertCount(0, $entry->revisions()); + $this->assertNull($entry->workingCopy()); + Event::assertDispatchedTimes(RevisionDeleted::class, 3); + } + + #[Test] + public function it_leaves_revisions_belonging_to_other_entries() + { + [$entry, $revisions, $workingCopy] = $this->entryWithRevisions('1'); + [$other, $otherRevisions, $otherWorkingCopy] = $this->entryWithRevisions('2'); + + $entry->delete(); + + $this->assertRevisionFilesAreGone($entry, $revisions, $workingCopy); + $this->assertRevisionFilesExist($other, $otherRevisions, $otherWorkingCopy); + $this->assertCount(2, $other->revisions()); + $this->assertNotNull($other->workingCopy()); + } + + #[Test] + public function it_keeps_revisions_when_entry_deletion_is_cancelled() + { + Event::fake([RevisionDeleted::class]); + + Event::listen(EntryDeleting::class, function () { + return false; + }); + + [$entry, $revisions, $workingCopy] = $this->entryWithRevisions('1'); + + $this->assertFalse($entry->delete()); + + $this->assertRevisionFilesExist($entry, $revisions, $workingCopy); + $this->assertCount(2, $entry->revisions()); + $this->assertNotNull($entry->workingCopy()); + Event::assertNotDispatched(RevisionDeleted::class); + } + + #[Test] + public function it_deletes_revisions_when_the_entry_is_deleted_quietly() + { + Event::fake([EntryDeleted::class, RevisionDeleted::class]); + + [$entry, $revisions, $workingCopy] = $this->entryWithRevisions('1'); + + $entry->deleteQuietly(); + + $this->assertRevisionFilesAreGone($entry, $revisions, $workingCopy); + Event::assertNotDispatched(EntryDeleted::class); + Event::assertNotDispatched(RevisionDeleted::class); + } + + #[Test] + public function it_deletes_revisions_from_a_custom_revisions_path() + { + $custom = Path::tidy($this->dir.'/revisions'); + + config(['statamic.revisions.path' => $custom]); + + Stache::store('revisions')->directory(config('statamic.revisions.path')); + + [$entry, $revisions, $workingCopy] = $this->entryWithRevisions('1'); + + $this->assertStringStartsWith($custom, $revisions[0]->path()); + $this->assertRevisionFilesExist($entry, $revisions, $workingCopy); + + $entry->delete(); + + $this->assertRevisionFilesAreGone($entry, $revisions, $workingCopy); + $this->assertFileDoesNotExist($this->fakeStacheDirectory.'/revisions/collections/blog/en/1'); + } + + private function entryWithRevisions(string $id): array + { + $entry = EntryFactory::id($id) + ->slug('entry-'.$id) + ->collection('blog') + ->date('2010-12-25') + ->data(['title' => 'Title '.$id]) + ->create(); + + $revisions = collect([ + $entry->makeRevision()->message('Revision one')->date(Carbon::parse('2017-02-01')), + $entry->makeRevision()->message('Revision two')->date(Carbon::parse('2017-02-03')), + ]); + + $revisions->each->save(); + + $workingCopy = $entry->makeWorkingCopy(); + $workingCopy->save(); + + return [$entry, $revisions->all(), $workingCopy]; + } + + private function assertRevisionFilesExist($entry, array $revisions, Revision $workingCopy): void + { + foreach ($revisions as $revision) { + $this->assertFileExists($revision->path()); + } + + $this->assertFileExists($workingCopy->path()); + $this->assertDirectoryExists($this->revisionDirectory($entry)); + } + + private function assertRevisionFilesAreGone($entry, array $revisions, Revision $workingCopy): void + { + foreach ($revisions as $revision) { + $this->assertFileDoesNotExist($revision->path()); + } + + $this->assertFileDoesNotExist($workingCopy->path()); + $this->assertDirectoryDoesNotExist($this->revisionDirectory($entry)); + } + + private function revisionDirectory($entry): string + { + return Revisions::directory().'/collections/blog/en/'.$entry->id(); + } + private function setTestBlueprint($handle, $fields) { $blueprint = Blueprint::makeFromFields($fields)->setHandle($handle);