Eloquent ile bir modelin alt türünün örneğini alın


22

Masaya Animalgöre bir modelim var animal.

Bu tablo, kedi veya köpektype gibi değerler içerebilen bir alan içerir .

Gibi nesneler oluşturmak mümkün olmak istiyorum:

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

Ancak, böyle bir hayvanı getirebilmek:

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

Ancak, nerede kontrol edebileceğim veya tip ipucu yöntemleriyle çalışacak $animalbir örnek Dogveya alana Catbağlı olarak . Bunun nedeni, kodun% 90'ının paylaşılmasıdır, ancak biri havlayabilir ve diğeri miyavlayabilir.typeinstance of

Yapabileceğimi biliyorum Dog::find($id), ama istediğim şey bu değil: Nesnenin türünü ancak getirildikten sonra belirleyebilirim. Ayrıca Hayvan getirebilir ve daha sonra find()doğru nesne üzerinde çalışabilir , ama bu açıkça istemiyorum iki veritabanı çağrıları yapıyor.

Hayvansal Köpek gibi bir Eloquent modelini "elle" başlatmanın bir yolunu aramaya çalıştım, ancak karşılık gelen herhangi bir yöntem bulamadım. Kaçırdığım herhangi bir fikir veya yöntem lütfen?


@ B001 course Tabii ki, Köpek ya da Kedi sınıfım karşılık gelen arayüzlere sahip olacak, burada nasıl yardımcı olduğunu görmüyorum?
ClmentM

@ClmentM Bire çok polimorfik ilişkiye benziyor laravel.com/docs/6.x/…
vivek_23

@ vivek_23 Pek değil, bu durumda belirli bir türdeki yorumları filtrelemeye yardımcı olur, ancak sonunda yorum istediğinizi zaten biliyorsunuzdur. Burada geçerli değil.
ClmentM

@ClmentM Sanırım öyle. Hayvan Kedi veya Köpek olabilir. Bu nedenle, hayvan türünü hayvan tablosundan aldığınızda, veritabanında nelerin depolandığına bağlı olarak size Köpek veya Kedi örneği verir. Buradaki son satırda , Yorum modelindeki yorumlanabilir ilişki, yorumun hangi modelin türüne bağlı olduğuna bağlı olarak bir Yayın veya Video örneği döndürülür.
vivek_23

@ vivek_23 Belgelere daha fazla daldım ve denedim, ancak Eloquent, *_typealt tip modelini belirlemek için adlı gerçek sütuna dayanıyor . Benim durumumda gerçekten sadece bir masam var, bu yüzden güzel bir özellik olsa da, benim durumumda değil.
ClmentM

Yanıtlar:


7

Laravel'deki Polimorfik İlişkileri Resmi Laravel Dokümanlarında açıklandığı gibi kullanabilirsiniz . Bunu nasıl yapabileceğiniz aşağıda açıklanmıştır.

Modeldeki ilişkileri verilen şekilde tanımlayın

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

Burada animalstabloda iki sütuna ihtiyacınız olacak , birincisi animable_typeve diğeri animable_idçalışma zamanında ona eklenen model türünü belirlemek.

Köpek veya Kedi modelini verildiği gibi getirebilirsiniz,

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

Bundan sonra, $animkullanarak nesnenin sınıfını kontrol edebilirsiniz instanceof.

Bu yaklaşım, uygulamada başka bir hayvan türü (tilki veya aslan) eklerseniz gelecekteki genişlemeniz için size yardımcı olacaktır. Kod tabanınızı değiştirmeden çalışır. İhtiyacınıza ulaşmak için doğru yol budur. Bununla birlikte, polimorfizm ilişkisi olmadan polimorfizm ve istekli yüklemeyi elde etmek için alternatif bir yaklaşım yoktur. Bir Polimorfik ilişki kullanmıyorsanız , birden fazla veritabanı çağrısı ile sonuçlanırsınız. Ancak, kalıcı türü ayıran tek bir sütun varsa, yanlış yapılandırılmış bir şemanız olabilir. Gelecekteki gelişiminiz için de basitleştirmek istiyorsanız bunu geliştirmenizi öneririz.

Modeli iç Yeniden Yazma newInstance()ve newFromBuilder()iyi / önerilen yol değildir ve size çerçevesinden güncelleme alırsınız kez üzerinde yeniden işleme gerek.


1
Sorunun yorumlarında, sadece bir masası olduğunu ve polimorfik özelliklerin OP durumunda kullanılamayacağını söyledi.
shock_gone_wild

3
Ben sadece verilen senaryonun nasıl olduğunu söylüyorum. Şahsen ben de Polimorfik İlişkiler;) kullanmak istiyorum
shock_gone_wild

1
@KiranManiya detaylı cevabınız için teşekkür ederim. Daha fazla arka planla ilgileniyorum. (1) soru sorucu veritabanı modelinin yanlış olduğunu ve (2) genel / korumalı üye işlevlerini genişletmenin neden iyi / önerilen olmadığını açıklayabilir misiniz?
Christoph Kluge

1
@ChristophKluge, zaten biliyorsun. (1) DB modeli, laravel tasarım modelleri bağlamında yanlıştır. Laravel tarafından tanımlanan tasarım desenini takip etmek istiyorsanız, buna göre DB şemasına sahip olmalısınız. (2) Geçersiz kılmayı önerdiğiniz bir çerçeve iç yöntemidir. Bu sorunla karşılaşırsam yapmam. Laravel çerçevesi yerleşik polimorfizm desteğine sahiptir, bu yüzden neden tekerleği yeniden icat etmek yerine bunu kullanmıyoruz? Cevapta iyi bir ipucu verdiniz ama dezavantajlı kodu hiç tercih etmedim, bunun yerine gelecekteki genişlemeyi basitleştirmeye yardımcı olacak bir şeyi kodlayabiliriz.
Kiran Maniya

2
Ama ... tüm soru Laravel Tasarım kalıplarıyla ilgili değil. Yine, belirli bir senaryomuz var (Belki veritabanı harici bir Uygulama tarafından oluşturulur). Herkes, sıfırdan inşa ederseniz polimorfizmin gidilecek yol olacağını kabul edecektir. Aslında cevabınız teknik olarak orijinal soruya cevap vermiyor.
shock_gone_wild

5

Modeldeki newInstanceyöntemi geçersiz kılabilir Animalve özniteliklerden türü kontrol edebilir ve ardından ilgili modeli başlatabilirsiniz.

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

Ayrıca newFromBuilderyöntemi geçersiz kılmanız gerekir .


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

Bunun nasıl çalıştığını merak etmiyorum. Animal :: find (1) hata verir: Animal :: find (1) öğesini çağırırsanız "undefined index type". Yoksa bir şey mi kaçırıyorum?
shock_gone_wild

@shock_gone_wild typeVeritabanında adlandırılmış bir sütun var mı?
Chris Neal

Evet bende var. Ancak bir dd ($ attritubutes) yaparsam $ attributes dizisi boştur. Hangi mükemmel mantıklı. Bunu gerçek bir örnekte nasıl kullanıyorsunuz?
shock_gone_wild

5

Bunu gerçekten yapmak istiyorsanız, Hayvan modelinizde aşağıdaki yaklaşımı kullanabilirsiniz.

<?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'nin yorumlarında belirttiği gibi: Veritabanı tasarımı zaten ayarlanmış ve bu nedenle Laravel'in Polimorfik İlişkileri burada bir seçenek gibi görünmemektedir.

ben Chris Neal cevap gibi Geçenlerde benzer bir şey yapmak zorunda çünkü (dbase / DBF dosyaları için anlamlı desteklemek için kendi Veritabanı Sürücü yazma) ve laravel en Eloquent ORM dahili özelliklerine sahip bir çok deneyim kazanmıştır.

Model başına açık bir eşleme tutarken kodu daha dinamik hale getirmek için kişisel lezzetimi ekledim.

Hızla test ettiğim desteklenen özellikler:

  • Animal::find(1) sorunuzda sorulduğu gibi çalışıyor
  • Animal::all() de çalışıyor
  • Animal::where(['type' => 'dog'])->get() geri dönücek AnimalDog-objects bir koleksiyon olarak
  • Bu özelliği kullanan her eloquent sınıfı için dinamik nesne eşleme
  • AnimalHiçbir eşleme yapılandırılmadığında (veya DB'de yeni bir eşleme göründüğünde) -modele dönüş

Dezavantajları:

  • Modelin dahili newInstance()ve newFromBuilder()tamamen yeniden yazılması (kopyala yapıştır). Bu, çerçeveden bu üye işlevlerine herhangi bir güncelleme yapılacaksa, kodu elle benimsemeniz gerektiği anlamına gelir.

Umarım yardımcı olur ve senaryonuzda herhangi bir öneri, soru ve ek kullanım durumu için hazırım. Bunun kullanım örnekleri ve örnekleri:

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

Ve bu nasıl kullanılabileceğine ve bunun için ilgili sonuçların altında bir örnektir:

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

hangi sonuç aşağıdaki:

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

Ve eğer kullanmak istiyorsanız MorphTrait, elbette bunun için tam kod:

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

Sadece meraktan. Bu, Animal :: all () ile de çalışır mı? Ortaya çıkan koleksiyon 'Köpekler' ve 'Kediler' karışımı mı?
shock_gone_wild

@shock_gone_wild oldukça iyi bir soru! Yerel olarak test ettim ve cevabıma ekledim. İyi çalışıyor gibi görünüyor :-)
Christoph Kluge

2
laravel'in yerleşik işlevini değiştirmek doğru bir yol değildir. Biz laravel'i güncelledikten sonra tüm değişiklikler kaybolacak ve her şeyi mahvedecek. Farkında olmak.
Navin D. Shah

Hey Navin, bundan bahsettiğiniz için teşekkür ederim ama cevabımın içinde zaten bir dezavantaj olduğu açıkça belirtiliyor. Karşı soru: O zaman doğru yol nedir?
Christoph Kluge

2

Sanırım ne aradığını biliyorum. Laravel sorgu kapsamlarını kullanan bu zarif çözümü düşünün, ek bilgi için https://laravel.com/docs/6.x/eloquent#query-scopes adresine bakın :

Paylaşılan mantığı tutan bir üst sınıf oluşturun:

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

Genel sorgu kapsamı ve savingolay işleyicisi olan bir alt öğe (veya birden çok) oluşturun :

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

(aynı şey başka bir sınıf için de geçerlidir Cat, sadece sabiti değiştirin)

Genel sorgu kapsamı, varsayılan bir sorgu değişikliği olarak işlev görür, böylece Dogsınıf her zaman ile kayıtları arar type='dog'.

Diyelim ki 3 kaydımız var:

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

Şimdi çağırarak Dog::find(1)neden olacaktır nullvarsayılan sorgu kapsamı bulamayacakları için, id:1bir olan Cat. Aramak Animal::find(1)ve Cat::find(1)her ikisi de çalışır, ancak sonuncusu size gerçek bir Cat nesnesi verir.

Bu kurulumun güzel yanı, yukarıdaki sınıfları aşağıdaki gibi ilişkiler oluşturmak için kullanabilmenizdir:

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

Ve bu ilişki otomatik olarak size sadece tüm hayvanları verecektir type='dog'şeklinde (Dog sınıf ) . Sorgu kapsamı otomatik olarak uygulanır.

Buna ek olarak, görüşmesi özelliğini Dog::create($properties)otomatik olarak ayarlayacaktır typeiçin 'dog'nedeniyle savingolay kanca (bkz https://laravel.com/docs/6.x/eloquent#events ).

Aramanın Animal::create($properties)varsayılan olmadığını unutmayın, typebu yüzden burada manuel olarak ayarlamanız gerekir (beklenen).


0

Laravel'i kullanmanıza rağmen, bu durumda, Laravel kısa yollarına bağlı kalmamalısınız.

Çözmeye çalıştığınız bu sorun, diğer birçok dilin / çerçevenin Fabrika yöntemi kalıbını ( https://en.wikipedia.org/wiki/Factory_method_pattern ) kullanarak çözdüğü klasik bir sorundur .

Kodunuzun daha kolay anlaşılmasını ve gizli numaraların bulunmasını istemiyorsanız, kaputun altındaki gizli / sihirli numaralar yerine iyi bilinen bir desen kullanmalısınız.


0

Hayvan sınıfında yöntem yapmanın en kolay yolu

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

Çözme modeli

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

Bu, model türüne bağlı olarak Animal, Dog veya Cat sınıfının örneğini döndürür

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.