From c09020eb41ce142ad53cc188c373c48248a5395e Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 30 Apr 2024 14:41:02 +0100 Subject: [PATCH 1/5] Add `firstOrFail` and `firstOr` methods to base query builder --- src/Exceptions/ItemNotFoundException.php | 11 ++++ src/Query/Builder.php | 29 ++++++++++- src/Query/EloquentQueryBuilder.php | 29 ++++++++++- tests/Data/Entries/EntryQueryBuilderTest.php | 55 ++++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 src/Exceptions/ItemNotFoundException.php diff --git a/src/Exceptions/ItemNotFoundException.php b/src/Exceptions/ItemNotFoundException.php new file mode 100644 index 0000000000..69e1d8995b --- /dev/null +++ b/src/Exceptions/ItemNotFoundException.php @@ -0,0 +1,11 @@ +where('id', $id)->get($columns)->first(); } - public function first() + public function first($columns = ['*']) { - return $this->get()->first(); + return $this->get($columns)->first(); + } + + public function firstOrFail($columns = ['*']) + { + if (! is_null($item = $this->first($columns))) { + return $item; + } + + throw new ItemNotFoundException(); + } + + public function firstOr($columns = ['*'], ?Closure $callback = null) + { + if ($columns instanceof Closure) { + $callback = $columns; + + $columns = ['*']; + } + + if (! is_null($model = $this->first($columns))) { + return $model; + } + + return $callback(); } public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index a2c4b0ae98..962eed2368 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -9,6 +9,7 @@ use Illuminate\Support\LazyCollection; use InvalidArgumentException; use Statamic\Contracts\Query\Builder; +use Statamic\Exceptions\ItemNotFoundException; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Blink; use Statamic\Support\Arr; @@ -66,9 +67,33 @@ public function get($columns = ['*']) return $items; } - public function first() + public function first($columns = ['*']) { - return $this->get()->first(); + return $this->get($columns)->first(); + } + + public function firstOrFail($columns = ['*']) + { + if (! is_null($item = $this->first($columns))) { + return $item; + } + + throw new ItemNotFoundException(); + } + + public function firstOr($columns = ['*'], ?Closure $callback = null) + { + if ($columns instanceof Closure) { + $callback = $columns; + + $columns = ['*']; + } + + if (! is_null($model = $this->first($columns))) { + return $model; + } + + return $callback(); } public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index ac60eedbc2..52343a923d 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -4,6 +4,7 @@ use Facades\Tests\Factories\EntryFactory; use Illuminate\Support\Carbon; +use Statamic\Exceptions\ItemNotFoundException; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; @@ -769,4 +770,58 @@ public function entries_are_found_using_lazy() $this->assertInstanceOf(\Illuminate\Support\LazyCollection::class, $entries); $this->assertCount(3, $entries); } + + /** @test */ + public function entry_can_be_found_using_first_or_fail() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $firstOrFail = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->firstOrFail(); + + $this->assertSame($entry, $firstOrFail); + } + + /** @test */ + public function exception_is_thrown_when_entry_does_not_exist_using_first_or_fail() + { + $this->expectException(ItemNotFoundException::class); + + Entry::query() + ->where('collection', 'posts') + ->where('id', 'ze-hoff') + ->firstOrFail(); + } + + /** @test */ + public function entry_can_be_found_using_first_or() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $firstOrFail = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->firstOr(function () { + return 'fallback'; + }); + + $this->assertSame($entry, $firstOrFail); + } + + /** @test */ + public function callback_is_called_when_entry_does_not_exist_using_first_or() + { + $firstOrFail = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->firstOr(function () { + return 'fallback'; + }); + + $this->assertSame('fallback', $firstOrFail); + } } From ae0b251735fed257b05a06e246e9e3caf95a2061 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 30 Apr 2024 14:57:54 +0100 Subject: [PATCH 2/5] Add `sole` method --- .../MultipleRecordsFoundException.php | 9 +++++ src/Exceptions/RecordsNotFoundException.php | 9 +++++ src/Query/Builder.php | 19 +++++++++ src/Query/EloquentQueryBuilder.php | 19 +++++++++ tests/Data/Entries/EntryQueryBuilderTest.php | 40 +++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 src/Exceptions/MultipleRecordsFoundException.php create mode 100644 src/Exceptions/RecordsNotFoundException.php diff --git a/src/Exceptions/MultipleRecordsFoundException.php b/src/Exceptions/MultipleRecordsFoundException.php new file mode 100644 index 0000000000..28c6dfaf2d --- /dev/null +++ b/src/Exceptions/MultipleRecordsFoundException.php @@ -0,0 +1,9 @@ +get($columns); + + $count = $result->count(); + + if ($count === 0) { + throw new RecordsNotFoundException(); + } + + if ($count > 1) { + throw new MultipleRecordsFoundException($count); + } + + return $result->first(); + } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $page = $page ?: Paginator::resolveCurrentPage($pageName); diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index 962eed2368..7dcf33bdce 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -10,6 +10,8 @@ use InvalidArgumentException; use Statamic\Contracts\Query\Builder; use Statamic\Exceptions\ItemNotFoundException; +use Statamic\Exceptions\MultipleRecordsFoundException; +use Statamic\Exceptions\RecordsNotFoundException; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Blink; use Statamic\Support\Arr; @@ -96,6 +98,23 @@ public function firstOr($columns = ['*'], ?Closure $callback = null) return $callback(); } + public function sole($columns = ['*']) + { + $result = $this->get($columns); + + $count = $result->count(); + + if ($count === 0) { + throw new RecordsNotFoundException(); + } + + if ($count > 1) { + throw new MultipleRecordsFoundException($count); + } + + return $result->first(); + } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $paginator = $this->builder->paginate($perPage, $this->selectableColumns($columns), $pageName, $page); diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 52343a923d..9cbf8d297a 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -5,6 +5,8 @@ use Facades\Tests\Factories\EntryFactory; use Illuminate\Support\Carbon; use Statamic\Exceptions\ItemNotFoundException; +use Statamic\Exceptions\MultipleRecordsFoundException; +use Statamic\Exceptions\RecordsNotFoundException; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; @@ -824,4 +826,42 @@ public function callback_is_called_when_entry_does_not_exist_using_first_or() $this->assertSame('fallback', $firstOrFail); } + + /** @test */ + public function sole_entry_is_returned() + { + Collection::make('posts')->save(); + $entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + + $sole = Entry::query() + ->where('collection', 'posts') + ->where('id', 'hoff') + ->sole(); + + $this->assertSame($entry, $sole); + } + + /** @test */ + public function exception_is_thrown_by_sole_when_multiple_entries_are_returned_from_query() + { + Collection::make('posts')->save(); + EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create(); + EntryFactory::collection('posts')->id('smoff')->slug('joe-hasselsmoff')->data(['title' => 'Joe Hasselsmoff'])->create(); + + $this->expectException(MultipleRecordsFoundException::class); + + Entry::query() + ->where('collection', 'posts') + ->sole(); + } + + /** @test */ + public function exception_is_thrown_by_sole_when_no_entries_are_returned_from_query() + { + $this->expectException(RecordsNotFoundException::class); + + Entry::query() + ->where('collection', 'posts') + ->sole(); + } } From a6927b13144bba9f1cc1107abb44cfb31f817ec9 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 30 Apr 2024 15:03:17 +0100 Subject: [PATCH 3/5] Add `count` method --- src/Query/Builder.php | 5 +++++ src/Query/EloquentQueryBuilder.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 3181d63b68..0cc3a991eb 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -580,6 +580,11 @@ public function sole($columns = ['*']) return $result->first(); } + public function exists() + { + return $this->count() >= 1; + } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $page = $page ?: Paginator::resolveCurrentPage($pageName); diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index 7dcf33bdce..60cb692dc3 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -115,6 +115,11 @@ public function sole($columns = ['*']) return $result->first(); } + public function exists() + { + return $this->builder->count() >= 1; + } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) { $paginator = $this->builder->paginate($perPage, $this->selectableColumns($columns), $pageName, $page); From f3ce9349a09c9868588147c92b76a665bcea9096 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 30 Apr 2024 15:17:18 +0100 Subject: [PATCH 4/5] Add tests for `exists` method --- tests/Data/Entries/EntryQueryBuilderTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 9cbf8d297a..49516c7e2d 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -864,4 +864,18 @@ public function exception_is_thrown_by_sole_when_no_entries_are_returned_from_qu ->where('collection', 'posts') ->sole(); } + + /** @test */ + public function exists_returns_true_when_results_are_found() + { + $this->createDummyCollectionAndEntries(); + + $this->assertTrue(Entry::query()->exists()); + } + + /** @test */ + public function exists_returns_false_when_no_results_are_found() + { + $this->assertFalse(Entry::query()->exists()); + } } From 67e9181355b366a33a247ee0aeb1ea646c8ea66a Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Tue, 16 Jul 2024 12:09:00 +0100 Subject: [PATCH 5/5] use `#[Test]` instead of metadata --- tests/Data/Entries/EntryQueryBuilderTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 60d5cf50fa..ee758fd9af 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -901,7 +901,7 @@ public function values_can_be_plucked() ], Entry::query()->where('type', 'b')->pluck('slug')->all()); } - /** @test */ + #[Test] public function entry_can_be_found_using_first_or_fail() { Collection::make('posts')->save(); @@ -915,7 +915,7 @@ public function entry_can_be_found_using_first_or_fail() $this->assertSame($entry, $firstOrFail); } - /** @test */ + #[Test] public function exception_is_thrown_when_entry_does_not_exist_using_first_or_fail() { $this->expectException(ItemNotFoundException::class); @@ -926,7 +926,7 @@ public function exception_is_thrown_when_entry_does_not_exist_using_first_or_fai ->firstOrFail(); } - /** @test */ + #[Test] public function entry_can_be_found_using_first_or() { Collection::make('posts')->save(); @@ -942,7 +942,7 @@ public function entry_can_be_found_using_first_or() $this->assertSame($entry, $firstOrFail); } - /** @test */ + #[Test] public function callback_is_called_when_entry_does_not_exist_using_first_or() { $firstOrFail = Entry::query() @@ -955,7 +955,7 @@ public function callback_is_called_when_entry_does_not_exist_using_first_or() $this->assertSame('fallback', $firstOrFail); } - /** @test */ + #[Test] public function sole_entry_is_returned() { Collection::make('posts')->save(); @@ -969,7 +969,7 @@ public function sole_entry_is_returned() $this->assertSame($entry, $sole); } - /** @test */ + #[Test] public function exception_is_thrown_by_sole_when_multiple_entries_are_returned_from_query() { Collection::make('posts')->save(); @@ -983,7 +983,7 @@ public function exception_is_thrown_by_sole_when_multiple_entries_are_returned_f ->sole(); } - /** @test */ + #[Test] public function exception_is_thrown_by_sole_when_no_entries_are_returned_from_query() { $this->expectException(RecordsNotFoundException::class); @@ -993,7 +993,7 @@ public function exception_is_thrown_by_sole_when_no_entries_are_returned_from_qu ->sole(); } - /** @test */ + #[Test] public function exists_returns_true_when_results_are_found() { $this->createDummyCollectionAndEntries(); @@ -1001,7 +1001,7 @@ public function exists_returns_true_when_results_are_found() $this->assertTrue(Entry::query()->exists()); } - /** @test */ + #[Test] public function exists_returns_false_when_no_results_are_found() { $this->assertFalse(Entry::query()->exists());