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 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 exists() + { + return $this->count() >= 1; } public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) diff --git a/src/Query/EloquentQueryBuilder.php b/src/Query/EloquentQueryBuilder.php index e12b349e0b..286755e6a5 100644 --- a/src/Query/EloquentQueryBuilder.php +++ b/src/Query/EloquentQueryBuilder.php @@ -9,6 +9,9 @@ use Illuminate\Support\LazyCollection; 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; @@ -66,9 +69,55 @@ 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 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 exists() + { + return $this->builder->count() >= 1; } 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 cf08300da3..ee758fd9af 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -6,6 +6,9 @@ use Illuminate\Support\Carbon; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Statamic\Exceptions\ItemNotFoundException; +use Statamic\Exceptions\MultipleRecordsFoundException; +use Statamic\Exceptions\RecordsNotFoundException; use Statamic\Facades\Blueprint; use Statamic\Facades\Collection; use Statamic\Facades\Entry; @@ -897,4 +900,110 @@ public function values_can_be_plucked() 'thing-2', ], Entry::query()->where('type', 'b')->pluck('slug')->all()); } + + #[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); + } + + #[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(); + } + + #[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()); + } }