Skip to content

Commit

Permalink
[11.x] Added Support for automatically guessing inverse polymorphic r…
Browse files Browse the repository at this point in the history
…elations
  • Loading branch information
samlev committed May 31, 2024
1 parent eb42cbf commit 3e7ae71
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
165 changes: 82 additions & 83 deletions tests/Database/DatabaseEloquentInverseRelationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand All @@ -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();

Expand Down Expand Up @@ -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
Expand All @@ -316,9 +301,6 @@ public function getForeignKey()
return 'parent_stub_id';
}
}
class HasInverseRelationParentSubclassStub extends HasInverseRelationParentStub
{
}

class HasInverseRelationRelatedStub extends Model
{
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 3e7ae71

Please sign in to comment.