From 43c8bff8dbc802fb92380c632a3d20727fd4f304 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Tue, 28 Sep 2021 20:29:56 +0100 Subject: [PATCH] [8.x] Add `whereBelongsTo()` Eloquent builder method (#38927) * Add failing test for whereBelongsTo * Implement whereBelongsTo * mock in tests * Add test for relationship guess * Add exceptions * Remove early returns * Update Builder.php * Update Builder.php * Throw RelationNotFoundException * Refactor to catch BadMethodCallException * Remove rogue get_class * move method to trait * qualify foreign key * formatting Co-authored-by: Taylor Otwell --- .../Concerns/QueriesRelationships.php | 53 +++++++++++++++++++ .../Eloquent/RelationNotFoundException.php | 9 +++- .../Database/DatabaseEloquentBuilderTest.php | 47 ++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index c2baa667bc2a..aae3ffb11860 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -2,8 +2,11 @@ namespace Illuminate\Database\Eloquent\Concerns; +use BadMethodCallException; use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\RelationNotFoundException; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -455,6 +458,56 @@ public function orWhereMorphedTo($relation, $model) return $this->whereMorphedTo($relation, $model, 'or'); } + /** + * Add a "belongs to" relationship where clause to the query. + * + * @param \Illuminate\Database\Eloquent\Model $related + * @param string $relationship + * @param string $boolean + * @return $this + * + * @throws \Exception + */ + public function whereBelongsTo($related, $relationshipName = null, $boolean = 'and') + { + if ($relationshipName === null) { + $relationshipName = Str::camel(class_basename($related)); + } + + try { + $relationship = $this->model->{$relationshipName}(); + } catch (BadMethodCallException $exception) { + throw RelationNotFoundException::make($this->model, $relationshipName); + } + + if (! $relationship instanceof BelongsTo) { + throw RelationNotFoundException::make($this->model, $relationshipName, BelongsTo::class); + } + + $this->where( + $relationship->getQualifiedForeignKeyName(), + '=', + $related->getAttributeValue($relationship->getOwnerKeyName()), + $boolean, + ); + + return $this; + } + + /** + * Add an "BelongsTo" relationship with an "or where" clause to the query. + * + * @param \Illuminate\Database\Eloquent\Model $related + * @param string $relationship + * @return $this + * + * @throws \Exception + */ + public function orWhereBelongsTo($related, $relationshipName = null) + { + return $this->whereBelongsTo($related, $relationshipName, 'or'); + } + /** * Add subselect queries to include an aggregate value for a relationship. * diff --git a/src/Illuminate/Database/Eloquent/RelationNotFoundException.php b/src/Illuminate/Database/Eloquent/RelationNotFoundException.php index 5acc0b309562..73257bb101e0 100755 --- a/src/Illuminate/Database/Eloquent/RelationNotFoundException.php +++ b/src/Illuminate/Database/Eloquent/RelationNotFoundException.php @@ -25,13 +25,18 @@ class RelationNotFoundException extends RuntimeException * * @param object $model * @param string $relation + * @param string|null $type * @return static */ - public static function make($model, $relation) + public static function make($model, $relation, $type = null) { $class = get_class($model); - $instance = new static("Call to undefined relationship [{$relation}] on model [{$class}]."); + $instance = new static( + is_null($type) + ? "Call to undefined relationship [{$relation}] on model [{$class}]." + : "Call to undefined relationship [{$relation}] on model [{$class}] of type [{$type}].", + ); $instance->model = $class; $instance->relation = $relation; diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 8e36ff1dc48e..863344c4ec46 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -906,6 +906,35 @@ public function testPostgresOperatorsWhere() $this->assertEquals($result, $builder); } + public function testWhereBelongsTo() + { + $related = new EloquentBuilderTestWhereBelongsToStub([ + 'id' => 1, + 'parent_id' => 2, + ]); + + $parent = new EloquentBuilderTestWhereBelongsToStub([ + 'id' => 2, + 'parent_id' => 1, + ]); + + $builder = $this->getBuilder(); + $builder->shouldReceive('from')->with('eloquent_builder_test_where_belongs_to_stubs'); + $builder->setModel($related); + $builder->getQuery()->shouldReceive('where')->once()->with('eloquent_builder_test_where_belongs_to_stubs.parent_id', '=', 2, 'and'); + + $result = $builder->whereBelongsTo($parent); + $this->assertEquals($result, $builder); + + $builder = $this->getBuilder(); + $builder->shouldReceive('from')->with('eloquent_builder_test_where_belongs_to_stubs'); + $builder->setModel($related); + $builder->getQuery()->shouldReceive('where')->once()->with('eloquent_builder_test_where_belongs_to_stubs.parent_id', '=', 2, 'and'); + + $result = $builder->whereBelongsTo($parent, 'parent'); + $this->assertEquals($result, $builder); + } + public function testDeleteOverride() { $builder = $this->getBuilder(); @@ -1948,3 +1977,21 @@ class EloquentBuilderTestStubStringPrimaryKey extends Model protected $keyType = 'string'; } + +class EloquentBuilderTestWhereBelongsToStub extends Model +{ + protected $fillable = [ + 'id', + 'parent_id', + ]; + + public function eloquentBuilderTestWhereBelongsToStub() + { + return $this->belongsTo(self::class, 'parent_id', 'id', 'parent'); + } + + public function parent() + { + return $this->belongsTo(self::class, 'parent_id', 'id', 'parent'); + } +}