From fc6648388c23f7f825f2f22883ef687795944831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Sjo=CC=88berg?= Date: Tue, 4 Aug 2015 21:56:15 +0200 Subject: [PATCH 1/2] Added the ability to specify a morph map for polymorphic relations --- src/Illuminate/Database/Eloquent/Model.php | 28 ++++++++++- .../Database/Eloquent/Relations/MorphTo.php | 4 +- .../Database/Eloquent/Relations/Relation.php | 27 +++++++++++ .../DatabaseEloquentIntegrationTest.php | 48 +++++++++++++++++++ tests/Database/DatabaseEloquentMorphTest.php | 31 ++++++++++++ 5 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 1b6f58af97dd..235396091f3a 100755 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -870,6 +870,8 @@ public function morphTo($name = null, $type = null, $id = null) // as a belongs-to style relationship since morph-to extends that class and // we will pass in the appropriate values so that it behaves as expected. else { + $class = $this->getActualClassName($class); + $instance = new $class; return new MorphTo( @@ -878,6 +880,23 @@ public function morphTo($name = null, $type = null, $id = null) } } + /** + * Retrieve the fully qualified class name from a slug. + * + * @param string $class + * @return string + */ + protected function getActualClassName($class) + { + $morphMap = Relation::morphMap(); + + if (isset($morphMap[$class])) { + return $morphMap[$class]; + } + + return $class; + } + /** * Define a one-to-many relationship. * @@ -2054,7 +2073,14 @@ protected function getMorphs($name, $type, $id) */ public function getMorphClass() { - return $this->morphClass ?: get_class($this); + $morphMap = Relation::morphMap(); + $class = get_class($this); + + if (! empty($morphMap) && in_array($class, $morphMap)) { + return array_flip($morphMap)[$class]; + } + + return $this->morphClass ?: $class; } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php index bfc2ceff1798..49f80e43e515 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphTo.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphTo.php @@ -213,7 +213,9 @@ protected function gatherKeysByType($type) */ public function createModelByType($type) { - return new $type; + $class = $this->parent->getActualClassName($type); + + return new $class; } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 3b9d39df3a54..65e47e4904fc 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -38,6 +38,13 @@ abstract class Relation */ protected static $constraints = true; + /** + * An array to map class names to their morph names in database. + * + * @var array + */ + protected static $morphMap = []; + /** * Create a new relation instance. * @@ -272,6 +279,26 @@ public function wrap($value) return $this->parent->newQueryWithoutScopes()->getQuery()->getGrammar()->wrap($value); } + /** + * Set the morph map for polymorphic relations. + * + * @param array|null $map + * @param bool $merge + * @return array + */ + public static function morphMap($map = null, $merge = true) + { + if (is_array($map)) { + if ($merge) { + array_merge(static::$morphMap, $map); + } else { + static::$morphMap = $map; + } + } + + return static::$morphMap; + } + /** * Handle dynamic method calls to the relationship. * diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 26b71bf48e16..73407351e188 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -76,6 +76,8 @@ public function tearDown() $this->schema()->drop('friends'); $this->schema()->drop('posts'); $this->schema()->drop('photos'); + + Illuminate\Database\Eloquent\Relations\Relation::morphMap([], false); } /** @@ -340,6 +342,52 @@ public function testBasicMorphManyRelationship() $this->assertEquals('First Post', $photos[3]->imageable->name); } + public function testMorphMapIsUsedForCreatingAndFetchingThroughRelation() + { + Illuminate\Database\Eloquent\Relations\Relation::morphMap([ + 'user' => 'EloquentTestUser', + 'post' => 'EloquentTestPost', + ], false); + + $user = EloquentTestUser::create(['email' => 'taylorotwell@gmail.com']); + $user->photos()->create(['name' => 'Avatar 1']); + $user->photos()->create(['name' => 'Avatar 2']); + $post = $user->posts()->create(['name' => 'First Post']); + $post->photos()->create(['name' => 'Hero 1']); + $post->photos()->create(['name' => 'Hero 2']); + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $user->photos); + $this->assertInstanceOf('EloquentTestPhoto', $user->photos[0]); + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $post->photos); + $this->assertInstanceOf('EloquentTestPhoto', $post->photos[0]); + $this->assertEquals(2, $user->photos->count()); + $this->assertEquals(2, $post->photos->count()); + $this->assertEquals('Avatar 1', $user->photos[0]->name); + $this->assertEquals('Avatar 2', $user->photos[1]->name); + $this->assertEquals('Hero 1', $post->photos[0]->name); + $this->assertEquals('Hero 2', $post->photos[1]->name); + + $this->assertEquals('user', $user->photos[0]->imageable_type); + $this->assertEquals('user', $user->photos[1]->imageable_type); + $this->assertEquals('post', $post->photos[0]->imageable_type); + $this->assertEquals('post', $post->photos[1]->imageable_type); + } + + public function testMorphMapIsUsedWhenFetchingParent() + { + Illuminate\Database\Eloquent\Relations\Relation::morphMap([ + 'user' => 'EloquentTestUser', + 'post' => 'EloquentTestPost', + ], false); + + $user = EloquentTestUser::create(['email' => 'taylorotwell@gmail.com']); + $user->photos()->create(['name' => 'Avatar 1']); + + $photo = EloquentTestPhoto::first(); + $this->assertEquals('user', $photo->imageable_type); + $this->assertInstanceOf('EloquentTestUser', $photo->imageable); + } + public function testEmptyMorphToRelationship() { $photo = EloquentTestPhoto::create(['name' => 'Avatar 1']); diff --git a/tests/Database/DatabaseEloquentMorphTest.php b/tests/Database/DatabaseEloquentMorphTest.php index 6ea33bf3653c..817bc92a7130 100755 --- a/tests/Database/DatabaseEloquentMorphTest.php +++ b/tests/Database/DatabaseEloquentMorphTest.php @@ -165,6 +165,18 @@ public function testUpdateOrCreateMethodCreatesNewMorphModel() $this->assertTrue($relation->updateOrCreate(['foo'], ['bar']) instanceof Model); } + public function testCreateFunctionOnNamespacedMorph() + { + $relation = $this->getNamespacedRelation('namespace'); + $created = m::mock('Illuminate\Database\Eloquent\Model'); + $created->shouldReceive('setAttribute')->once()->with('morph_id', 1); + $created->shouldReceive('setAttribute')->once()->with('morph_type', 'namespace'); + $relation->getRelated()->shouldReceive('newInstance')->once()->with(['name' => 'taylor'])->andReturn($created); + $created->shouldReceive('save')->once()->andReturn(true); + + $this->assertEquals($created, $relation->create(['name' => 'taylor'])); + } + protected function getOneRelation() { $builder = m::mock('Illuminate\Database\Eloquent\Builder'); @@ -194,6 +206,25 @@ protected function getManyRelation() return new MorphMany($builder, $parent, 'table.morph_type', 'table.morph_id', 'id'); } + + protected function getNamespacedRelation($alias) + { + Illuminate\Database\Eloquent\Relations\Relation::morphMap([ + $alias => 'Foo\Bar\EloquentModelNamespacedStub', + ]); + + $builder = m::mock('Illuminate\Database\Eloquent\Builder'); + $builder->shouldReceive('whereNotNull')->once()->with('table.morph_id'); + $builder->shouldReceive('where')->once()->with('table.morph_id', '=', 1); + $related = m::mock('Illuminate\Database\Eloquent\Model'); + $builder->shouldReceive('getModel')->andReturn($related); + $parent = m::mock('Foo\Bar\EloquentModelNamespacedStub'); + $parent->shouldReceive('getAttribute')->with('id')->andReturn(1); + $parent->shouldReceive('getMorphClass')->andReturn($alias); + $builder->shouldReceive('where')->once()->with('table.morph_type', $alias); + + return new MorphOne($builder, $parent, 'table.morph_type', 'table.morph_id', 'id'); + } } class EloquentMorphResetModelStub extends Illuminate\Database\Eloquent\Model From 719f391bd2eb4bad62dd7b60083496fa523a3a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Sjo=CC=88berg?= Date: Sun, 9 Aug 2015 16:47:43 +0200 Subject: [PATCH 2/2] Improved performance of morph map and clarified structure and intent --- src/Illuminate/Database/Eloquent/Model.php | 4 +- .../Database/Eloquent/Relations/Relation.php | 12 ++---- .../DatabaseEloquentIntegrationTest.php | 37 +++++++++++++++++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 235396091f3a..984754600043 100755 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2076,8 +2076,8 @@ public function getMorphClass() $morphMap = Relation::morphMap(); $class = get_class($this); - if (! empty($morphMap) && in_array($class, $morphMap)) { - return array_flip($morphMap)[$class]; + if (! empty($morphMap)) { + return array_search($class, $morphMap, true); } return $this->morphClass ?: $class; diff --git a/src/Illuminate/Database/Eloquent/Relations/Relation.php b/src/Illuminate/Database/Eloquent/Relations/Relation.php index 65e47e4904fc..f1724866307f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/src/Illuminate/Database/Eloquent/Relations/Relation.php @@ -282,18 +282,14 @@ public function wrap($value) /** * Set the morph map for polymorphic relations. * - * @param array|null $map - * @param bool $merge + * @param array|null $map + * @param bool $merge * @return array */ - public static function morphMap($map = null, $merge = true) + public static function morphMap(array $map = null, $merge = true) { if (is_array($map)) { - if ($merge) { - array_merge(static::$morphMap, $map); - } else { - static::$morphMap = $map; - } + static::$morphMap = $merge ? array_merge(static::$morphMap, $map) : $map; } return static::$morphMap; diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 73407351e188..f95453a65032 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -3,6 +3,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Model as Eloquent; use Illuminate\Pagination\AbstractPaginator as Paginator; +use Illuminate\Database\Eloquent\Relations\Relation; class DatabaseEloquentIntegrationTest extends PHPUnit_Framework_TestCase { @@ -347,7 +348,7 @@ public function testMorphMapIsUsedForCreatingAndFetchingThroughRelation() Illuminate\Database\Eloquent\Relations\Relation::morphMap([ 'user' => 'EloquentTestUser', 'post' => 'EloquentTestPost', - ], false); + ]); $user = EloquentTestUser::create(['email' => 'taylorotwell@gmail.com']); $user->photos()->create(['name' => 'Avatar 1']); @@ -375,10 +376,10 @@ public function testMorphMapIsUsedForCreatingAndFetchingThroughRelation() public function testMorphMapIsUsedWhenFetchingParent() { - Illuminate\Database\Eloquent\Relations\Relation::morphMap([ + Relation::morphMap([ 'user' => 'EloquentTestUser', 'post' => 'EloquentTestPost', - ], false); + ]); $user = EloquentTestUser::create(['email' => 'taylorotwell@gmail.com']); $user->photos()->create(['name' => 'Avatar 1']); @@ -388,6 +389,36 @@ public function testMorphMapIsUsedWhenFetchingParent() $this->assertInstanceOf('EloquentTestUser', $photo->imageable); } + public function testMorphMapIsMergedByDefault() + { + $map1 = [ + 'user' => 'EloquentTestUser', + ]; + $map2 = [ + 'post' => 'EloquentTestPost', + ]; + + Relation::morphMap($map1); + Relation::morphMap($map2); + + $this->assertEquals(array_merge($map1, $map2), Relation::morphMap()); + } + + public function testMorphMapOverwritesCurrentMap() + { + $map1 = [ + 'user' => 'EloquentTestUser', + ]; + $map2 = [ + 'post' => 'EloquentTestPost', + ]; + + Relation::morphMap($map1, false); + $this->assertEquals($map1, Relation::morphMap()); + Relation::morphMap($map2, false); + $this->assertEquals($map2, Relation::morphMap()); + } + public function testEmptyMorphToRelationship() { $photo = EloquentTestPhoto::create(['name' => 'Avatar 1']);