Eloquent를 사용하여 모델의 하위 유형 인스턴스 가져 오기


22

테이블을 Animal기반으로 모델 이 있습니다 animal.

이 테이블에는 cat 또는 dogtype 같은 값을 포함 할 수 있는 필드가 있습니다 .

다음과 같은 객체를 만들 수 있기를 원합니다.

class Animal extends Model { }
class Dog extends Animal { }
class Cat extends Animal { }

그러나 다음과 같은 동물을 가져올 수 있습니다.

$animal = Animal::find($id);

그러나 필드 $animal의 인스턴스 Dog또는 필드 Cat에 따라 어디에서 type사용할 수 있는지 instance of또는 유형 힌트 메소드에서 작동 하는지 확인할 수 있습니다 . 그 이유는 코드의 90 %가 공유되지만 하나는 짖을 수 있고 다른 하나는 야옹을 할 수 있기 때문입니다.

내가 할 수 있다는 것을 알고 Dog::find($id)있지만 원하는 것은 아닙니다. 한 번 가져온 객체의 유형을 결정할 수 있습니다. 또한 Animal을 가져 와서 find()올바른 객체에서 실행할 수는 있지만 분명히 원하지 않는 두 개의 데이터베이스 호출을 수행하고 있습니다.

Animal의 Dog와 같은 Eloquent 모델을 "수동으로"인스턴스화하는 방법을 찾으려고했지만 해당하는 방법을 찾을 수 없었습니다. 내가 놓친 아이디어 나 방법은 무엇입니까?


@ B001 ᛦ 물론 내 Dog 또는 Cat 클래스에 해당 인터페이스가있을 것입니다. 여기서 어떻게 도움이되는지 보지 못합니까?
ClmentM

@ClmentM 일대 다 다형성 관계 laravel.com/docs/6.x/…
vivek_23

@ vivek_23 실제로이 경우에는 주어진 유형의 주석을 필터링하는 데 도움이되지만 이미 주석을 원한다는 것을 이미 알고 있습니다. 여기에는 적용되지 않습니다.
ClmentM

@ ClmentM 나는 그렇게 생각합니다. 동물은 고양이 또는 개일 수 있습니다. 따라서 동물 테이블에서 동물 유형을 검색하면 데이터베이스에 저장된 내용에 따라 Dog 또는 Cat 인스턴스가 제공됩니다. 마지막 줄 에 주석 모델에 대한 주석 가능한 관계는 주석을 소유 한 모델 유형에 따라 Post 또는 Video 인스턴스를 반환합니다.
vivek_23

@ vivek_23 설명서에 더 들어가서 시도해 보았지만 Eloquent는 *_type이름이있는 실제 열을 기반으로 하위 유형 모델을 결정합니다. 내 경우에는 실제로 하나의 테이블 만 있으므로 내 경우가 아닌 멋진 기능입니다.
ClmentM

답변:


7

공식 라 라벨 문서에 설명 된대로 라 라벨에서 다형성 관계를 사용할 수 있습니다 . 그 방법은 다음과 같습니다.

주어진 모델의 관계를 정의

class Animal extends Model{
    public function animable(){
        return $this->morphTo();
    }
}

class Dog extends Model{
    public function animal(){
        return $this->morphOne('App\Animal', 'animable');
    }
}

class Cat extends Model{
    public function animal(){
        return $this->morphOne('App\Animal', 'animable');
    }
}

여기에는 animals테이블에 두 개의 열이 필요 합니다. 첫 번째는 animable_type다른 하나는 animable_id런타임에 연결된 모델 유형을 결정하는 것입니다.

주어진 개 또는 고양이 모델을 가져올 수 있습니다.

$animal = Animal::find($id);
$anim = $animal->animable; //this will return either Cat or Dog Model

그런 다음을 $anim사용하여 객체의 클래스를 확인할 수 있습니다 instanceof.

이 접근 방식은 응용 프로그램에 다른 동물 유형 (여우 또는 사자)을 추가 할 경우 향후 확장에 도움이됩니다. 코드베이스를 변경하지 않고도 작동합니다. 이것이 귀하의 요구 사항을 충족시키는 올바른 방법입니다. 그러나, 다형성 관계를 사용하지 않고 다형성을 달성하고 열망하는 로딩을위한 대안적인 접근법은 없다. 다형성 관계를 사용하지 않으면 하나 이상의 데이터베이스 호출로 끝납니다. 그러나 모달 유형을 구분하는 단일 열이있는 경우 구조화 된 스키마가 잘못되었을 수 있습니다. 향후 개발을 위해 단순화하려는 경우 개선하는 것이 좋습니다.

모델의 내부 재 작성 newInstance()하고하는 것은 newFromBuilder()좋은 / 추천 방법이 아니다 당신은 당신이 프레임 워크에서 업데이트를 얻을 수 있습니다 일단에 재 작업해야합니다.


1
그 질문에 대한 의견에서 그는 단 하나의 테이블 만 가지고 있으며 OP의 경우에는 다형성 기능을 사용할 수 없다고 말했습니다.
shock_gone_wild

3
주어진 시나리오가 무엇인지를 말하고 있습니다. 나는 개인적으로 다형성 관계도 사용할 것입니다;)
shock_gone_wild

1
@KiranManiya 자세한 답변 주셔서 감사합니다. 더 많은 배경에 관심이 있습니다. (1) 질문자 데이터베이스 모델이 잘못되었고 (2) 공개 / 보호 멤버 기능을 확장하는 것이 좋지 않거나 권장되지 않는 이유를 자세히 설명 할 수 있습니까?
Christoph Kluge

1
@ChristophKluge, 당신은 이미 알고 있습니다. (1) 라 라벨 디자인 패턴의 맥락에서 DB 모델이 잘못되었습니다. laravel에서 정의한 디자인 패턴을 따르려면 그에 따라 DB 스키마가 있어야합니다. (2) 재정의하도록 제안한 프레임 워크 내부 방법입니다. 이 문제에 직면하면 그렇게하지 않습니다. 라 라벨 프레임 워크에는 다형성 지원 기능이 내장되어있어 바퀴를 재발 명하는 데 사용하지 않겠습니까? 당신은 대답에 대한 좋은 단서를 주었지만 단점이있는 코드를 선호하지는 않습니다. 대신 확장을 단순화하는 데 도움이되는 코드를 작성할 수 있습니다.
Kiran Maniya

2
그러나 ... 전체 질문은 라 라벨 디자인 패턴에 관한 것이 아닙니다. 다시, 우리는 주어진 시나리오를 가지고 있습니다 (아마도 데이터베이스는 외부 응용 프로그램에 의해 생성됩니다). 처음부터 다형성하면 다형성이 좋은 방법이 될 것입니다. 실제로 귀하의 답변은 원래의 질문에 기술적으로 답변하지 않습니다.
shock_gone_wild

5

모델 의 newInstance메소드를 재정의 Animal하고 속성에서 유형을 확인한 다음 해당 모델을 초기화 할 수 있다고 생각합니다 .

    public function newInstance($attributes = [], $exists = false)
    {
        // This method just provides a convenient way for us to generate fresh model
        // instances of this current model. It is particularly useful during the
        // hydration of new objects via the Eloquent query builder instances.
        $modelName = ucfirst($attributes['type']);
        $model = new $modelName((array) $attributes);

        $model->exists = $exists;

        $model->setConnection(
            $this->getConnectionName()
        );

        $model->setTable($this->getTable());

        $model->mergeCasts($this->casts);

        return $model;
    }

또한 newFromBuilder메소드 를 대체해야합니다 .


    /**
     * Create a new model instance that is existing.
     *
     * @param  array  $attributes
     * @param  string|null  $connection
     * @return static
     */
    public function newFromBuilder($attributes = [], $connection = null)
    {
        $model = $this->newInstance([
            'type' => $attributes['type']
        ], true);

        $model->setRawAttributes((array) $attributes, true);

        $model->setConnection($connection ?: $this->getConnectionName());

        $model->fireModelEvent('retrieved', false);

        return $model;
    }

나는 이것이 어떻게 작동하는지 알지 못한다. Animal :: find (1)을 호출하면 Animal :: find (1)에서 "정의되지 않은 인덱스 유형"오류가 발생합니다. 아니면 뭔가 빠졌습니까?
shock_gone_wild

@shock_gone_wild type데이터베이스에 이름이 지정된 열이 있습니까?
크리스 닐

네, 있어요. 그러나 dd ($ attritubutes)를 수행하면 $ attributes 배열이 비어 있습니다. 완벽하게 이해되는 것입니다. 실제 예에서 이것을 어떻게 사용합니까?
shock_gone_wild

5

이 작업을 실제로하려면 Animal 모델 내부에서 다음 방법을 사용할 수 있습니다.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Animal extends Model
{

    // other code in animal model .... 

    public static function __callStatic($method, $parameters)
    {
        if ($method == 'find') {
            $model = parent::find($parameters[0]);

            if ($model) {
                switch ($model->type) {
                    case 'dog':
                        return new \App\Dog($model->attributes);
                    case 'cat':
                        return new \App\Cat($model->attributes);
                }
                return $model;
            }
        }

        return parent::__callStatic($method, $parameters);
    }
}

5

OP가 그의 의견에 언급했듯이 : 데이터베이스 디자인은 이미 설정되어 있으므로 Laravel의 다형성 관계 는 여기서 옵션이 아닌 것 같습니다.

나는 Chris Neal의 대답을 좋아한다 내가 최근 비슷한 일을했기 때문에 (디베이스 / DBF 파일을 웅변을 지원하기 위해 내 자신의 데이터베이스 드라이버를 작성) 및 Laravel의 설득력 ORM의 내부에 많은 경험을 얻었다.

모델마다 명시 적으로 매핑을 유지하면서 코드를보다 역동적으로 만들기 위해 개인 취향을 추가했습니다.

내가 신속하게 테스트 한 지원 기능 :

  • Animal::find(1) 귀하의 질문에 따라 작동
  • Animal::all() 잘 작동합니다
  • Animal::where(['type' => 'dog'])->get()- AnimalDog객체를 컬렉션으로 반환합니다
  • 이 특성을 사용하는 웅변 클래스 별 동적 객체 매핑
  • Animal매핑이 구성되지 않은 경우 -model로 폴백 (또는 DB에 새 매핑이 표시됨)

단점 :

  • 모델의 내부 newInstance()newFromBuilder()전체를 다시 작성합니다 (복사 및 붙여 넣기). 즉, 프레임 워크에서이 멤버 함수로 업데이트되는 경우 직접 코드를 채택해야합니다.

도움이 되길 바랍니다. 시나리오에 대한 제안, 질문 및 추가 사용 사례가 있습니다. 유스 케이스와 예제는 다음과 같습니다.

class Animal extends Model
{
    use MorphTrait; // You'll find the trait in the very end of this answer

    protected $morphKey = 'type'; // This is your column inside the database
    protected $morphMap = [ // This is the value-to-class mapping
        'dog' => AnimalDog::class,
        'cat' => AnimalCat::class,
    ];

}

class AnimalCat extends Animal {}
class AnimalDog extends Animal {}

그리고 이것이 어떻게 사용될 수 있고 각각의 결과 아래에있는 예입니다 :

$cat = Animal::find(1);
$dog = Animal::find(2);
$new = Animal::find(3);
$all = Animal::all();

echo sprintf('ID: %s - Type: %s - Class: %s - Data: %s', $cat->id, $cat->type, get_class($cat), $cat, json_encode($cat->toArray())) . PHP_EOL;
echo sprintf('ID: %s - Type: %s - Class: %s - Data: %s', $dog->id, $dog->type, get_class($dog), $dog, json_encode($dog->toArray())) . PHP_EOL;
echo sprintf('ID: %s - Type: %s - Class: %s - Data: %s', $new->id, $new->type, get_class($new), $new, json_encode($new->toArray())) . PHP_EOL;

dd($all);

결과는 다음과 같습니다.

ID: 1 - Type: cat - Class: App\AnimalCat - Data: {"id":1,"type":"cat"}
ID: 2 - Type: dog - Class: App\AnimalDog - Data: {"id":2,"type":"dog"}
ID: 3 - Type: new-animal - Class: App\Animal - Data: {"id":3,"type":"new-animal"}

// Illuminate\Database\Eloquent\Collection {#1418
//  #items: array:2 [
//    0 => App\AnimalCat {#1419
//    1 => App\AnimalDog {#1422
//    2 => App\Animal {#1425

그리고 당신이 원한다면 MorphTrait여기에 전체 코드가 있습니다 :

<?php namespace App;

trait MorphTrait
{

    public function newInstance($attributes = [], $exists = false)
    {
        // This method just provides a convenient way for us to generate fresh model
        // instances of this current model. It is particularly useful during the
        // hydration of new objects via the Eloquent query builder instances.
        if (isset($attributes['force_class_morph'])) {
            $class = $attributes['force_class_morph'];
            $model = new $class((array)$attributes);
        } else {
            $model = new static((array)$attributes);
        }

        $model->exists = $exists;

        $model->setConnection(
            $this->getConnectionName()
        );

        $model->setTable($this->getTable());

        return $model;
    }

    /**
     * Create a new model instance that is existing.
     *
     * @param array $attributes
     * @param string|null $connection
     * @return static
     */
    public function newFromBuilder($attributes = [], $connection = null)
    {
        $newInstance = [];
        if ($this->isValidMorphConfiguration($attributes)) {
            $newInstance = [
                'force_class_morph' => $this->morphMap[$attributes->{$this->morphKey}],
            ];
        }

        $model = $this->newInstance($newInstance, true);

        $model->setRawAttributes((array)$attributes, true);

        $model->setConnection($connection ?: $this->getConnectionName());

        $model->fireModelEvent('retrieved', false);

        return $model;
    }

    private function isValidMorphConfiguration($attributes): bool
    {
        if (!isset($this->morphKey) || empty($this->morphMap)) {
            return false;
        }

        if (!array_key_exists($this->morphKey, (array)$attributes)) {
            return false;
        }

        return array_key_exists($attributes->{$this->morphKey}, $this->morphMap);
    }
}

그냥 호기심. 이것은 Animal :: all ()에서도 작동합니까? 결과 모음은 'Dogs'와 'Cats'가 혼합되어 있습니까?
shock_gone_wild

@shock_gone_wild 꽤 좋은 질문입니다! 로컬로 테스트하여 답변에 추가했습니다. 잘 작동하는 것 같습니다 :-)
Christoph Kluge

2
라 라벨의 내장 함수를 수정하는 것은 올바른 방법이 아닙니다. 라 라벨을 업데이트하면 모든 변경 사항이 풀리고 모든 것이 엉망이됩니다. 알아 두세요
Navin D. Shah

Navin, 이것에 대해 언급 해 주셔서 감사하지만 이미 내 대답에 단점으로 명시되어 있습니다. 반대 질문 : 그렇다면 올바른 방법은 무엇입니까?
Christoph Kluge

2

나는 당신이 찾고있는 것을 알고 있다고 생각합니다. Laravel 쿼리 범위를 사용하는이 우아한 솔루션을 고려하십시오. 자세한 내용은 https://laravel.com/docs/6.x/eloquent#query-scopes 를 참조 하십시오 .

공유 로직을 보유하는 상위 클래스를 작성하십시오.

class Animal extends \Illuminate\Database\Eloquent\Model
{
    const TYPE_DOG = 'dog';
    const TYPE_CAT = 'cat';
}

글로벌 쿼리 범위와 saving이벤트 핸들러를 사용 하여 하위 (또는 다중)를 작성하십시오 .

class Dog extends Animal
{
    public static function boot()
    {
        parent::boot();

        static::addGlobalScope('type', function(\Illuminate\Database\Eloquent\Builder $builder) {
            $builder->where('type', self::TYPE_DOG);
        });

        // Add a listener for when saving models of this type, so that the `type`
        // is always set correctly.
        static::saving(function(Dog $model) {
            $model->type = self::TYPE_DOG;
        });
    }
}

(다른 클래스에도 동일 Cat하게 상수를 대체하십시오)

전역 쿼리 범위는 기본 쿼리 수정으로 작동하여 Dog클래스에서 항상로 레코드를 찾습니다 type='dog'.

3 개의 레코드가 있다고 가정 해 봅시다.

- id:1 => Cat
- id:2 => Dog
- id:3 => Mouse

이제 호출 Dog::find(1)초래 null기본 쿼리의 범위는 찾을 수 없기 때문에, id:1이다 Cat. 마지막 하나만 실제 Cat 객체를 제공하지만 호출 Animal::find(1)Cat::find(1)둘 다 작동합니다.

이 설정의 좋은 점은 위의 클래스를 사용하여 다음과 같은 관계를 만들 수 있다는 것입니다.

class Owner
{
    public function dogs()
    {
        return $this->hasMany(Dog::class);
    }
}

그리고이 관계는 자동으로 모든 동물에게 type='dog'(의 형태로Dog 클래스 ) 입니다. 쿼리 범위가 자동으로 적용됩니다.

또한 호출 Dog::create($properties)은 이벤트 후크 type'dog'인해를로 자동 설정합니다 saving( https://laravel.com/docs/6.x/eloquent#events 참조 ).

호출 Animal::create($properties)에는 기본값이 type없으므로 여기서 수동으로 설정해야합니다 (예상).


0

라 라벨을 사용하고 있지만이 경우 라 라벨 단축키를 고수해서는 안된다고 생각합니다.

해결하려는이 문제는 많은 다른 언어 / 프레임 워크가 Factory 메소드 패턴 ( https://en.wikipedia.org/wiki/Factory_method_pattern )을 사용하여 해결하는 고전적인 문제입니다 .

코드를 이해하기 쉽게하고 숨겨진 트릭을 사용하지 않으려면 후드 아래의 숨겨진 / 마술 트릭 대신 잘 알려진 패턴을 사용해야합니다.


0

가장 쉬운 방법은 Animal 클래스에서 메소드를 만드는 것입니다

public function resolve()
{
    $model = $this;
    if ($this->type == 'dog'){
        $model = new Dog();
    }else if ($this->type == 'cat'){
        $model = new Cat();
    }
    $model->setRawAttributes($this->getAttributes(), true);
    return $model;
}

해석 모델

$animal = Animal::first()->resolve();

모델 유형에 따라 Animal, Dog 또는 Cat 클래스의 인스턴스를 반환합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.