Отримати примірник підтипу моделі з Eloquent


22

У мене є Animalмодель, заснована на animalтаблиці.

Ця таблиця містить typeполе, яке може містити такі значення, як кішка чи собака .

Я хотів би мати можливість створювати такі об'єкти, як:

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()на потрібний об’єкт, але це робиться два дзвінки до бази даних, яких я, очевидно, не хочу.

Я намагався шукати спосіб «вручну» створити красномовну модель, як Dog from Animal, але не знайшов відповідного методу. Будь-яку ідею чи метод я пропустив, будь ласка?


@ B001 ᛦ Звичайно, мій клас Собака чи Кішка матиме відповідні інтерфейси, я не бачу, як це допомагає тут?
ClmentM

@ClmentM Виглядає як один з багатьма поліморфними стосунками laravel.com/docs/6.x/…
vivek_23

@ vivek_23 Не дуже, в цьому випадку це допомагає фільтрувати коментарі даного типу, але ви вже знаєте, що хочете коментарів врешті-решт. Тут не застосовується.
ClmentM

@ClmentM Я думаю, що це так. Твариною може бути кот або собака. Отже, коли ви отримуєте тип тварини з таблиці тварин, це дає вам екземпляр собаки чи кішки залежно від того, що зберігається в базі даних. Останній рядок там говорить про те, що коментоване відношення до моделі коментаря поверне або екземпляр Повідомлення, або Відео, залежно від того, який тип моделі належить коментарю.
vivek_23

@ vivek_23 Я занурився більше в документацію і спробував її, але красномовний базується на фактичній колонці з *_typeназвою, щоб визначити модель підтипу. У моєму випадку я дійсно маю лише один стіл, тому, хоча це приємна особливість, не в моєму випадку.
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таблиці, перший є, animable_typeа другий - animable_idвизначити тип моделі, що додається до неї під час виконання.

Ви можете отримати модель Собака або Кішка, як дано,

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

Після цього ви можете перевірити $animклас об'єкта, використовуючи instanceof.

Цей підхід допоможе вам у подальшому розширенні, якщо в додаток додасте інший тип тварини (наприклад, лисицю чи лева). Він буде працювати без зміни вашої кодової бази. Це правильний спосіб досягти вашої вимоги. Однак альтернативного підходу для досягнення поліморфізму та прагнення до навантаження разом без використання поліморфних зв’язків немає. Якщо ви не використовуєте поліморфні стосунки , ви отримаєте більше одного дзвінка до бази даних. Однак якщо у вас є один стовпець, який диференціює модальний тип, можливо, у вас неправильна структурована схема. Я пропоную вам покращити це, якщо ви хочете спростити його і для подальшого розвитку.

Перезапис внутрішніх моделей newInstance()і newFromBuilder()не є хорошим / рекомендованим способом, і вам доведеться переробляти його, як тільки ви отримаєте оновлення з фреймворку.


1
У коментарях до питання він сказав, що у нього є лише одна таблиця, а поліморфні ознаки не застосовуються у випадку з ОП.
shock_gone_wild

3
Я просто констатую, що таке даний сценарій. Я особисто також би використовував поліморфні стосунки;)
shock_gone_wild

1
@KiranManiya дякую за детальну відповідь. Мене цікавить більше інформації. Чи можете ви пояснити, чому (1) модель бази даних запитуючих не є правильною та (2) розширення функцій громадськості та захищених членів не годиться / не рекомендується?
Крістоф Клюге

1
@ChristophKluge, Ви вже знаєте. (1) Модель БД помилкова в контексті моделей дизайну laravel. Якщо ви хочете слідувати шаблону дизайну, визначеному laravel, вам слід мати схему БД відповідно до нього. (2) Це рамковий внутрішній метод, який ви запропонували перевизначити. Я не зроблю цього, якщо я коли-небудь зіткнуся з цим питанням. Laravel Framework має вбудовану підтримку поліморфізму, тож чому б ми не використати це, а не переосмислити колесо? Ви дали хорошу підказку у відповіді, але я ніколи не вважав за краще код з недоліком, натомість ми можемо кодувати щось, що допомагає спростити майбутнє розширення.
Кіран Манія

2
Але ... все питання не в моделях Laravel Design. Знову ми маємо заданий сценарій (Можливо, база даних створена зовнішнім додатком). Усі погодяться, що поліморфізм - це шлях, якщо будувати з нуля. Насправді ваша відповідь технічно не відповідає на початкове запитання.
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). Або я щось пропускаю?
shock_gone_wild

@shock_gone_wild У вас є стовпець, названий typeу базі даних?
Кріс Ніл

Так, у мене є. Але масив $ атрибутів порожній, якщо я роблю dd ($ attritubutes). Що ідеально має сенс. Як ви використовуєте це на реальному прикладі?
shock_gone_wild

5

Якщо ви дійсно хочете це зробити, ви можете використовувати наступний підхід у вашій моделі 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);
    }
}

5

Як заявив ОП у своїх коментарях: Дизайн бази даних вже налаштований, і тому поліморфні стосунки Ларавеля тут, здається, не є варіантом.

Мені подобається відповідь Кріса Ніла, тому що я нещодавно мені довелося зробити щось подібне (написати власний драйвер бази даних для підтримки красномовних файлів dbase / DBF) та набув багато досвіду роботи з красномовними красномовними ORM Laravel.

Я додав до нього свій особистий аромат, щоб зробити код більш динамічним, зберігаючи чітке відображення для кожної моделі.

Підтримувані функції, які я швидко перевірив:

  • Animal::find(1) працює, як задано у вашому запитанні
  • Animal::all() працює також
  • Animal::where(['type' => 'dog'])->get()поверне AnimalDog-об'єкти як колекція
  • Динамічне відображення об'єктів для красномовного класу, який використовує цю ознаку
  • Зворотний зв'язок до Animal-моделі у випадку, якщо у вас не налаштовано картографування (або нове відображення з’явилося в БД)

Недоліки:

  • Це перезапис внутрішнього 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, і це зіпсує все. Бережись.
Навін Д. Шах

Ей, Навіне, дякую, що ти згадав про це, але це вже чітко зазначено як недолік у моїй відповіді. Питання зустрічного: Який тоді правильний шлях?
Крістоф Клюге

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

Тепер виклик Dog::find(1)призведе до null, оскільки область запиту за замовчуванням не знайде того, id:1що є Cat. Виклик Animal::find(1)і Cat::find(1)буде працювати обом, хоча лише останній дає вам фактичний об'єкт Cat.

Найприємніше в цьому налаштуванні, що ви можете використовувати класи вище для створення відносин, таких як:

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.

Ця проблема, яку ви намагаєтеся вирішити, є класичною проблемою, яку вирішує багато інших мов / фреймворків за допомогою заводського методу ( 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 залежно від типу моделі

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.