Dapatkan contoh subtipe model dengan Eloquent

22

Saya punya Animalmodel, berdasarkan animaltabel.

Tabel ini berisi typebidang, yang bisa berisi nilai-nilai seperti kucing atau anjing .

Saya ingin dapat membuat objek seperti:

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

Namun, bisa menjemput binatang seperti ini:

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

Tetapi di mana $animalakan menjadi contoh Dogatau Cattergantung pada typebidang, bahwa saya dapat memeriksa menggunakan instance ofatau yang akan bekerja dengan metode tipe mengisyaratkan. Alasannya adalah bahwa 90% dari kode dibagikan, tetapi satu dapat menggonggong, dan yang lainnya dapat mengeong.

Saya tahu bahwa saya dapat melakukannya Dog::find($id), tetapi bukan itu yang saya inginkan: Saya dapat menentukan jenis objek hanya setelah diambil. Saya juga bisa mengambil Hewan, dan kemudian berjalan find()di objek yang tepat, tetapi ini melakukan dua panggilan basis data, yang jelas saya tidak mau.

Saya mencoba mencari cara untuk "secara manual" instantiate model Eloquent seperti Dog from Animal, tetapi saya tidak dapat menemukan metode yang sesuai. Adakah ide atau metode yang saya lewatkan?

ClmentM
sumber
@ B001 ᛦ Tentu saja, kelas Anjing atau Kucing saya akan memiliki antarmuka yang sesuai, saya tidak melihat bagaimana ini membantu di sini?
ClmentM
@ClmentM Sepertinya satu ke banyak hubungan polimorfik laravel.com/docs/6.x/…
vivek_23
@ vivek_23 Tidak juga, dalam hal ini membantu memfilter komentar dari jenis tertentu, tetapi Anda sudah tahu bahwa Anda ingin komentar pada akhirnya. Tidak berlaku di sini.
ClmentM
@ClmentM, saya rasa tidak. Hewan dapat berupa Kucing atau Anjing. Jadi, ketika Anda mengambil jenis hewan dari tabel hewan, itu akan memberi Anda contoh Dog atau Cat tergantung pada apa yang disimpan dalam database. Baris terakhir di sana mengatakan Relasi yang dapat dikomentari pada model Komentar akan mengembalikan instance Post atau Video, tergantung pada jenis model mana yang memiliki komentar.
vivek_23
@ vivek_23 Saya menyelam lebih dalam ke dalam dokumentasi dan mencobanya, tetapi Eloquent didasarkan pada kolom aktual dengan *_typenama untuk menentukan model subtipe. Dalam kasus saya, saya benar-benar hanya memiliki satu meja, jadi sementara itu fitur yang bagus, tidak dalam kasus saya.
ClmentM

Jawaban:

7

Anda dapat menggunakan Hubungan Polimorfik di Laravel seperti yang dijelaskan dalam Dokumen Laravel Resmi . Inilah cara Anda dapat melakukannya.

Tentukan hubungan dalam model seperti yang diberikan

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');
    }
}

Di sini Anda akan membutuhkan dua kolom dalam animalstabel, yang pertama adalah animable_typedan yang lainnya adalah animable_iduntuk menentukan jenis model yang dilampirkan pada saat runtime.

Anda dapat mengambil model Anjing atau Kucing seperti yang diberikan,

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

Setelah itu, Anda dapat memeriksa kelas $animobjek dengan menggunakan instanceof.

Pendekatan ini akan membantu Anda untuk ekspansi di masa depan jika Anda menambahkan jenis hewan lain (yaitu rubah atau singa) dalam aplikasi. Ini akan bekerja tanpa mengubah basis kode Anda. Ini adalah cara yang benar untuk mencapai kebutuhan Anda. Namun, tidak ada pendekatan alternatif untuk mencapai polimorfisme dan bersemangat memuat bersama tanpa menggunakan hubungan polimorfik. Jika Anda tidak menggunakan hubungan Polymorphic , Anda akan berakhir dengan lebih dari satu panggilan basis data. Namun, jika Anda memiliki satu kolom yang membedakan jenis modal, mungkin Anda memiliki skema terstruktur yang salah. Saya sarankan Anda meningkatkannya jika Anda ingin menyederhanakannya untuk pengembangan di masa depan juga.

Menulis ulang internal model newInstance()dan newFromBuilder()bukan cara yang baik / disarankan dan Anda harus memperbaikinya setelah Anda mendapatkan pembaruan dari framework.

Kiran Maniya
sumber
1
Dalam komentar dari pertanyaan yang dia katakan, bahwa dia hanya memiliki satu tabel dan fitur polimorfik tidak dapat digunakan dalam kasus OP.
shock_gone_wild
3
Saya hanya menyatakan, seperti apa skenario yang diberikan. Saya pribadi juga akan menggunakan Hubungan Polymorphic;)
shock_gone_wild
1
@KiranManiya terima kasih atas jawaban terperinci Anda. Saya tertarik pada lebih banyak latar belakang. Bisakah Anda menguraikan mengapa (1) model database kuesioner salah dan (2) memperluas fungsi anggota publik / terlindungi tidak baik / disarankan?
Christoph Kluge
1
@ChristophKluge, Anda sudah tahu. (1) Model DB salah dalam konteks pola desain laravel. Jika Anda ingin mengikuti pola desain yang ditentukan oleh laravel, Anda harus memiliki skema DB sesuai dengannya. (2) Ini adalah metode kerangka kerja internal yang Anda sarankan untuk timpa. Saya tidak akan melakukannya jika saya pernah menghadapi masalah ini. Kerangka kerja Laravel memiliki dukungan polimorfisme built-in, jadi mengapa tidak kita gunakan yang menciptakan kembali roda? Anda memberikan petunjuk yang baik dalam jawaban tetapi saya tidak pernah memilih kode dengan kerugian, alih-alih kita dapat mengkode sesuatu yang membantu menyederhanakan ekspansi di masa mendatang.
Kiran Maniya
2
Tapi ... seluruh pertanyaannya bukan tentang pola Desain Laravel. Sekali lagi, kami memiliki skenario yang diberikan (Mungkin database dibuat oleh Aplikasi eksternal). Semua orang akan setuju bahwa polimorfisme akan menjadi cara untuk pergi jika Anda membangun dari awal. Sebenarnya jawaban Anda secara teknis tidak menjawab pertanyaan awal.
shock_gone_wild
5

Saya pikir Anda bisa mengganti newInstancemetode pada Animalmodel, dan memeriksa tipe dari atribut dan kemudian init model yang sesuai.

    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;
    }

Anda juga harus mengganti newFromBuildermetode.


    /**
     * 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;
    }
Chris Neal
sumber
Saya tidak mengerti bagaimana ini bekerja. Animal :: find (1) akan menimbulkan kesalahan: "tipe indeks tidak terdefinisi" jika Anda memanggil Animal :: find (1). Atau apakah saya melewatkan sesuatu?
shock_gone_wild
@shock_gone_wild Apakah Anda memiliki kolom yang bernama typedalam database?
Chris Neal
Ya saya punya. Tetapi array $ attributes kosong jika saya melakukan dd ($ attritubutes). Itu sangat masuk akal. Bagaimana Anda menggunakan ini dalam contoh nyata?
shock_gone_wild
5

Jika Anda benar-benar ingin melakukan ini, Anda bisa menggunakan pendekatan berikut di dalam model Hewan Anda.

<?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);
    }
}
shock_gone_wild
sumber
5

Seperti yang dinyatakan OP dalam komentarnya: Desain basis data sudah ditetapkan dan oleh karena itu Hubungan Polimorfik Laravel tampaknya tidak menjadi pilihan di sini.

Saya suka jawaban dari Chris Neal karena saya harus melakukan sesuatu yang serupa baru-baru ini (menulis Driver Database saya sendiri untuk mendukung Eloquent untuk file dbase / DBF) dan mendapatkan banyak pengalaman dengan internal Laravel's Eloquent ORM.

Saya telah menambahkan cita rasa pribadi saya untuk membuatnya agar kode lebih dinamis dengan tetap menjaga pemetaan eksplisit per model.

Fitur yang didukung yang saya uji dengan cepat:

  • Animal::find(1) berfungsi seperti yang ditanyakan dalam pertanyaan Anda
  • Animal::all() berfungsi juga
  • Animal::where(['type' => 'dog'])->get()akan mengembalikan AnimalDog-objek sebagai koleksi
  • Pemetaan objek dinamis per kelas fasih yang menggunakan sifat ini
  • Fallback ke Animal-model jika tidak ada pemetaan yang dikonfigurasi (atau pemetaan baru muncul di DB)

Kekurangan:

  • Ini menulis ulang model internal newInstance()dan newFromBuilder()seluruhnya (salin dan tempel). Ini berarti jika akan ada pembaruan dari kerangka kerja ke fungsi anggota ini Anda harus mengadopsi kode dengan tangan.

Saya harap ini membantu dan saya siap untuk saran, pertanyaan, dan kasus penggunaan tambahan dalam skenario Anda. Berikut adalah kasus penggunaan dan contoh untuk itu:

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 {}

Dan ini adalah contoh bagaimana itu dapat digunakan dan di bawah hasil masing-masing untuk itu:

$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);

yang menghasilkan sebagai berikut:

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

Dan jika Anda ingin Anda menggunakan di MorphTraitsini tentu saja kode lengkap untuk itu:

<?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);
    }
}
Christoph Kluge
sumber
Hanya karena penasaran. Apakah ini juga berfungsi dengan Animal :: all () Apakah koleksi yang dihasilkan merupakan campuran dari 'Anjing' dan 'Kucing'?
shock_gone_wild
@shock_gone_wild pertanyaan yang cukup bagus! Saya mengujinya secara lokal dan menambahkannya ke jawaban saya. Tampaknya bekerja juga :-)
Christoph Kluge
2
memodifikasi fungsi bawaan laravel bukanlah cara yang benar. Semua perubahan akan hilang setelah kami memperbarui laravel dan itu akan mengacaukan segalanya. Waspadalah.
Navin D. Shah
Hai Navin, terima kasih telah menyebutkan ini, tetapi sudah jelas dinyatakan sebagai kerugian dalam jawaban saya. Pertanyaan balasan: Lalu, bagaimana cara yang benar?
Christoph Kluge
2

Saya rasa saya tahu apa yang Anda cari. Pertimbangkan solusi elegan ini yang menggunakan ruang lingkup permintaan Laravel, lihat https://laravel.com/docs/6.x/eloquent#query-scopes untuk informasi tambahan:

Buat kelas induk yang menyimpan logika bersama:

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

Buat anak (atau beberapa) dengan cakupan kueri global dan savingpengendali acara:

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;
        });
    }
}

(Hal yang sama berlaku untuk kelas lain Cat, cukup ganti konstanta)

Ruang lingkup kueri global bertindak sebagai modifikasi kueri default, sehingga Dogkelas akan selalu mencari catatan dengan type='dog'.

Katakanlah kita memiliki 3 catatan:

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

Sekarang panggilan Dog::find(1)akan menghasilkan null, karena ruang lingkup permintaan default tidak akan menemukan yang id:1mana a Cat. Memanggil Animal::find(1)dan Cat::find(1)keduanya akan berfungsi, meskipun hanya yang terakhir memberi Anda objek Cat yang sebenarnya.

Yang menyenangkan dari pengaturan ini adalah Anda dapat menggunakan kelas-kelas di atas untuk membuat hubungan seperti:

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

Dan relasi ini secara otomatis hanya akan memberi Anda semua hewan dengan type='dog'(dalam bentuk Dogkelas). Lingkup kueri diterapkan secara otomatis.

Selain itu, panggilan Dog::create($properties)akan secara otomatis mengatur typeke 'dog'karena savingkait acara (lihat https://laravel.com/docs/6.x/eloquent#events ).

Perhatikan bahwa panggilan Animal::create($properties)tidak memiliki standar, typejadi di sini Anda perlu mengaturnya secara manual (yang diharapkan).

Api
sumber
0

Meskipun Anda menggunakan Laravel, dalam hal ini, saya pikir Anda tidak harus berpegang pada jalan pintas Laravel.

Masalah yang Anda coba selesaikan adalah masalah klasik yang dipecahkan oleh banyak bahasa / kerangka kerja lainnya menggunakan pola metode Pabrik ( https://en.wikipedia.org/wiki/Factory_method_pattern ).

Jika Anda ingin kode Anda lebih mudah dipahami dan tanpa trik tersembunyi, Anda harus menggunakan pola yang terkenal alih-alih trik tersembunyi / sulap di bawah tenda.

rubens21
sumber
0

Cara termudah adalah dengan membuat metode di kelas Hewan

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;
}

Menyelesaikan model

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

Ini akan mengembalikan instance dari kelas Hewan, Anjing atau Kucing tergantung pada jenis model

Ruben Danielyan
sumber