Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Add firstOrfail, firstOr, sole and exists methods to base query builder #9976

Open
wants to merge 9 commits into
base: 5.x
Choose a base branch
from
11 changes: 11 additions & 0 deletions src/Exceptions/ItemNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Statamic\Exceptions;

class ItemNotFoundException extends \Exception
{
public function __construct()
{
parent::__construct('Item not found');
}
}
9 changes: 9 additions & 0 deletions src/Exceptions/MultipleRecordsFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Statamic\Exceptions;

use Illuminate\Database\MultipleRecordsFoundException as LaravelMultipleRecordsFoundException;

class MultipleRecordsFoundException extends LaravelMultipleRecordsFoundException
{
}
9 changes: 9 additions & 0 deletions src/Exceptions/RecordsNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Statamic\Exceptions;

use Illuminate\Database\RecordsNotFoundException as LaravelRecordsNotFoundException;

class RecordsNotFoundException extends LaravelRecordsNotFoundException
{
}
53 changes: 51 additions & 2 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use Illuminate\Support\LazyCollection;
use InvalidArgumentException;
use Statamic\Contracts\Query\Builder as Contract;
use Statamic\Exceptions\ItemNotFoundException;
use Statamic\Exceptions\MultipleRecordsFoundException;
use Statamic\Exceptions\RecordsNotFoundException;
use Statamic\Extensions\Pagination\LengthAwarePaginator;
use Statamic\Facades\Pattern;
use Statamic\Query\Concerns\FakesQueries;
Expand Down Expand Up @@ -534,9 +537,55 @@ public function find($id, $columns = ['*'])
return $this->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)
Expand Down
53 changes: 51 additions & 2 deletions src/Query/EloquentQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
109 changes: 109 additions & 0 deletions tests/Data/Entries/EntryQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Loading