diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 1b6f58af97dd..984754600043 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)) { + return array_search($class, $morphMap, true); + } + + 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..f1724866307f 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,22 @@ 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(array $map = null, $merge = true) + { + if (is_array($map)) { + static::$morphMap = $merge ? array_merge(static::$morphMap, $map) : $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..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 { @@ -76,6 +77,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 +343,82 @@ public function testBasicMorphManyRelationship() $this->assertEquals('First Post', $photos[3]->imageable->name); } + public function testMorphMapIsUsedForCreatingAndFetchingThroughRelation() + { + Illuminate\Database\Eloquent\Relations\Relation::morphMap([ + 'user' => 'EloquentTestUser', + 'post' => 'EloquentTestPost', + ]); + + $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() + { + Relation::morphMap([ + 'user' => 'EloquentTestUser', + 'post' => 'EloquentTestPost', + ]); + + $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 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']); 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