Uzyskaj instancję podtypu modelu za pomocą Eloquent

22

Mam Animalmodel oparty na animalstole.

Ta tabela zawiera typepole, które może zawierać wartości, takie jak kot lub pies .

Chciałbym móc tworzyć takie obiekty jak:

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

Jednak będąc w stanie przynieść takie zwierzę:

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

Ale gdzie $animalbyłoby wystąpienie Doglub Catzależne od typepola, które mogę sprawdzić za pomocą instance oflub które będą działać z metodami podpowiedzi typu. Powodem jest to, że 90% kodu jest współdzielone, ale jeden może szczekać, a drugi miauczy.

Wiem, że mogę to zrobić Dog::find($id), ale nie tego chcę: mogę określić typ obiektu tylko po jego pobraniu. Mógłbym również pobrać Zwierzę, a następnie uruchomić find()odpowiedni obiekt, ale wykonuje to dwa wywołania bazy danych, których oczywiście nie chcę.

Próbowałem znaleźć sposób na „ręczne” utworzenie elokwentnego modelu, takiego jak Dog from Animal, ale nie mogłem znaleźć żadnej metody odpowiadającej. Pomyliłem jakiś pomysł lub metodę?

ClmentM
źródło
@ B001 ᛦ Oczywiście moja klasa psów lub kotów będzie miała odpowiednie interfejsy, nie widzę, jak to tutaj pomaga?
ClmentM
@ClmentM Wygląda jak jeden do wielu polimorficznych związków laravel.com/docs/6.x/…
vivek_23
@ vivek_23 Nie bardzo, w tym przypadku pomaga filtrować komentarze danego typu, ale już wiesz, że w końcu chcesz komentarze. Nie dotyczy tutaj.
ClmentM
@ClmentM Myślę, że tak. Zwierzę może być Kotem lub Psem. Tak więc, gdy pobierasz typ zwierzęcia ze stołu zwierząt, daje to instancję Psa lub Kota w zależności od tego, co jest przechowywane w bazie danych. W ostatnim wierszu jest napisane: Relacja, którą można komentować w modelu komentarza, zwróci instancję postu lub wideo, w zależności od typu modelu będącego właścicielem komentarza.
vivek_23
@ vivek_23 Zanurzyłem więcej w dokumentacji i spróbowałem, ale Eloquent opiera się na rzeczywistej kolumnie z *_typenazwą, aby określić model podtypu. W moim przypadku tak naprawdę mam tylko jeden stół, więc chociaż jest to miła funkcja, nie w moim przypadku.
ClmentM

Odpowiedzi:

7

Możesz używać związków polimorficznych w Laravel, jak wyjaśniono w oficjalnych dokumentach Laravel . Oto jak możesz to zrobić.

Zdefiniuj relacje w modelu, jak podano

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

Tutaj potrzebujesz dwóch kolumn w animalstabeli, pierwsza to animable_typekolejna, a druga to animable_idokreślenie typu modelu dołączonego do niej w czasie wykonywania.

Możesz pobrać model psa lub kota, jak podano,

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

Następnie możesz sprawdzić $animklasę obiektu, używając instanceof.

Takie podejście pomoże ci w przyszłej ekspansji, jeśli dodasz inny typ zwierzęcia (tj. Lisa lub lwa) w aplikacji. Będzie działać bez zmiany bazy kodu. To jest właściwy sposób na spełnienie wymagań. Jednak nie ma alternatywnego podejścia do osiągnięcia polimorfizmu i chętnego ładowania razem bez stosowania zależności polimorficznej. Jeśli nie użyjesz relacji polimorficznej , otrzymasz więcej niż jedno wywołanie bazy danych. Jeśli jednak masz jedną kolumnę, która odróżnia typ modalny, być może masz niewłaściwy schemat strukturalny. Sugeruję, aby to poprawić, jeśli chcesz uprościć to również w przyszłości.

Przepisanie wewnętrznego modelu newInstance()i newFromBuilder()nie jest dobrym / zalecanym sposobem i musisz go przerobić, gdy otrzymasz aktualizację z frameworka.

Kiran Maniya
źródło
1
W komentarzach do pytania powiedział, że ma tylko jedną tabelę, a cechy polimorficzne nie są użyteczne w przypadku OP.
shock_gone_wild
3
Mówię tylko, jak wygląda dany scenariusz. Osobiście
użyłbym
1
@ KiranManiya dziękuję za szczegółową odpowiedź. Interesuje mnie więcej tła. Czy potrafisz wyjaśnić, dlaczego (1) model bazy danych pytających jest nieprawidłowy i (2) rozszerzenie publicznych / chronionych funkcji członkowskich nie jest dobre / zalecane?
Christoph Kluge
1
@ChristophKluge, już wiesz. (1) Model DB jest niepoprawny w kontekście wzorców projektowych laravel. Jeśli chcesz podążać za wzorcem projektowym zdefiniowanym przez laravel, powinieneś mieć zgodnie z nim schemat DB. (2) Jest to wewnętrzna metoda ramowa, którą zasugerowałeś. Nie zrobię tego, jeśli kiedykolwiek napotkam ten problem. Szkielet Laravela ma wbudowaną obsługę polimorfizmu, więc dlaczego nie użyjemy tego raczej na nowo wynajdując koło? Dałeś dobrą wskazówkę w odpowiedzi, ale nigdy nie wolałem kodu z wadą, zamiast tego możemy zakodować coś, co pomaga uprościć przyszłą rozbudowę.
Kiran Maniya
2
Ale ... całe pytanie nie dotyczy wzorów Laravel Design. Ponownie mamy dany scenariusz (być może baza danych jest tworzona przez zewnętrzną aplikację). Wszyscy zgodzą się, że polimorfizm byłby dobrym rozwiązaniem, jeśli budujesz od zera. W rzeczywistości twoja odpowiedź technicznie nie odpowiada na pierwotne pytanie.
shock_gone_wild
5

Myślę, że możesz zastąpić newInstancemetodę na Animalmodelu i sprawdzić typ z atrybutów, a następnie zainicjować odpowiedni model.

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

Musisz także zastąpić newFromBuildermetodę.


    /**
     * 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
źródło
Nie wiem, jak to działa. Animal :: find (1) zgłosi błąd: „niezdefiniowany typ indeksu”, jeśli wywołasz Animal :: find (1). A może coś mi brakuje?
shock_gone_wild
@shock_gone_wild Czy masz kolumnę o nazwie typew bazie danych?
Chris Neal
Tak, mam. Ale tablica $ atrybuty jest pusta, jeśli wykonam dd ($ attritubutes). Co doskonale ma sens. Jak wykorzystujesz to na prawdziwym przykładzie?
shock_gone_wild
5

Jeśli naprawdę chcesz to zrobić, możesz zastosować następujące podejście w swoim modelu zwierząt.

<?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
źródło
5

Jak stwierdził OP w swoich komentarzach: Projekt bazy danych jest już ustawiony i dlatego relacje polimorficzne Laravela nie wydają się być tutaj opcją.

Podoba mi się odpowiedź Chrisa Neala, ponieważ ostatnio musiałem zrobić coś podobnego (napisanie własnego sterownika bazy danych do obsługi Eloquent dla plików dbase / DBF) i zdobyłem duże doświadczenie z elementami wewnętrznymi Eloquent ORM Laravela.

Dodałem do tego mój osobisty gust, aby kod był bardziej dynamiczny, a jednocześnie zachowywał wyraźne mapowanie dla poszczególnych modeli.

Obsługiwane funkcje, które szybko przetestowałem:

  • Animal::find(1) działa zgodnie z pytaniem
  • Animal::all() działa również
  • Animal::where(['type' => 'dog'])->get()zwróci AnimalDog-objects jako kolekcję
  • Dynamiczne mapowanie obiektów dla elokwentnej klasy, która korzysta z tej cechy
  • Powrót do Animal-model w przypadku, gdy nie skonfigurowano mapowania (lub nowe mapowanie pojawiło się w DB)

Niedogodności:

  • Przepisuje wewnętrznie newInstance()i newFromBuilder()całkowicie model (skopiuj i wklej). Oznacza to, że jeśli będzie jakaś aktualizacja z frameworka do tych funkcji członkowskich, będziesz musiał ręcznie zaadaptować kod.

Mam nadzieję, że to pomoże i jestem gotowy na wszelkie sugestie, pytania i dodatkowe przypadki użycia w twoim scenariuszu. Oto przykłady użycia i przykłady:

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

A to jest przykład tego, jak można go użyć i poniżej odpowiednich wyników:

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

co daje następujące wyniki:

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

A jeśli chcesz użyć MorphTraittutaj, jest to oczywiście pełny 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);
    }
}
Christoph Kluge
źródło
Z czystej ciekawości. Czy to również działa z Animal :: all () Czy powstały zbiór jest mieszanką „Dogs” i „Cats”?
shock_gone_wild
@shock_gone_wild całkiem dobre pytanie! Przetestowałem to lokalnie i dodałem do mojej odpowiedzi. Wydaje się również działać :-)
Christoph Kluge
2
modyfikowanie wbudowanej funkcji laravela jest nieprawidłowe. Wszystkie zmiany zostaną utracone, gdy zaktualizujemy laravel i wszystko to zepsuje. Być świadomym.
Navin D. Shah
Hej, Navin, dziękuję, że o tym wspomniałeś, ale w mojej odpowiedzi jest to już wyraźnie określone jako wada. Odpowiedź na pytanie: w takim razie jaki jest właściwy sposób?
Christoph Kluge
2

Myślę, że wiem, czego szukasz. Rozważ to eleganckie rozwiązanie, które wykorzystuje zakresy zapytań Laravela, zobacz https://laravel.com/docs/6.x/eloquent#query-scopes, aby uzyskać dodatkowe informacje:

Utwórz klasę nadrzędną, która ma wspólną logikę:

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

Utwórz dziecko (lub wiele) z globalnym zakresem zapytania i savingobsługą zdarzeń:

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

(to samo dotyczy innej klasy Cat, wystarczy zastąpić stałą)

Globalny zasięg zapytania działa jak domyślna modyfikacja zapytania, dzięki czemu Dogklasa zawsze będzie szukała rekordów type='dog'.

Powiedzmy, że mamy 3 rekordy:

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

Teraz wywołanie Dog::find(1)spowoduje null, ponieważ domyślny zakres zapytania nie znajdzie tego, id:1co jest Cat. Wywołanie Animal::find(1)i Cat::find(1)będzie działać, chociaż tylko ostatni daje rzeczywisty obiekt Cat.

Zaletą tego ustawienia jest to, że możesz użyć powyższych klas do tworzenia relacji takich jak:

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

I ta relacja automatycznie da ci tylko wszystkie zwierzęta z type='dog'(w formie Dogklas). Zakres zapytania jest stosowany automatycznie.

Ponadto wywołanie Dog::create($properties)automatycznie ustawi wartość na typez 'dog'powodu savingzdarzenia związanego z zdarzeniem (patrz https://laravel.com/docs/6.x/eloquent#events ).

Pamiętaj, że dzwonienie Animal::create($properties)nie ma wartości domyślnej, typedlatego musisz ustawić ją ręcznie (czego należy się spodziewać).

Płomień
źródło
0

Chociaż używasz Laravela, w tym przypadku myślę, że nie powinieneś trzymać się skrótów Laravela.

Problem, który próbujesz rozwiązać, to klasyczny problem, który wiele innych języków / frameworków rozwiązuje przy użyciu wzorca metody Factory ( https://en.wikipedia.org/wiki/Factory_method_pattern ).

Jeśli chcesz, aby Twój kod był łatwiejszy do zrozumienia i bez ukrytych sztuczek, powinieneś użyć dobrze znanego wzoru zamiast ukrytych / magicznych sztuczek pod maską.

rubens21
źródło
0

Najłatwiejszym sposobem jest stworzenie metody w klasie 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;
}

Model rozstrzygający

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

Zwróci instancję klasy Animal, Dog lub Cat w zależności od typu modelu

Ruben Danielyan
źródło