รับอินสแตนซ์ของชนิดย่อยของแบบจำลองด้วย 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% มีการแชร์ แต่หนึ่งสามารถเห่าและอื่น ๆ สามารถ meow

ฉันรู้ว่าฉันสามารถทำได้Dog::find($id)แต่มันไม่ใช่สิ่งที่ฉันต้องการ: ฉันสามารถกำหนดประเภทของวัตถุได้เมื่อมันถูกดึงออกมาแล้ว ฉันยังสามารถดึงข้อมูลสัตว์แล้วเรียกใช้find()บนวัตถุที่ถูกต้อง แต่นี่เป็นการเรียกฐานข้อมูลสองสายซึ่งฉันไม่ต้องการ

ฉันพยายามหาวิธีที่จะ "สร้างด้วยตนเอง" ยกตัวอย่างนางแบบ Eloquent เช่น Dog from Animal แต่ฉันไม่สามารถหาวิธีที่สอดคล้องกันได้ มีความคิดหรือวิธีการที่ฉันพลาดไปไหม


@ B001 ᛦแน่นอนว่าคลาส Dog หรือ Cat ของฉันจะมีอินเตอร์เฟสที่สอดคล้องกันฉันไม่เห็นว่ามันช่วยได้อย่างไร
ClMM

@ClmentM ดูเหมือนความสัมพันธ์แบบ polymorphic กับlaravel.com/docs/6.x/…
vivek_23

@ vivek_23 ไม่จริงในกรณีนี้จะช่วยกรองความคิดเห็นของประเภทที่กำหนด แต่คุณรู้อยู่แล้วว่าคุณต้องการความคิดเห็นในตอนท้าย ใช้ไม่ได้ที่นี่
ClMM

@ClmentM ฉันคิดว่ามันทำ สัตว์สามารถเป็นได้ทั้งแมวหรือสุนัข ดังนั้นเมื่อคุณดึงข้อมูลประเภทสัตว์จากตารางสัตว์มันจะให้ตัวอย่างของสุนัขหรือแมวอย่างใดอย่างหนึ่งขึ้นอยู่กับสิ่งที่เก็บไว้ในฐานข้อมูล บรรทัดสุดท้ายที่มีข้อความระบุว่าความสัมพันธ์ที่สามารถแสดงความคิดเห็นได้ในโมเดลความคิดเห็นจะส่งคืนอินสแตนซ์โพสต์หรือวิดีโอขึ้นอยู่กับประเภทของโมเดลที่เป็นเจ้าของความคิดเห็น
vivek_23

@ vivek_23 ฉันพุ่งเข้าไปในเอกสารมากขึ้นและลองดู แต่ Eloquent ขึ้นอยู่กับคอลัมน์จริงที่มี*_typeชื่อเพื่อกำหนดรูปแบบย่อย ในกรณีของฉันฉันมีตารางเดียวจริง ๆ ดังนั้นในขณะที่เป็นคุณลักษณะที่ดีไม่ใช่ในกรณีของฉัน
ClMM

คำตอบ:


7

คุณสามารถใช้ Polymorphic ความสัมพันธ์ใน 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

หลังจากนั้นคุณสามารถตรวจสอบระดับของวัตถุโดยใช้$animinstanceof

วิธีนี้จะช่วยคุณในการขยายตัวในอนาคตหากคุณเพิ่มสัตว์ประเภทอื่น (เช่นสุนัขจิ้งจอกหรือสิงโต) ในแอปพลิเคชัน มันจะทำงานได้โดยไม่ต้องเปลี่ยน codebase ของคุณ นี่เป็นวิธีที่ถูกต้องเพื่อให้บรรลุความต้องการของคุณ อย่างไรก็ตามไม่มีวิธีการอื่นเพื่อให้ได้ polymorphism และกระตือรือร้นในการโหลดเข้าด้วยกันโดยไม่ใช้ความสัมพันธ์ polymorphic หากคุณไม่ได้ใช้ความสัมพันธ์แบบ Polymorphicคุณจะจบลงด้วยการเรียกฐานข้อมูลมากกว่าหนึ่งครั้ง อย่างไรก็ตามหากคุณมีคอลัมน์เดียวที่แยกประเภทของคำกริยาบางทีคุณอาจมีโครงสร้างที่ผิดพลาด ฉันขอแนะนำให้คุณปรับปรุงว่าถ้าคุณต้องการทำให้มันง่ายสำหรับการพัฒนาในอนาคตเช่นกัน

เขียนใหม่แบบจำลองภายในnewInstance()และnewFromBuilder()ไม่ใช่วิธีที่ดี / แนะนำและคุณต้องทำใหม่เมื่อคุณได้รับการอัพเดตจากกรอบงาน


1
ในความคิดเห็นของคำถามที่เขากล่าวว่าเขามีเพียงหนึ่งตารางและคุณสมบัติ polymorphic ไม่สามารถใช้งานได้ในกรณีของ OP
shock_gone_wild

3
ฉันแค่บอกว่าสถานการณ์เป็นอย่างไร ฉันเองก็ใช้ความสัมพันธ์แบบ polymorphic;)
shock_gone_wild

1
@KiranManiya ขอบคุณสำหรับคำตอบโดยละเอียดของคุณ ฉันสนใจพื้นหลังเพิ่มเติม คุณช่วยอธิบายได้ไหมว่าทำไม (1) โมเดลฐานข้อมูลของผู้ถามผิดและ (2) การขยายฟังก์ชั่นสมาชิก / สาธารณะที่มีการป้องกันไม่ดี / แนะนำ?
Christoph Kluge

1
@ChristophKluge คุณรู้อยู่แล้ว (1) DB model ผิดในบริบทของรูปแบบการออกแบบ laravel หากคุณต้องการทำตามรูปแบบการออกแบบที่กำหนดโดย laravel คุณควรมี schema ของ DB ตามนั้น (2) เป็นวิธีการภายในกรอบที่คุณได้แนะนำให้แทนที่ ฉันจะไม่ทำมันถ้าฉันประสบปัญหานี้ เฟรมเวิร์ก Laravel มีการสนับสนุนหลายรูปแบบในตัวดังนั้นทำไมเราไม่ใช้สิ่งนั้นแทนที่จะประดิษฐ์วงล้อใหม่ คุณให้คำตอบที่ดี แต่ฉันไม่เคยต้องการรหัสที่มีข้อเสียแทนเราสามารถเขียนโค้ดบางอย่างที่ช่วยให้การขยายตัวในอนาคตง่ายขึ้น
Kiran Maniya

2
แต่ ... คำถามทั้งหมดไม่ได้เกี่ยวกับรูปแบบของ Laravel Design อีกครั้งเรามีสถานการณ์ที่กำหนด (บางทีฐานข้อมูลถูกสร้างขึ้นโดยแอปพลิเคชันภายนอก) ทุกคนจะเห็นพ้องต้องกันว่า polymorphism เป็นหนทางที่จะไปหากคุณสร้างตั้งแต่เริ่มต้น ในความเป็นจริงคำตอบของคุณไม่ได้ตอบคำถามเดิมทางเทคนิค
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;
    }

ฉันไม่คิดว่ามันทำงานอย่างไร สัตว์ :: ค้นหา (1) จะโยนข้อผิดพลาด: "ดัชนีที่ไม่ได้กำหนดประเภท" ถ้าคุณเรียกใช้สัตว์ :: ค้นหา (1) หรือฉันกำลังพลาดอะไรอยู่?
shock_gone_wild

@shock_gone_wild คุณมีคอลัมน์ชื่อtypeในฐานข้อมูลหรือไม่
Chris Neal

ใช่ฉันมี. แต่อาร์เรย์ $ attribute นั้นว่างเปล่าถ้าฉันทำ dd ($ attritubutes) ซึ่งทำให้รู้สึกอย่างสมบูรณ์แบบ คุณใช้สิ่งนี้ในตัวอย่างจริงได้อย่างไร
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 ระบุไว้ในความคิดเห็นของเขา:การออกแบบฐานข้อมูลได้ถูกตั้งค่าไว้แล้วดังนั้นความสัมพันธ์แบบ Polymorphicของ Laravel จึงไม่น่าจะเป็นตัวเลือกที่นี่

ฉันชอบคำตอบของ Chris Nealเพราะฉันต้องทำสิ่งที่คล้ายกันเมื่อเร็ว ๆ นี้ (เขียนไดร์เวอร์ฐานข้อมูลของตัวเองเพื่อรองรับ Eloquent สำหรับไฟล์ dbase / DBF) และได้รับประสบการณ์มากมายจาก Eloquent ORM ของ Laravel

ฉันได้เพิ่มรสนิยมส่วนตัวของฉันไว้ในนั้นเพื่อให้โค้ดมีไดนามิกมากขึ้นในขณะที่ยังคงทำแผนที่ชัดเจนต่อรุ่น

คุณสมบัติที่รองรับซึ่งฉันทดสอบอย่างรวดเร็ว:

  • Animal::find(1) ทำงานตามที่ถามในคำถามของคุณ
  • Animal::all() ทำงานได้เช่นกัน
  • Animal::where(['type' => 'dog'])->get()จะส่งคืนAnimalDog-objects เป็นคอลเล็กชัน
  • การแมปวัตถุแบบไดนามิกต่อระดับภาษาพูดซึ่งใช้ลักษณะนี้
  • ย้อนกลับไปยังAnimal-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);
    }
}

เพิ่งออกมาจากความอยากรู้ สิ่งนี้ยังใช้งานได้กับสัตว์ :: ทั้งหมด () คอลเลกชันที่ได้นั้นเป็นส่วนผสมของ 'Dogs' และ 'Cats' หรือไม่?
shock_gone_wild

@shock_gone_wild คำถามที่ดีงาม! ฉันทดสอบในเครื่องและเพิ่มไปยังคำตอบของฉัน ดูเหมือนว่าจะทำงานเช่นกัน :-)
Christoph Kluge

2
การแก้ไขฟังก์ชั่น laravel ในตัวไม่ถูกต้อง การเปลี่ยนแปลงทั้งหมดจะหายไปเมื่อเราอัพเดท laravel และมันจะเลอะทุกอย่าง ระวัง.
Navin D. Shah

เฮ้นาวินขอบคุณที่พูดถึงเรื่องนี้ แต่มันก็ระบุไว้ชัดเจนว่าเป็นข้อเสียในคำตอบของฉัน ตอบโต้คำถาม: อะไรคือวิธีที่ถูกต้องแล้ว?
Christoph 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เพียงแค่เปลี่ยนค่าคงที่)

ขอบเขตแบบสอบถามทั่วโลกทำหน้าที่เป็นแบบสอบถามการปรับเปลี่ยนค่าเริ่มต้นดังกล่าวที่ระดับมักจะมองหาระเบียนที่มีDogtype='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

วิธีที่ง่ายที่สุดคือการสร้างวิธีในคลาสสัตว์

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.