Skip to content

Commit 882294e

Browse files
[5.x] Add firstOrfail, firstOr, sole and exists methods to base query builder (#9976)
Co-authored-by: Jason Varga <[email protected]>
1 parent 56f0f31 commit 882294e

File tree

5 files changed

+222
-0
lines changed

5 files changed

+222
-0
lines changed

src/Query/Builder.php

+48
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Statamic\Extensions\Pagination\LengthAwarePaginator;
1515
use Statamic\Facades\Pattern;
1616
use Statamic\Query\Concerns\FakesQueries;
17+
use Statamic\Query\Exceptions\MultipleRecordsFoundException;
18+
use Statamic\Query\Exceptions\RecordsNotFoundException;
1719
use Statamic\Query\Scopes\AppliesScopes;
1820

1921
abstract class Builder implements Contract
@@ -565,6 +567,52 @@ public function first()
565567
return $this->get()->first();
566568
}
567569

570+
public function firstOrFail($columns = ['*'])
571+
{
572+
if (! is_null($item = $this->select($columns)->first())) {
573+
return $item;
574+
}
575+
576+
throw new RecordsNotFoundException();
577+
}
578+
579+
public function firstOr($columns = ['*'], ?Closure $callback = null)
580+
{
581+
if ($columns instanceof Closure) {
582+
$callback = $columns;
583+
584+
$columns = ['*'];
585+
}
586+
587+
if (! is_null($model = $this->select($columns)->first())) {
588+
return $model;
589+
}
590+
591+
return $callback();
592+
}
593+
594+
public function sole($columns = ['*'])
595+
{
596+
$result = $this->get($columns);
597+
598+
$count = $result->count();
599+
600+
if ($count === 0) {
601+
throw new RecordsNotFoundException();
602+
}
603+
604+
if ($count > 1) {
605+
throw new MultipleRecordsFoundException($count);
606+
}
607+
608+
return $result->first();
609+
}
610+
611+
public function exists()
612+
{
613+
return $this->count() >= 1;
614+
}
615+
568616
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
569617
{
570618
$page = $page ?: Paginator::resolveCurrentPage($pageName);

src/Query/EloquentQueryBuilder.php

+48
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Statamic\Contracts\Query\Builder;
1212
use Statamic\Extensions\Pagination\LengthAwarePaginator;
1313
use Statamic\Facades\Blink;
14+
use Statamic\Query\Exceptions\MultipleRecordsFoundException;
15+
use Statamic\Query\Exceptions\RecordsNotFoundException;
1416
use Statamic\Query\Scopes\AppliesScopes;
1517
use Statamic\Support\Arr;
1618

@@ -80,6 +82,52 @@ public function first()
8082
return $this->get()->first();
8183
}
8284

85+
public function firstOrFail($columns = ['*'])
86+
{
87+
if (! is_null($item = $this->select($columns)->first($columns))) {
88+
return $item;
89+
}
90+
91+
throw new RecordsNotFoundException();
92+
}
93+
94+
public function firstOr($columns = ['*'], ?Closure $callback = null)
95+
{
96+
if ($columns instanceof Closure) {
97+
$callback = $columns;
98+
99+
$columns = ['*'];
100+
}
101+
102+
if (! is_null($model = $this->select($columns)->first())) {
103+
return $model;
104+
}
105+
106+
return $callback();
107+
}
108+
109+
public function sole($columns = ['*'])
110+
{
111+
$result = $this->get($columns);
112+
113+
$count = $result->count();
114+
115+
if ($count === 0) {
116+
throw new RecordsNotFoundException();
117+
}
118+
119+
if ($count > 1) {
120+
throw new MultipleRecordsFoundException($count);
121+
}
122+
123+
return $result->first();
124+
}
125+
126+
public function exists()
127+
{
128+
return $this->builder->count() >= 1;
129+
}
130+
83131
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
84132
{
85133
$paginator = $this->builder->paginate($perPage, $this->selectableColumns($columns), $pageName, $page);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Statamic\Query\Exceptions;
4+
5+
use Illuminate\Database\MultipleRecordsFoundException as LaravelMultipleRecordsFoundException;
6+
7+
class MultipleRecordsFoundException extends LaravelMultipleRecordsFoundException
8+
{
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Statamic\Query\Exceptions;
4+
5+
use Illuminate\Database\RecordsNotFoundException as LaravelRecordsNotFoundException;
6+
7+
class RecordsNotFoundException extends LaravelRecordsNotFoundException
8+
{
9+
}

tests/Data/Entries/EntryQueryBuilderTest.php

+108
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Statamic\Facades\Blueprint;
1010
use Statamic\Facades\Collection;
1111
use Statamic\Facades\Entry;
12+
use Statamic\Query\Exceptions\MultipleRecordsFoundException;
13+
use Statamic\Query\Exceptions\RecordsNotFoundException;
1214
use Statamic\Query\Scopes\Scope;
1315
use Tests\PreventSavingStacheItemsToDisk;
1416
use Tests\TestCase;
@@ -922,6 +924,112 @@ public function values_can_be_plucked()
922924
'thing-2',
923925
], Entry::query()->where('type', 'b')->pluck('slug')->all());
924926
}
927+
928+
#[Test]
929+
public function entry_can_be_found_using_first_or_fail()
930+
{
931+
Collection::make('posts')->save();
932+
$entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create();
933+
934+
$firstOrFail = Entry::query()
935+
->where('collection', 'posts')
936+
->where('id', 'hoff')
937+
->firstOrFail();
938+
939+
$this->assertSame($entry, $firstOrFail);
940+
}
941+
942+
#[Test]
943+
public function exception_is_thrown_when_entry_does_not_exist_using_first_or_fail()
944+
{
945+
$this->expectException(RecordsNotFoundException::class);
946+
947+
Entry::query()
948+
->where('collection', 'posts')
949+
->where('id', 'ze-hoff')
950+
->firstOrFail();
951+
}
952+
953+
#[Test]
954+
public function entry_can_be_found_using_first_or()
955+
{
956+
Collection::make('posts')->save();
957+
$entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create();
958+
959+
$firstOrFail = Entry::query()
960+
->where('collection', 'posts')
961+
->where('id', 'hoff')
962+
->firstOr(function () {
963+
return 'fallback';
964+
});
965+
966+
$this->assertSame($entry, $firstOrFail);
967+
}
968+
969+
#[Test]
970+
public function callback_is_called_when_entry_does_not_exist_using_first_or()
971+
{
972+
$firstOrFail = Entry::query()
973+
->where('collection', 'posts')
974+
->where('id', 'hoff')
975+
->firstOr(function () {
976+
return 'fallback';
977+
});
978+
979+
$this->assertSame('fallback', $firstOrFail);
980+
}
981+
982+
#[Test]
983+
public function sole_entry_is_returned()
984+
{
985+
Collection::make('posts')->save();
986+
$entry = EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create();
987+
988+
$sole = Entry::query()
989+
->where('collection', 'posts')
990+
->where('id', 'hoff')
991+
->sole();
992+
993+
$this->assertSame($entry, $sole);
994+
}
995+
996+
#[Test]
997+
public function exception_is_thrown_by_sole_when_multiple_entries_are_returned_from_query()
998+
{
999+
Collection::make('posts')->save();
1000+
EntryFactory::collection('posts')->id('hoff')->slug('david-hasselhoff')->data(['title' => 'David Hasselhoff'])->create();
1001+
EntryFactory::collection('posts')->id('smoff')->slug('joe-hasselsmoff')->data(['title' => 'Joe Hasselsmoff'])->create();
1002+
1003+
$this->expectException(MultipleRecordsFoundException::class);
1004+
1005+
Entry::query()
1006+
->where('collection', 'posts')
1007+
->sole();
1008+
}
1009+
1010+
#[Test]
1011+
public function exception_is_thrown_by_sole_when_no_entries_are_returned_from_query()
1012+
{
1013+
$this->expectException(RecordsNotFoundException::class);
1014+
1015+
Entry::query()
1016+
->where('collection', 'posts')
1017+
->sole();
1018+
}
1019+
1020+
#[Test]
1021+
public function exists_returns_true_when_results_are_found()
1022+
{
1023+
$this->createDummyCollectionAndEntries();
1024+
1025+
$this->assertTrue(Entry::query()->exists());
1026+
}
1027+
1028+
#[Test]
1029+
public function exists_returns_false_when_no_results_are_found()
1030+
{
1031+
$this->assertFalse(Entry::query()->exists());
1032+
}
9251033
}
9261034

9271035
class CustomScope extends Scope

0 commit comments

Comments
 (0)