테이블을 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 모델을 “수동으로”인스턴스화하는 방법을 찾으려고했지만 해당하는 방법을 찾을 수 없었습니다. 내가 놓친 아이디어 나 방법은 무엇입니까?
답변
공식 라 라벨 문서에 설명 된대로 라 라벨에서 다형성 관계를 사용할 수 있습니다 . 그 방법은 다음과 같습니다.
주어진 모델의 관계를 정의
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()
좋은 / 추천 방법이 아니다 당신은 당신이 프레임 워크에서 업데이트를 얻을 수 있습니다 일단에 재 작업해야합니다.
답변
모델 의 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 모델 내부에서 다음 방법을 사용할 수 있습니다.
<?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);
}
}
답변
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);
}
}
답변
나는 당신이 찾고있는 것을 알고 있다고 생각합니다. 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
없으므로 여기서 수동으로 설정해야합니다 (예상).
답변
라 라벨을 사용하고 있지만이 경우 라 라벨 단축키를 고수해서는 안된다고 생각합니다.
해결하려는이 문제는 많은 다른 언어 / 프레임 워크가 Factory 메소드 패턴 ( https://en.wikipedia.org/wiki/Factory_method_pattern )을 사용하여 해결하는 고전적인 문제입니다 .
코드를 이해하기 쉽게하고 숨겨진 트릭을 사용하지 않으려면 후드 아래의 숨겨진 / 마술 트릭 대신 잘 알려진 패턴을 사용해야합니다.
답변
가장 쉬운 방법은 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 클래스의 인스턴스를 반환합니다.