From 3e7ae71ad2bb5de7173aec066ec9afdde4bcb8c8 Mon Sep 17 00:00:00 2001 From: Samuel Levy Date: Fri, 31 May 2024 13:45:15 +1000 Subject: [PATCH] [11.x] Added Support for automatically guessing inverse polymorphic relations --- .../Concerns/SupportsInverseRelations.php | 7 +- .../DatabaseEloquentInverseRelationTest.php | 165 +++++++++--------- 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsInverseRelations.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsInverseRelations.php index a25985028e07..0801e7c32690 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsInverseRelations.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsInverseRelations.php @@ -69,12 +69,15 @@ protected function getPossibleInverseRelations(): array $possibleInverseRelations = [ Str::camel(Str::beforeLast($this->getParent()->getForeignKey(), $this->getParent()->getKeyName())), Str::camel(class_basename($this->getParent())), - 'ownedBy', 'owner', ]; + if (method_exists($this, 'getMorphType')) { + array_push($possibleInverseRelations, Str::beforeLast($this->getMorphType(), '_type')); + } + if (get_class($this->getParent()) === get_class($this->getModel())) { - array_push($possibleInverseRelations, 'parent', 'ancestor'); + array_push($possibleInverseRelations, 'parent'); } return array_filter($possibleInverseRelations); diff --git a/tests/Database/DatabaseEloquentInverseRelationTest.php b/tests/Database/DatabaseEloquentInverseRelationTest.php index f0a0da29e650..4bdd0c461e1c 100755 --- a/tests/Database/DatabaseEloquentInverseRelationTest.php +++ b/tests/Database/DatabaseEloquentInverseRelationTest.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsInverseRelations; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Str; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -32,29 +33,28 @@ public function testBuilderCallbackIsNotAppliedWhenInverseRelationIsNotSet() public function testBuilderCallbackIsNotSetIfInverseRelationIsEmptyString() { $builder = m::mock(Builder::class); - - $this->expectException(RelationNotFoundException::class); $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationRelatedStub()); $builder->shouldReceive('afterQuery')->never(); + $this->expectException(RelationNotFoundException::class); + (new HasInverseRelationStub($builder, new HasInverseRelationParentStub()))->inverse(''); } public function testBuilderCallbackIsNotSetIfInverseRelationshipDoesNotExist() { $builder = m::mock(Builder::class); - - $this->expectException(RelationNotFoundException::class); $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationRelatedStub()); $builder->shouldReceive('afterQuery')->never(); + $this->expectException(RelationNotFoundException::class); + (new HasInverseRelationStub($builder, new HasInverseRelationParentStub()))->inverse('foo'); } public function testWithoutInverseMethodRemovesInverseRelation() { $builder = m::mock(Builder::class); - $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationRelatedStub()); $builder->shouldReceive('afterQuery')->once()->andReturnSelf(); @@ -70,10 +70,10 @@ public function testWithoutInverseMethodRemovesInverseRelation() public function testBuilderCallbackIsAppliedWhenInverseRelationIsSet() { + $parent = new HasInverseRelationParentStub(); + $builder = m::mock(Builder::class); $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationRelatedStub()); - - $parent = new HasInverseRelationParentStub(); $builder->shouldReceive('afterQuery')->withArgs(function (\Closure $callback) use ($parent) { $relation = (new \ReflectionFunction($callback))->getClosureThis(); @@ -150,160 +150,145 @@ public function testInverseRelationIsNotSetIfInverseRelationIsUnset() } } - public function testProvidesPossibleRelationBasedOnParent() + public function testProvidesPossibleInverseRelationBasedOnParent() { $builder = m::mock(Builder::class); $builder->shouldReceive('getModel')->andReturn(new HasOneInverseChildModel); - $parent = new HasInverseRelationParentStub; - $relation = (new HasInverseRelationStub($builder, $parent)); - - $possibleRelations = ['parentStub', 'hasInverseRelationParentStub', 'ownedBy', 'owner']; - $this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations()); - } - - public function testProvidesPossibleRecursiveRelationsIfRelatedIsTheSameClassAsParent() - { - $builder = m::mock(Builder::class); - $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationParentStub); - - $parent = new HasInverseRelationParentStub; - $relation = (new HasInverseRelationStub($builder, $parent)); + $relation = (new HasInverseRelationStub($builder, new HasInverseRelationParentStub)); - $possibleRelations = ['parentStub', 'hasInverseRelationParentStub', 'ownedBy', 'owner', 'parent', 'ancestor']; + $possibleRelations = ['parentStub', 'hasInverseRelationParentStub', 'owner']; $this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations()); } - public function testDoesNotProvidePossibleRecursiveRelationsIfRelatedIsNotTheSameClassAsParent() + public function testProvidesPossiblePolymorphicRelationsIfRelationHasGetMorphType() { $builder = m::mock(Builder::class); $builder->shouldReceive('getModel')->andReturn(new HasOneInverseChildModel); - $parent = new HasInverseRelationParentStub; - $relation = (new HasInverseRelationStub($builder, $parent)); + $relation = new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'fooable_type'); - $possibleRelations = ['parentStub', 'hasInverseRelationParentStub', 'ownedBy', 'owner']; - $this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations()); + $this->assertTrue(in_array('fooable', $relation->exposeGetPossibleInverseRelations())); } - public function testDoesNotProvidePossibleRecursiveRelationsIfRelatedClassIsAncestorOfParent() + public function testProvidesPossibleRecursiveRelationsIfRelatedIsTheSameClassAsParent() { $builder = m::mock(Builder::class); $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationParentStub); - $parent = new HasInverseRelationParentSubclassStub; - $relation = (new HasInverseRelationStub($builder, $parent)); + $relation = (new HasInverseRelationStub($builder, new HasInverseRelationParentStub)); - $possibleRelations = ['parentStub', 'hasInverseRelationParentSubclassStub', 'ownedBy', 'owner']; - $this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations()); + $this->assertTrue(in_array('parent', $relation->exposeGetPossibleInverseRelations())); } - public function testDoesNotProvidePossibleRecursiveRelationsIfRelatedClassIsSubclassOfParent() + public function testProvidesAllPossibleRelationsIfRelationHasGetMorphTypeAndRelatedIsTheSameClassAsParent() { $builder = m::mock(Builder::class); - $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationParentSubclassStub); + $builder->shouldReceive('getModel')->andReturn(new HasInverseRelationParentStub); - $parent = new HasInverseRelationParentStub; - $relation = (new HasInverseRelationStub($builder, $parent)); + $relation = new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'barable_type'); - $possibleRelations = ['parentStub', 'hasInverseRelationParentStub', 'ownedBy', 'owner']; + $possibleRelations = ['parentStub', 'hasInverseRelationParentStub', 'owner', 'barable', 'parent']; $this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations()); } #[DataProvider('guessedParentRelationsDataProvider')] public function testGuessesInverseRelationBasedOnParent($guessedRelation) { - $builder = m::mock(Builder::class); $related = m::mock(Model::class); + $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === $guessedRelation); + + $builder = m::mock(Builder::class); $builder->shouldReceive('getModel')->andReturn($related); - $parent = new HasInverseRelationParentStub; - $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === $guessedRelation); - $relation = (new HasInverseRelationStub($builder, $parent)); + $relation = (new HasInverseRelationStub($builder, new HasInverseRelationParentStub)); $this->assertSame($guessedRelation, $relation->exposeGuessInverseRelation()); } - #[DataProvider('guessedRecursiveRelationsDataProvider')] - public function testGuessesRecursiveInverseRelationsIfRelatedIsSameClassAsParent($guessedRelation) + public function testGuessesRecursiveInverseRelationsIfRelatedIsSameClassAsParent() { - $builder = m::mock(Builder::class); $related = m::mock(Model::class); - $builder->shouldReceive('getModel')->andReturn($related); + $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === 'parent'); $parent = clone $related; $parent->shouldReceive('getForeignKey')->andReturn('recursive_parent_id'); $parent->shouldReceive('getKeyName')->andReturn('id'); - $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === $guessedRelation); + + $builder = m::mock(Builder::class); + $builder->shouldReceive('getModel')->andReturn($related); $relation = (new HasInverseRelationStub($builder, $parent)); - $this->assertSame($guessedRelation, $relation->exposeGuessInverseRelation()); + $this->assertSame('parent', $relation->exposeGuessInverseRelation()); } - #[DataProvider('guessedRecursiveRelationsDataProvider')] - public function testDoesNotGuessRecursiveInverseRelationsIfRelatedIsNotSameClassAsParent($guessedRelation) + public function testGuessesPolymorphicInverseRelationsIfRelationHasGetMorphType() { - $builder = m::mock(Builder::class); $related = m::mock(Model::class); - $builder->shouldReceive('getModel')->andReturn($related); + $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === 'bazable'); - $related->shouldReceive('isRelation')->andReturn(false); - $related->shouldReceive('isRelation')->with($guessedRelation)->never(); + $builder = m::mock(Builder::class); + $builder->shouldReceive('getModel')->andReturn($related); - $relation = new HasInverseRelationStub($builder, new HasInverseRelationParentStub); + $relation = new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'bazable_type'); - $this->assertNull($relation->exposeGuessInverseRelation()); + $this->assertSame('bazable', $relation->exposeGuessInverseRelation()); } #[DataProvider('guessedParentRelationsDataProvider')] public function testSetsGuessedInverseRelationBasedOnParent($guessedRelation) { - $builder = m::mock(Builder::class); $related = m::mock(Model::class); - $builder->shouldReceive('getModel')->andReturn($related); + $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === $guessedRelation); - $parent = new HasInverseRelationParentStub; + $builder = m::mock(Builder::class); + $builder->shouldReceive('getModel')->andReturn($related); $builder->shouldReceive('afterQuery')->once()->andReturnSelf(); - $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === $guessedRelation); - $relation = (new HasInverseRelationStub($builder, $parent))->inverse(); + + $relation = (new HasInverseRelationStub($builder, new HasInverseRelationParentStub))->inverse(); $this->assertSame($guessedRelation, $relation->getInverseRelationship()); } - #[DataProvider('guessedRecursiveRelationsDataProvider')] - public function testDoesNotSetRecursiveInverseRelationsIfRelatedIsNotSameClassAsParent($guessedRelation) + public function testSetsRecursiveInverseRelationsIfRelatedIsSameClassAsParent() { - $builder = m::mock(Builder::class); $related = m::mock(Model::class); - $builder->shouldReceive('getModel')->andReturn($related); + $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === 'parent'); - $parent = new HasInverseRelationParentStub; - $builder->shouldReceive('afterQuery')->never(); - foreach (self::guessedParentRelationsDataProvider() as $notRelated) { - $related->shouldReceive('isRelation')->with($notRelated[0])->once()->andReturn(false); - } + $parent = clone $related; + $parent->shouldReceive('getForeignKey')->andReturn('recursive_parent_id'); + $parent->shouldReceive('getKeyName')->andReturn('id'); + + $builder = m::mock(Builder::class); + $builder->shouldReceive('getModel')->andReturn($related); + $builder->shouldReceive('afterQuery')->once()->andReturnSelf(); - $related->shouldReceive('isRelation')->with($guessedRelation)->never(); - $this->expectException(RelationNotFoundException::class); $relation = (new HasInverseRelationStub($builder, $parent))->inverse(); - $this->assertNull($relation->getInverseRelationship()); + $this->assertSame('parent', $relation->getInverseRelationship()); + } + + public function testSetsPolymorphicInverseRelationsIfRelationHasGetMorphType() + { + $related = m::mock(Model::class); + $related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === 'bingable'); + + $builder = m::mock(Builder::class); + $builder->shouldReceive('getModel')->andReturn($related); + $builder->shouldReceive('afterQuery')->once()->andReturnSelf(); + + $relation = (new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'bingable_type'))->inverse(); + + $this->assertSame('bingable', $relation->getInverseRelationship()); } public static function guessedParentRelationsDataProvider() { yield ['parentStub']; yield ['hasInverseRelationParentStub']; - yield ['ownedBy']; yield ['owner']; } - - public static function guessedRecursiveRelationsDataProvider() - { - yield ['parent']; - yield ['ancestor']; - } } class HasInverseRelationParentStub extends Model @@ -316,9 +301,6 @@ public function getForeignKey() return 'parent_stub_id'; } } -class HasInverseRelationParentSubclassStub extends HasInverseRelationParentStub -{ -} class HasInverseRelationRelatedStub extends Model { @@ -377,3 +359,20 @@ public function exposeGuessInverseRelation(): string|null return $this->guessInverseRelation(); } } + +class HasInversePolymorphicRelationStub extends HasInverseRelationStub +{ + public function __construct( + Builder $query, + Model $parent, + protected string $morphType, + ) { + parent::__construct($query, $parent); + $this->morphType = Str::of($morphType)->snake()->finish('_type')->toString(); + } + + protected function getMorphType() + { + return $this->morphType; + } +}