Eloquentを使用してモデルのサブタイプのインスタンスを取得する


22

Animalに基づいたモデルがありanimalます。

このテーブルには、catdogtypeなどの値を含むことができるフィールドが含まれています。

次のようなオブジェクトを作成できるようにしたいと思います。

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)、それは私が望んでいることではありません。フェッチされた後でのみ、オブジェクトのタイプを判別できます。動物をフェッチfind()して適切なオブジェクトで実行することもできますが、これは2つのデータベース呼び出しを実行しているため、明らかに不要です。

Dog from AnimalのようなEloquentモデルを「手動で」インスタンス化する方法を探しましたが、対応するメソッドが見つかりませんでした。見逃したアイデアや方法を教えてください。


@ B001ᛦもちろん、私の犬または猫のクラスには対応するインターフェースがあり、ここでそれがどのように役立つかわかりませんか?
ClmentM

多くの多型の関係の1つのような@ClmentMのルックスlaravel.com/docs/6.x/...
vivek_23

@ vivek_23実際には、この場合、特定のタイプのコメントをフィルタリングするのに役立ちますが、最後にコメントが必要であることはすでにわかっています。ここでは適用されません。
ClmentM

@ClmentMそうだと思います。動物は猫または犬のいずれかです。したがって、動物のテーブルから動物の種類を取得すると、データベースに何が格納されているかに応じて、犬または猫のインスタンスが提供されます。最後の行には、CommentモデルのCommentableリレーションが、コメントを所有するモデルのタイプに応じて、PostまたはVideoインスタンスのいずれかが返される
vivek_23

@ vivek_23ドキュメントを詳しく調べて試してみましたが、Eloquentは*_type名前付きの実際の列に基づいてサブタイプモデルを決定しています。私の場合、実際にはテーブルが1つしかないため、これは素晴らしい機能ですが、私の場合はそうではありません。
ClmentM

回答:


7

Laravelの公式ドキュメントで説明されているように、Laravelで多態関係を使用できます。これを行う方法を次に示します。

与えられたようにモデルの関係を定義する

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テーブルに2つの列が必要です。最初の列はanimable_typeと別のあるanimable_id、実行時にそれに接続されているモデルのタイプを決定します。

指定されたとおりにDogまたはCatモデルをフェッチできます。

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

その後、あなたはチェックすることができます $animオブジェクトのクラスをinstanceof

このアプローチは、アプリケーションに別の種類の動物(キツネやライオン)を追加した場合に、将来の拡張に役立ちます。コードベースを変更しなくても機能します。これは、要件を達成するための正しい方法です。ただし、ポリモーフィズムの関係を使用せずに、ポリモーフィズムと熱心なロードを同時に実現するための代替アプローチはありません。使用しない場合ポリモーフィックリレーションシップを結果として複数のデータベース呼び出しが発生します。ただし、モーダルタイプを区別する単一の列がある場合は、構造化スキーマが間違っている可能性があります。今後の開発のために単純化したい場合は、改善することをお勧めします。

モデルの内部newInstance()を書き直すnewFromBuilder()ことは良い/推奨されない方法であり、フレームワークから更新を取得したら、モデルでやり直す必要があります。


1
彼は質問のコメントで、彼はテーブルが1つしかなく、OPの場合、多態性の特徴は使用できないと述べました。
shock_gone_wild

3
私はただ、与えられたシナリオがどのようなものかを述べています。個人的には、ポリモーフィック関係も使用します;)
shock_gone_wild

1
@きらんまにゃ詳しい回答ありがとうございます。もっと背景に興味があります。(1)質問者データベースモデルが間違っている、(2)パブリック/保護されたメンバー関数の拡張が適切ではない、または推奨されない理由を詳しく説明できますか?
Christoph Kluge

1
@ChristophKluge、あなたはすでに知っています。(1)DBモデルがlaravel設計パターンのコンテキストで間違っています。laravelで定義された設計パターンに従う場合は、それに応じたDBスキーマが必要です。(2)これは、オーバーライドすることを提案したフレームワークの内部メソッドです。この問題に直面した場合、私はそれをしません。Laravelフレームワークには組み込みのポリモーフィズムサポートがあるので、それを使用して、ホイールを再発明するのではないでしょうか。あなたは答えの良い手がかりを与えましたが、将来の拡張を単純化するのに役立つ何かをコーディングできる代わりに、私は不利な点のあるコードを好んだことはありません。
キランマニヤ

2
しかし...問題全体は、Laravelデザインパターンに関するものではありません。ここでも、所定のシナリオがあります(おそらく、データベースは外部アプリケーションによって作成されます)。ゼロから構築する場合は、ポリモーフィズムを使用する方法に誰もが同意するでしょう。実際、あなたの答えは技術的に元の質問には答えません。
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)は「undefined index type」というエラーをスローします。それとも何か不足していますか?
shock_gone_wild

@shock_gone_wild typeデータベースに名前が付けられた列がありますか?
クリスニール

はい、持っています。しかし、dd($ attritubutes)を実行すると、$ attributes配列は空になります。これは完全に理にかなっています。実際の例でこれをどのように使用しますか?
shock_gone_wild

5

これを本当に実行したい場合は、動物モデル内で次のアプローチを使用できます。

<?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の多態的な関係はここではオプションではないようです。

クリス・ニールの答えが好きは最近同様のことをしなければならず(dbase / DBFファイル用のEloquentをサポートするために独自のデータベースドライバーを作成する必要があり)、LaravelのEloquent ORMの内部で多くの経験を得たのでです。

モデルごとに明示的なマッピングを維持しながらコードをより動的にするために、それに個人的なフレーバーを追加しました。

私がすぐにテストしたサポートされている機能:

  • Animal::find(1) あなたの質問で尋ねられたように機能します
  • Animal::all() うまくいく
  • Animal::where(['type' => 'dog'])->get() 戻ります AnimalDog-objectsをコレクションとしてます
  • この特性を使用する雄弁語クラスごとの動的オブジェクトマッピング
  • Animalマッピングが構成されていない場合(または新しいマッピングがDB に表示された場合)の-model へのフォールバック

短所:

  • これは、モデルの内部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()でも機能しますか?結果のコレクションは「犬」と「猫」の混合ですか?
shock_gone_wild

@shock_gone_wildかなり良い質問です!私はそれをローカルでテストし、私の回答に追加しました。同様に動作するようです:-)
クリストフクルージ

2
laravelの組み込み関数を変更するのは正しい方法ではありません。laravelを更新すると、すべての変更が失われ、すべてが台無しになります。注意してください。
Navin D. Shah

ヘイナヴィン、これについて言及してくれてありがとう 反対の質問:正しい方法は何ですか?
クリストフ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

デフォルトのクエリスコープはwhichがを見つけられないため、呼び出しDog::find(1)は結果としてnullになります。を呼び出すと、両方とも機能しますが、最後の1つだけが実際のCatオブジェクトを提供します。id:1CatAnimal::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

Laravelを使用していますが、この場合はLaravelのショートカットに固執しないでください。

あなたが解決しようとしているこの問題は、他の多くの言語/フレームワークが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.