다형성 관계는 모델이 하나의 모델과 관련되지 않고 여러 개의 모델과 관련될 수 있는 관계로 관계형 DBMS 의 SQL 로는 표현할 수 없는 관계입니다.


예를 들어 아래와 같이 picture, user, product 세 개의 모델이 있을 경우 picture 모델은 user 모델에 속할 수도 product 모델에 속할 수도 있습니다.

즉 사진을 저장하고 있는 테이블은 사용자의 프로필 사진일 수도 있고 판매하는 제품의 사진일 수도 있습니다. 

이때 User, Product 모델은 imageable 메소드를 사용하여 소유하고 있는 Picture 모델에 접근할 수 있습니다.

위 관계도에서 가장 중요한 필드는 필드는 pictures 테이블의 imageable_id 와 imageable_type 입니다.

imageable_id 는 user 또는 product 의 id 를 나타내며 imageable_type 은 picture 모델을 소유하고 있는 모델의 클래스 이름을 나타내며 imageable 관계를 엑세스 할 때 Eloquent ORM 이 어떤 타입의 소유 모델을 반환해야 하는지 알려줍니다.


그러면 위와 같은 다형성 관계를 직접 구현하기 위해 migration 을 생성해 봅시다.

$ php artisan make:migration create_product_table --create=products
$ php artisan make:migration create_picture_table --create=pictures
CODE

products migration 을 열어서 테이블에 price 컬럼을 추가해 주고 저장합니다. 

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->increments('id');
		$table->string('name');
        $table->integer('price');
        $table->timestamps();
    });
}
CODE

이제 picture migration 파일은 다형성 관계를 맺어주는 두 개의 컬럼(imageable_id, imageable_type)을 정의해 줍니다.

public function up()
{
    Schema::create('pictures', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title', 100);
        $table->string('path', 200);
        $table->integer('imageable_id');
        $table->string('imageable_type', 50);
        $table->timestamps();
    });
}
CODE

migration 을 실행하고 모델 클래스를 생성합니다.

$ php artisan migrate
$ php artisan make:model Picture
$ php artisan make:model Product
CODE


다형성을 갖는 모델인 Picture는 다형성에 접근하는 메소드에 morphTo($name = null, $type = null, $id = null) 메소드를 사용하여 다형성을 선언해 줍니다.

class Picture  extends Model
{
    /**
     * picture 의 모든 소유자 모델 리턴
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}
CODE


이제 다형성 모델에 접근하는 모델은 morphMany($related, $name, $type = null, $id = null, $localKey = null) 를 사용하여 다형성 모델 클래스와 접근자를 지정할 수 있습니다.

class User extends Model
{
    /**
     * Get all of the User member's photos.
     */
    public function pictures()
    {
        return $this->morphMany(Picture::class, 'imageable');
    }
}
CODE

Product 모델에도 똑같이 pictures() 메소드에 morphMany() 를 사용하여 다형성 모델에 접근할 수 있게 만들어 줍니다.

class Product extends Model
{
    /**
     * Get all of the product's photos.
     */
    public function pictures()
    {
        return $this->morphMany(Picture::class, 'imageable');
    }
}
CODE

이제 모델 간의 관계 설정이 완료되었고 테스트를 위해 모델 팩토리 사용하여 테스트 데이타를 입력해 보겠습니다.

database/factories/ModelFactory.php 를 열어서 다음과 같이 Product 와 Picture모델 팩토리를 추가해 줍니다.

$factory->define(App\Product::class, function ($faker) {
   
    return [
        'name' => $faker->sentence,
        'price' => $faker->randomNumber(5),   
        'created_at' => $faker->dateTimeBetween($startDate = '-2 years', $endDate = '-1 years'),
        'updated_at' => $faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'),
    ];
});
CODE

이제 Picture 팩토리를 추가해 줍니다..

$factory->define(App\Picture::class, function ($faker) {
   
 if (rand(0,1) == 0) :
        $type = 'App\User';
        $min = App\User::min('id');
        $max = App\User::max('id');
   else :
        $type = 'App\Product';
        $min = App\Product::min('id');
        $max = App\Product::max('id');
   endif;
   
   return [
        'title' => $faker->text,
        'path' => $faker->image($dir = 'storage', $width = 640, $height = 480) ,
        'imageable_id' => $faker->numberBetween($min, $max),   
        'imageable_type' =>  $type,
        'created_at' => $faker->dateTimeBetween($startDate = '-2 years', $endDate = '-1 years'),
        'updated_at' => $faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'),
    ];
});
CODE
  • if (rand(0, 1) == 0) : Picture 모델은 User 또는 Product 모델에 속할 수 있습니다. 0 또는 1 을 생성하여 0이면 User 모델에 속하게 하고 1면 Product 모델에 속하게 하기 위한 코드입니다.

  • 'path' => $faker->image($dir = 'storage', $width=640, $height=480) : faker 에는 랜덤하게 이미지를 생성하는 기능이 있으므로 이를 사용하여 이미지를 생성하고 storage 폴더에 저장합니다.

  • 'imageable_id' => $faker->numberBetween($min, $max) : User 혹은 Product 의 모델과 랜덤하게 연결합니다.

  • 'imageable_type' => $type : 위 if 문 결과에 따라 'App\User' 또는 'App\Product' 가 설정됩니다.


이제 모델 팩토리 설정이 완료되었으니 tinker  를 띄운 후에 모델 팩토리를 통해 Product 와 Picture 를 생성합니다.

Picture 모델은 랜덤하게 이미지를 생성해 주는 http://lorempixel.com/  에서 이미지를 받아오므로 시간이 꽤 걸립니다.

>>> factory('App\Product', 20)->create()
>>> factory('App\Picture', 50)->create()
CODE


이제 다형성 모델이 생성되었으므로 tinker 에서 user id 가 4인 모델이 갖고 있는 picture 들을 조회해 보겠습니다.

>>> $pics = App\User::find(4)->pictures
=> <Illuminate\Database\Eloquent\Collection #000000003c208c50000000002cd42b2b> [
       <App\Picture #000000003c208c55000000002cd42b2b> {
           id: 15,
           path: "storage/758f52f09f3225e3f0490115af3da36f.jpg",
           imageable_id: 4,
           imageable_type: "App\\User",
           created_at: "2013-09-26 11:13:36",
           updated_at: "2015-04-23 23:32:26"
       }
   ]
CODE


 product id 가 4인 모델의 모든 이미지도 다음과 같이 간단하게 조회 가능합니다.

>>> $pics = App\Product::find(4)->pictures
CODE


이제 Picture 모델을 통해 해당 picture 의 소유자를 확인하는 작업은 브라우저에서 테스트 할 수 있도록 Orm 컨트롤러에 메소드를 추가합니다.

public function getMorphTo($id)
{
    $pic = Picture::find($id);
    $imageable = $pic->imageable;
    dump($imageable);
}
CODE

이제 브라우저로 http://homestead.app/orm/morph-to/5 에 연결하여 $imageable 이 어떤 객체인지 확인해 보겠습니다. 독자들도 URI 의 id 를 바꿔 가면서 결과를 확인해 보면 

$imageable 객체는 소유자 클래스에 따라 App\User 또는 App\Product 가 되는 것을 확인할 수 있습니다.