Skip to content

Commit

Permalink
[11.x] Cleaned up Morph* relation guessing
Browse files Browse the repository at this point in the history
  • Loading branch information
samlev committed Jun 1, 2024
1 parent cc25c9d commit ff6ca30
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\RelationNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

trait SupportsInverseRelations
Expand Down Expand Up @@ -66,14 +67,13 @@ public function withoutInverse()
*/
protected function getPossibleInverseRelations(): array
{
return collect([
method_exists($this, 'getMorphType') ? Str::beforeLast($this->getMorphType(), '_type') : null,
return array_filter(array_unique([
Str::camel(Str::beforeLast($this->getForeignKeyName(), $this->getParent()->getKeyName())),
Str::camel(Str::beforeLast($this->getParent()->getForeignKey(), $this->getParent()->getKeyName())),
Str::camel(class_basename($this->getParent())),
'owner',
get_class($this->getParent()) === get_class($this->getModel()) ? 'parent' : null,
])->filter()->unique()->values()->all();
]));
}

/**
Expand All @@ -83,9 +83,10 @@ protected function getPossibleInverseRelations(): array
*/
protected function guessInverseRelation(): string|null
{
return collect($this->getPossibleInverseRelations())
->filter()
->firstWhere(fn ($relation) => $this->getModel()->isRelation($relation));
return Arr::first(
$this->getPossibleInverseRelations(),
fn ($relation) => $relation && $this->getModel()->isRelation($relation)
);
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

abstract class MorphOneOrMany extends HasOneOrMany
{
Expand Down Expand Up @@ -140,4 +141,17 @@ public function getMorphClass()
{
return $this->morphClass;
}

/**
* Gets possible inverse relations for the parent model.
*
* @return array<non-empty-string>
*/
protected function getPossibleInverseRelations(): array
{
return array_unique([
Str::beforeLast($this->getMorphType(), '_type'),
... parent::getPossibleInverseRelations(),
]);
}
}
67 changes: 67 additions & 0 deletions tests/Database/DatabaseEloquentInverseRelationMorphManyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,36 @@ public function testMorphManyInverseRelationIsProperlySetToParentWhenEagerLoaded
}
}

public function testMorphManyGuessedInverseRelationIsProperlySetToParentWhenLazyLoaded()
{
MorphManyInversePostModel::factory()->withComments()->count(3)->create();
$posts = MorphManyInversePostModel::all();

foreach ($posts as $post) {
$this->assertFalse($post->relationLoaded('guessedComments'));
$comments = $post->guessedComments;
foreach ($comments as $comment) {
$this->assertTrue($comment->relationLoaded('commentable'));
$this->assertSame($post, $comment->commentable);
}
}
}

public function testMorphManyGuessedInverseRelationIsProperlySetToParentWhenEagerLoaded()
{
MorphManyInversePostModel::factory()->withComments()->count(3)->create();
$posts = MorphManyInversePostModel::with('guessedComments')->get();

foreach ($posts as $post) {
$comments = $post->getRelation('guessedComments');

foreach ($comments as $comment) {
$this->assertTrue($comment->relationLoaded('commentable'));
$this->assertSame($post, $comment->commentable);
}
}
}

public function testMorphLatestOfManyInverseRelationIsProperlySetToParentWhenLazyLoaded()
{
MorphManyInversePostModel::factory()->count(3)->withComments()->create();
Expand Down Expand Up @@ -115,6 +145,33 @@ public function testMorphLatestOfManyInverseRelationIsProperlySetToParentWhenEag
}
}

public function testMorphLatestOfManyGuessedInverseRelationIsProperlySetToParentWhenLazyLoaded()
{
MorphManyInversePostModel::factory()->count(3)->withComments()->create();
$posts = MorphManyInversePostModel::all();

foreach ($posts as $post) {
$this->assertFalse($post->relationLoaded('guessedLastComment'));
$comment = $post->guessedLastComment;

$this->assertTrue($comment->relationLoaded('commentable'));
$this->assertSame($post, $comment->commentable);
}
}

public function testMorphLatestOfManyGuessedInverseRelationIsProperlySetToParentWhenEagerLoaded()
{
MorphManyInversePostModel::factory()->count(3)->withComments()->create();
$posts = MorphManyInversePostModel::with('guessedLastComment')->get();

foreach ($posts as $post) {
$comment = $post->getRelation('guessedLastComment');

$this->assertTrue($comment->relationLoaded('commentable'));
$this->assertSame($post, $comment->commentable);
}
}

public function testMorphOneOfManyInverseRelationIsProperlySetToParentWhenLazyLoaded()
{
MorphManyInversePostModel::factory()->count(3)->withComments()->create();
Expand Down Expand Up @@ -249,11 +306,21 @@ public function comments(): MorphMany
return $this->morphMany(MorphManyInverseCommentModel::class, 'commentable')->inverse('commentable');
}

public function guessedComments(): MorphMany
{
return $this->morphMany(MorphManyInverseCommentModel::class, 'commentable')->inverse();
}

public function lastComment(): MorphOne
{
return $this->morphOne(MorphManyInverseCommentModel::class, 'commentable')->latestOfMany()->inverse('commentable');
}

public function guessedLastComment(): MorphOne
{
return $this->morphOne(MorphManyInverseCommentModel::class, 'commentable')->latestOfMany()->inverse();
}

public function firstComment(): MorphOne
{
return $this->comments()->one();
Expand Down
31 changes: 31 additions & 0 deletions tests/Database/DatabaseEloquentInverseRelationMorphOneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,32 @@ public function testMorphOneInverseRelationIsProperlySetToParentWhenEagerLoaded(
}
}

public function testMorphOneGuessedInverseRelationIsProperlySetToParentWhenLazyLoaded()
{
MorphOneInverseImageModel::factory(6)->create();
$posts = MorphOneInversePostModel::all();

foreach ($posts as $post) {
$this->assertFalse($post->relationLoaded('guessedImage'));
$image = $post->guessedImage;
$this->assertTrue($image->relationLoaded('imageable'));
$this->assertSame($post, $image->imageable);
}
}

public function testMorphOneGuessedInverseRelationIsProperlySetToParentWhenEagerLoaded()
{
MorphOneInverseImageModel::factory(6)->create();
$posts = MorphOneInversePostModel::with('guessedImage')->get();

foreach ($posts as $post) {
$image = $post->getRelation('guessedImage');

$this->assertTrue($image->relationLoaded('imageable'));
$this->assertSame($post, $image->imageable);
}
}

public function testMorphOneInverseRelationIsProperlySetToParentWhenMaking()
{
$post = MorphOneInversePostModel::create();
Expand Down Expand Up @@ -201,6 +227,11 @@ public function image(): MorphOne
{
return $this->morphOne(MorphOneInverseImageModel::class, 'imageable')->inverse('imageable');
}

public function guessedImage(): MorphOne
{
return $this->morphOne(MorphOneInverseImageModel::class, 'imageable')->inverse();
}
}

class MorphOneInversePostModelFactory extends Factory
Expand Down
68 changes: 1 addition & 67 deletions tests/Database/DatabaseEloquentInverseRelationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function testProvidesPossibleInverseRelationBasedOnParent()
$relation = (new HasInverseRelationStub($builder, new HasInverseRelationParentStub));

$possibleRelations = ['hasInverseRelationParentStub', 'parentStub', 'owner'];
$this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations());
$this->assertSame($possibleRelations, array_values($relation->exposeGetPossibleInverseRelations()));
}

public function testProvidesPossibleInverseRelationBasedOnForeignKey()
Expand All @@ -171,16 +171,6 @@ public function testProvidesPossibleInverseRelationBasedOnForeignKey()
$this->assertTrue(in_array('test', $relation->exposeGetPossibleInverseRelations()));
}

public function testProvidesPossiblePolymorphicRelationsIfRelationHasGetMorphType()
{
$builder = m::mock(Builder::class);
$builder->shouldReceive('getModel')->andReturn(new HasOneInverseChildModel);

$relation = new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'fooable_type');

$this->assertTrue(in_array('fooable', $relation->exposeGetPossibleInverseRelations()));
}

public function testProvidesPossibleRecursiveRelationsIfRelatedIsTheSameClassAsParent()
{
$builder = m::mock(Builder::class);
Expand All @@ -191,17 +181,6 @@ public function testProvidesPossibleRecursiveRelationsIfRelatedIsTheSameClassAsP
$this->assertTrue(in_array('parent', $relation->exposeGetPossibleInverseRelations()));
}

public function testProvidesAllPossibleRelationsIfRelationHasGetMorphTypeForeignKeyAndRelatedIsTheSameClassAsParent()
{
$builder = m::mock(Builder::class);
$builder->shouldReceive('getModel')->andReturn(new HasInverseRelationParentStub);

$relation = new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'barable_type', 'test_id');

$possibleRelations = ['barable', 'test', 'parentStub', 'hasInverseRelationParentStub', 'owner', 'parent'];
$this->assertSame($possibleRelations, $relation->exposeGetPossibleInverseRelations());
}

#[DataProvider('guessedParentRelationsDataProvider')]
public function testGuessesInverseRelationBasedOnParent($guessedRelation)
{
Expand Down Expand Up @@ -246,19 +225,6 @@ public function testGuessesRecursiveInverseRelationsIfRelatedIsSameClassAsParent
$this->assertSame('parent', $relation->exposeGuessInverseRelation());
}

public function testGuessesPolymorphicInverseRelationsIfRelationHasGetMorphType()
{
$related = m::mock(Model::class);
$related->shouldReceive('isRelation')->andReturnUsing(fn ($relation) => $relation === 'bazable');

$builder = m::mock(Builder::class);
$builder->shouldReceive('getModel')->andReturn($related);

$relation = new HasInversePolymorphicRelationStub($builder, new HasInverseRelationParentStub, 'bazable_type');

$this->assertSame('bazable', $relation->exposeGuessInverseRelation());
}

#[DataProvider('guessedParentRelationsDataProvider')]
public function testSetsGuessedInverseRelationBasedOnParent($guessedRelation)
{
Expand Down Expand Up @@ -292,20 +258,6 @@ public function testSetsRecursiveInverseRelationsIfRelatedIsSameClassAsParent()
$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 function testSetsGuessedInverseRelationBasedOnForeignKey()
{
$related = m::mock(Model::class);
Expand Down Expand Up @@ -410,21 +362,3 @@ public function exposeGuessInverseRelation(): string|null
return $this->guessInverseRelation();
}
}

class HasInversePolymorphicRelationStub extends HasInverseRelationStub
{
public function __construct(
Builder $query,
Model $parent,
protected string $morphType,
?string $foreignKey = null,
) {
parent::__construct($query, $parent, $foreignKey);
$this->morphType = Str::of($morphType)->snake()->finish('_type')->toString();
}

protected function getMorphType()
{
return $this->morphType;
}
}

0 comments on commit ff6ca30

Please sign in to comment.