Skip to content

Commit

Permalink
Add a findOr method to Eloquent (#42092)
Browse files Browse the repository at this point in the history
* Add a `findOr` method to Eloquent

* Linting
  • Loading branch information
jessarcher committed Apr 22, 2022
1 parent e1ca235 commit b062355
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,29 @@ public function findOrNew($id, $columns = ['*'])
return $this->newModelInstance();
}

/**
* Find a model by its primary key or call a callback.
*
* @param mixed $id
* @param \Closure|array $columns
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|static[]|static|mixed
*/
public function findOr($id, $columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;

$columns = ['*'];
}

if (! is_null($model = $this->find($id, $columns))) {
return $model;
}

return $callback();
}

/**
* Get the first record matching the attributes or instantiate it.
*
Expand Down
31 changes: 31 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,37 @@ public function findOrFail($id, $columns = ['*'])
throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
}

/**
* Find a related model by its primary key or call a callback.
*
* @param mixed $id
* @param \Closure|array $columns
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|mixed
*/
public function findOr($id, $columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;

$columns = ['*'];
}

$result = $this->find($id, $columns);

$id = $id instanceof Arrayable ? $id->toArray() : $id;

if (is_array($id)) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}

return $callback();
}

/**
* Add a basic where clause to the query, and return the first result.
*
Expand Down
31 changes: 31 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,37 @@ public function findOrFail($id, $columns = ['*'])
throw (new ModelNotFoundException)->setModel(get_class($this->related), $id);
}

/**
* Find a related model by its primary key or call a callback.
*
* @param mixed $id
* @param \Closure|array $columns
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|mixed
*/
public function findOr($id, $columns = ['*'], Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;

$columns = ['*'];
}

$result = $this->find($id, $columns);

$id = $id instanceof Arrayable ? $id->toArray() : $id;

if (is_array($id)) {
if (count($result) === count(array_unique($id))) {
return $result;
}
} elseif (! is_null($result)) {
return $result;
}

return $callback();
}

/**
* Get the results of the relationship.
*
Expand Down
69 changes: 69 additions & 0 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,75 @@ public function testFindOrFailMethodWithManyUsingCollectionThrowsModelNotFoundEx
$builder->findOrFail(new Collection([1, 2]), ['column']);
}

public function testFindOrMethod()
{
$builder = m::mock(Builder::class.'[first]', [$this->getMockQueryBuilder()]);
$model = $this->getMockModel();
$model->shouldReceive('getKeyType')->andReturn('int');
$builder->setModel($model);
$builder->getQuery()->shouldReceive('where')->with('foo_table.foo', '=', 1)->twice();
$builder->getQuery()->shouldReceive('where')->with('foo_table.foo', '=', 2)->once();
$builder->shouldReceive('first')->andReturn($model)->once();
$builder->shouldReceive('first')->with(['column'])->andReturn($model)->once();
$builder->shouldReceive('first')->andReturn(null)->once();

$this->assertSame($model, $builder->findOr(1, fn () => 'callback result'));
$this->assertSame($model, $builder->findOr(1, ['column'], fn () => 'callback result'));
$this->assertSame('callback result', $builder->findOr(2, fn () => 'callback result'));
}

public function testFindOrMethodWithMany()
{
$builder = m::mock(Builder::class.'[get]', [$this->getMockQueryBuilder()]);
$model1 = $this->getMockModel();
$model2 = $this->getMockModel();
$builder->setModel($model1);
$builder->getQuery()->shouldReceive('whereIn')->with('foo_table.foo', [1, 2])->twice();
$builder->getQuery()->shouldReceive('whereIn')->with('foo_table.foo', [1, 2, 3])->once();
$builder->shouldReceive('get')->andReturn(new Collection([$model1, $model2]))->once();
$builder->shouldReceive('get')->with(['column'])->andReturn(new Collection([$model1, $model2]))->once();
$builder->shouldReceive('get')->andReturn(null)->once();

$result = $builder->findOr([1, 2], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame($model1, $result[0]);
$this->assertSame($model2, $result[1]);

$result = $builder->findOr([1, 2], ['column'], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame($model1, $result[0]);
$this->assertSame($model2, $result[1]);

$result = $builder->findOr([1, 2, 3], fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFindOrMethodWithManyUsingCollection()
{
$builder = m::mock(Builder::class.'[get]', [$this->getMockQueryBuilder()]);
$model1 = $this->getMockModel();
$model2 = $this->getMockModel();
$builder->setModel($model1);
$builder->getQuery()->shouldReceive('whereIn')->with('foo_table.foo', [1, 2])->twice();
$builder->getQuery()->shouldReceive('whereIn')->with('foo_table.foo', [1, 2, 3])->once();
$builder->shouldReceive('get')->andReturn(new Collection([$model1, $model2]))->once();
$builder->shouldReceive('get')->with(['column'])->andReturn(new Collection([$model1, $model2]))->once();
$builder->shouldReceive('get')->andReturn(null)->once();

$result = $builder->findOr(new Collection([1, 2]), fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame($model1, $result[0]);
$this->assertSame($model2, $result[1]);

$result = $builder->findOr(new Collection([1, 2]), ['column'], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame($model1, $result[0]);
$this->assertSame($model2, $result[1]);

$result = $builder->findOr(new Collection([1, 2, 3]), fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFirstOrFailMethodThrowsModelNotFoundException()
{
$this->expectException(ModelNotFoundException::class);
Expand Down
74 changes: 74 additions & 0 deletions tests/Database/DatabaseEloquentHasManyThroughIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,80 @@ public function testFindOrFailWithManyUsingCollectionThrowsAnException()
HasManyThroughTestCountry::first()->posts()->findOrFail(new Collection([1, 2]));
}

public function testFindOrMethod()
{
HasManyThroughTestCountry::create(['id' => 1, 'name' => 'United States of America', 'shortname' => 'us'])
->users()->create(['id' => 1, 'email' => 'taylorotwell@gmail.com', 'country_short' => 'us'])
->posts()->create(['id' => 1, 'title' => 'A title', 'body' => 'A body', 'email' => 'taylorotwell@gmail.com']);

$result = HasManyThroughTestCountry::first()->posts()->findOr(1, fn () => 'callback result');
$this->assertInstanceOf(HasManyThroughTestPost::class, $result);
$this->assertSame(1, $result->id);
$this->assertSame('A title', $result->title);

$result = HasManyThroughTestCountry::first()->posts()->findOr(1, ['posts.id'], fn () => 'callback result');
$this->assertInstanceOf(HasManyThroughTestPost::class, $result);
$this->assertSame(1, $result->id);
$this->assertNull($result->title);

$result = HasManyThroughTestCountry::first()->posts()->findOr(2, fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFindOrMethodWithMany()
{
HasManyThroughTestCountry::create(['id' => 1, 'name' => 'United States of America', 'shortname' => 'us'])
->users()->create(['id' => 1, 'email' => 'taylorotwell@gmail.com', 'country_short' => 'us'])
->posts()->createMany([
['id' => 1, 'title' => 'A title', 'body' => 'A body', 'email' => 'taylorotwell@gmail.com'],
['id' => 2, 'title' => 'Another title', 'body' => 'Another body', 'email' => 'taylorotwell@gmail.com'],
]);

$result = HasManyThroughTestCountry::first()->posts()->findOr([1, 2], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertSame('A title', $result[0]->title);
$this->assertSame('Another title', $result[1]->title);

$result = HasManyThroughTestCountry::first()->posts()->findOr([1, 2], ['posts.id'], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertNull($result[0]->title);
$this->assertNull($result[1]->title);

$result = HasManyThroughTestCountry::first()->posts()->findOr([1, 2, 3], fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFindOrMethodWithManyUsingCollection()
{
HasManyThroughTestCountry::create(['id' => 1, 'name' => 'United States of America', 'shortname' => 'us'])
->users()->create(['id' => 1, 'email' => 'taylorotwell@gmail.com', 'country_short' => 'us'])
->posts()->createMany([
['id' => 1, 'title' => 'A title', 'body' => 'A body', 'email' => 'taylorotwell@gmail.com'],
['id' => 2, 'title' => 'Another title', 'body' => 'Another body', 'email' => 'taylorotwell@gmail.com'],
]);

$result = HasManyThroughTestCountry::first()->posts()->findOr(new Collection([1, 2]), fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertSame('A title', $result[0]->title);
$this->assertSame('Another title', $result[1]->title);

$result = HasManyThroughTestCountry::first()->posts()->findOr(new Collection([1, 2]), ['posts.id'], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertNull($result[0]->title);
$this->assertNull($result[1]->title);

$result = HasManyThroughTestCountry::first()->posts()->findOr(new Collection([1, 2, 3]), fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFirstRetrievesFirstRecord()
{
$this->seedData();
Expand Down
71 changes: 71 additions & 0 deletions tests/Integration/Database/EloquentBelongsToManyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,77 @@ public function testFindOrNewMethod()
$this->assertInstanceOf(Tag::class, $post->tags()->findOrNew(666));
}

public function testFindOrMethod()
{
$post = Post::create(['title' => Str::random()]);
$post->tags()->create(['name' => Str::random()]);

$result = $post->tags()->findOr(1, fn () => 'callback result');
$this->assertInstanceOf(Tag::class, $result);
$this->assertSame(1, $result->id);
$this->assertNotNull($result->name);

$result = $post->tags()->findOr(1, ['id'], fn () => 'callback result');
$this->assertInstanceOf(Tag::class, $result);
$this->assertSame(1, $result->id);
$this->assertNull($result->name);

$result = $post->tags()->findOr(2, fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFindOrMethodWithMany()
{
$post = Post::create(['title' => Str::random()]);
$post->tags()->createMany([
['name' => Str::random()],
['name' => Str::random()],
]);

$result = $post->tags()->findOr([1, 2], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertNotNull($result[0]->name);
$this->assertNotNull($result[1]->name);

$result = $post->tags()->findOr([1, 2], ['id'], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertNull($result[0]->name);
$this->assertNull($result[1]->name);

$result = $post->tags()->findOr([1, 2, 3], fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFindOrMethodWithManyUsingCollection()
{
$post = Post::create(['title' => Str::random()]);
$post->tags()->createMany([
['name' => Str::random()],
['name' => Str::random()],
]);

$result = $post->tags()->findOr(new Collection([1, 2]), fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertNotNull($result[0]->name);
$this->assertNotNull($result[1]->name);

$result = $post->tags()->findOr(new Collection([1, 2]), ['id'], fn () => 'callback result');
$this->assertInstanceOf(Collection::class, $result);
$this->assertSame(1, $result[0]->id);
$this->assertSame(2, $result[1]->id);
$this->assertNull($result[0]->name);
$this->assertNull($result[1]->name);

$result = $post->tags()->findOr(new Collection([1, 2, 3]), fn () => 'callback result');
$this->assertSame('callback result', $result);
}

public function testFirstOrNewMethod()
{
$post = Post::create(['title' => Str::random()]);
Expand Down

0 comments on commit b062355

Please sign in to comment.