Lấy ví dụ về kiểu con của mô hình với Eloquent


22

Tôi có một Animalmô hình, dựa trên animalbảng.

Bảng này chứa một typetrường, có thể chứa các giá trị như mèo hoặc chó .

Tôi muốn có thể tạo các đối tượng như:

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

Tuy nhiên, có thể lấy một con vật như thế này:

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

Nhưng nơi $animalnào sẽ là một thể hiện Doghoặc Cattùy thuộc vào typetrường, mà tôi có thể kiểm tra bằng cách sử dụng instance ofhoặc nó sẽ hoạt động với các phương thức gợi ý kiểu. Lý do là 90% mã được chia sẻ, nhưng một cái có thể sủa và cái còn lại có thể kêu meo meo.

Tôi biết rằng tôi có thể làm Dog::find($id), nhưng đó không phải là điều tôi muốn: Tôi có thể xác định loại đối tượng chỉ khi nó được tải xuống. Tôi cũng có thể tìm nạp Động vật, rồi chạy find()đúng đối tượng, nhưng đây là hai cuộc gọi cơ sở dữ liệu mà tôi rõ ràng không muốn.

Tôi đã cố gắng tìm cách "thủ công" khởi tạo một mô hình Eloquent như Dog từ Animal, nhưng tôi không thể tìm thấy bất kỳ phương thức nào tương ứng. Bất kỳ ý tưởng hoặc phương pháp tôi bỏ lỡ xin vui lòng?


@ B001 Tất nhiên, lớp Dog hoặc Cat của tôi sẽ có giao diện tương ứng, tôi không thấy nó giúp gì ở đây?
ClmentM

@ClmentM Trông giống như một đến nhiều mối quan hệ đa hình laravel.com/docs/6.x/
vivek_23

@ vivek_23 Không thực sự, trong trường hợp này, nó giúp lọc các bình luận thuộc một loại nhất định, nhưng bạn đã biết rằng bạn muốn bình luận cuối cùng. Không áp dụng ở đây.
ClmentM

@ClmentM Tôi nghĩ là có. Động vật có thể là Mèo hoặc Chó. Vì vậy, khi bạn truy xuất loại động vật từ bảng động vật, nó sẽ cung cấp cho bạn một thể hiện của Chó hoặc Mèo tùy thuộc vào những gì được lưu trữ trong cơ sở dữ liệu. Dòng cuối cùng cho biết Mối quan hệ có thể nhận xét trên mô hình Nhận xét sẽ trả về một thể hiện Bài đăng hoặc Video, tùy thuộc vào loại mô hình nào sở hữu nhận xét.
vivek_23

@ vivek_23 Tôi đã tìm hiểu thêm về tài liệu và dùng thử, nhưng Eloquent dựa trên cột thực tế có *_typetên để xác định mô hình phụ. Trong trường hợp của tôi, tôi thực sự chỉ có một bảng, vì vậy trong khi đó là một tính năng hay, không phải trong trường hợp của tôi.
ClmentM

Câu trả lời:


7

Bạn có thể sử dụng Mối quan hệ đa hình trong Laravel như được giải thích trong Tài liệu chính thức của Laravel . Đây là cách bạn có thể làm điều đó.

Xác định các mối quan hệ trong mô hình như đã cho

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

Ở đây bạn sẽ cần hai cột trong animalsbảng, đầu tiên là animable_typevà một cột khác là animable_idxác định loại mô hình được gắn với nó khi chạy.

Bạn có thể lấy mô hình Dog hoặc Cat như đã cho,

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

Sau đó, bạn có thể kiểm tra $animlớp của đối tượng bằng cách sử dụng instanceof.

Cách tiếp cận này sẽ giúp bạn mở rộng trong tương lai nếu bạn thêm một loại động vật khác (ví dụ như cáo hoặc sư tử) trong ứng dụng. Nó sẽ hoạt động mà không thay đổi cơ sở mã của bạn. Đây là cách chính xác để đạt được yêu cầu của bạn. Tuy nhiên, không có cách tiếp cận khác để đạt được đa hình và háo hức tải cùng nhau mà không sử dụng mối quan hệ đa hình. Nếu bạn không sử dụng mối quan hệ Đa hình , bạn sẽ kết thúc với nhiều hơn một cuộc gọi cơ sở dữ liệu. Tuy nhiên, nếu bạn có một cột duy nhất phân biệt loại phương thức, có thể bạn có một lược đồ có cấu trúc sai. Tôi đề nghị bạn cải thiện điều đó nếu bạn muốn đơn giản hóa nó để phát triển trong tương lai.

Viết lại nội bộ của mô hình newInstance()newFromBuilder()không phải là cách tốt / được đề xuất và bạn phải làm lại trên mô hình đó một khi bạn sẽ nhận được bản cập nhật từ khung.


1
Trong các bình luận của câu hỏi mà anh ta nói, anh ta chỉ có một bảng và các tính năng đa hình không thể sử dụng được trong trường hợp của OP.
shock_gone_wild

3
Tôi chỉ nói rõ, kịch bản đã cho là như thế nào. Cá nhân tôi cũng sẽ sử dụng Mối quan hệ đa hình;)
shock_gone_wild

1
@KiranManiya cảm ơn bạn đã trả lời chi tiết. Tôi quan tâm đến nhiều nền tảng hơn. Bạn có thể giải thích tại sao (1) mô hình cơ sở dữ liệu của người hỏi là sai và (2) mở rộng các chức năng thành viên được bảo vệ / công khai không tốt / được đề xuất không?
Christoph Kluge

1
@ChristophKluge, Bạn đã biết. (1) Mô hình DB sai trong bối cảnh các mẫu thiết kế laravel. Nếu bạn muốn theo mô hình thiết kế được xác định bởi laravel, bạn nên có lược đồ DB theo nó. (2) Đây là một phương thức nội bộ khung mà bạn đã đề xuất để ghi đè. Tôi sẽ không làm điều đó nếu tôi phải đối mặt với vấn đề này. Khung làm việc của Laravel có hỗ trợ đa hình tích hợp, vậy tại sao chúng ta không sử dụng cái đó thay vì phát minh lại bánh xe? Bạn đã đưa ra một manh mối tốt trong câu trả lời nhưng tôi không bao giờ thích mã có bất lợi thay vào đó chúng ta có thể mã hóa thứ gì đó giúp đơn giản hóa việc mở rộng trong tương lai.
Kiran Maniya

2
Nhưng ... toàn bộ câu hỏi không phải là về các mẫu Thiết kế của Laravel. Một lần nữa, chúng ta có một kịch bản nhất định (Có lẽ cơ sở dữ liệu được tạo bởi một Ứng dụng bên ngoài). Mọi người sẽ đồng ý rằng đa hình sẽ là con đường để đi nếu bạn xây dựng từ đầu. Trong thực tế, câu trả lời của bạn về mặt kỹ thuật không trả lời câu hỏi ban đầu.
shock_gone_wild

5

Tôi nghĩ rằng bạn có thể ghi đè newInstancephương thức trên Animalmô hình và kiểm tra kiểu từ các thuộc tính và sau đó khởi tạo mô hình tương ứng.

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

Bạn cũng sẽ cần ghi đè newFromBuilderphương thức.


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

Tôi không biết làm thế nào điều này hoạt động. Animal :: find (1) sẽ đưa ra một lỗi: "loại chỉ mục không xác định" nếu bạn gọi Animal :: find (1). Hay tôi đang thiếu một cái gì đó?
shock_gone_wild

@shock_gone_wild Bạn có một cột có tên typetrong cơ sở dữ liệu không?
Chris Neal

Vâng tôi có. Nhưng mảng $ thuộc tính trống nếu tôi thực hiện một dd ($ attritubutes). Mà hoàn toàn có ý nghĩa. Làm thế nào để bạn sử dụng điều này trong một ví dụ thực tế?
shock_gone_wild

5

Nếu bạn thực sự muốn làm điều này, bạn có thể sử dụng phương pháp sau trong mô hình Động vật của mình.

<?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

Như OP đã nêu trong các bình luận của mình: Thiết kế cơ sở dữ liệu đã được thiết lập và do đó Mối quan hệ đa hình của Laravel dường như không phải là một lựa chọn ở đây.

Tôi thích câu trả lời của Chris Neal vì tôi đã phải làm một cái gì đó tương tự gần đây (viết Trình điều khiển cơ sở dữ liệu của riêng tôi để hỗ trợ Eloquent cho các tệp dbase / DBF) và đã có được nhiều kinh nghiệm với các phần bên trong của Eloquent ORM của Laravel.

Tôi đã thêm hương vị cá nhân của mình vào đó để làm cho mã động hơn trong khi vẫn giữ ánh xạ rõ ràng cho mỗi mô hình.

Các tính năng được hỗ trợ mà tôi đã nhanh chóng thử nghiệm:

  • Animal::find(1) làm việc như đã hỏi trong câu hỏi của bạn
  • Animal::all() làm việc tốt
  • Animal::where(['type' => 'dog'])->get()sẽ trả về AnimalDog-objects như một bộ sưu tập
  • Ánh xạ đối tượng động trên mỗi lớp hùng biện sử dụng đặc điểm này
  • Dự phòng Animal-model trong trường hợp không có ánh xạ được cấu hình (hoặc ánh xạ mới xuất hiện trong DB)

Nhược điểm:

  • Đó là viết lại nội bộ newInstance()newFromBuilder()toàn bộ mô hình (sao chép và dán). Điều này có nghĩa là nếu có bất kỳ cập nhật nào từ khung cho các chức năng thành viên này, bạn sẽ cần phải sử dụng mã bằng tay.

Tôi hy vọng nó có ích và tôi sẵn sàng cho bất kỳ đề xuất, câu hỏi và trường hợp sử dụng bổ sung nào trong kịch bản của bạn. Dưới đây là các trường hợp sử dụng và ví dụ cho nó:

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

Và đây là một ví dụ về cách nó có thể được sử dụng và dưới kết quả tương ứng cho nó:

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

kết quả như sau:

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

Và trong trường hợp bạn muốn bạn sử dụng thì MorphTraittất nhiên đây là mã đầy đủ cho nó:

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

Chỉ tò mò thôi. Cái này cũng hoạt động với Animal :: all () Bộ sưu tập kết quả có phải là hỗn hợp của 'Chó' và 'Mèo' không?
shock_gone_wild

@shock_gone_wild câu hỏi khá hay! Tôi đã thử nó tại địa phương và thêm nó vào câu trả lời của tôi. Có vẻ như cũng hoạt động :-)
Christoph Kluge

2
Sửa đổi chức năng tích hợp của laravel không đúng cách. Tất cả các thay đổi sẽ mất đi khi chúng tôi cập nhật laravel và nó sẽ làm rối tung mọi thứ. Hãy nhận biết.
Navin D. Shah

Này Navin, cảm ơn bạn đã đề cập đến điều này nhưng nó đã được nêu rõ là bất lợi trong câu trả lời của tôi. Câu hỏi truy cập: cách chính xác sau đó là gì?
Christoph Kluge

2

Tôi nghĩ rằng tôi biết những gì bạn đang tìm kiếm. Hãy xem xét giải pháp tao nhã này sử dụng phạm vi truy vấn của Laravel, xem https://laravel.com/docs/6.x/eloquent#query-scopes để biết thêm thông tin:

Tạo một lớp cha chứa logic được chia sẻ:

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

Tạo một đứa trẻ (hoặc nhiều) với phạm vi truy vấn toàn cầu và savingtrình xử lý sự kiện:

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

(áp dụng tương tự cho một lớp khác Cat, chỉ cần thay thế hằng số)

Phạm vi truy vấn toàn cầu hoạt động như một sửa đổi truy vấn mặc định, sao cho Doglớp sẽ luôn tìm kiếm các bản ghi type='dog'.

Nói rằng chúng tôi có 3 hồ sơ:

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

Bây giờ gọi Dog::find(1)sẽ dẫn đến null, bởi vì phạm vi truy vấn mặc định sẽ không tìm thấy id:1đó là một Cat. Gọi Animal::find(1)Cat::find(1)cả hai sẽ hoạt động, mặc dù chỉ có người cuối cùng cung cấp cho bạn một đối tượng Cat thực sự.

Điều thú vị của thiết lập này là bạn có thể sử dụng các lớp ở trên để tạo các mối quan hệ như:

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

Và mối quan hệ này sẽ tự động chỉ cung cấp cho bạn tất cả các động vật với type='dog'(dưới dạng các Doglớp). Phạm vi truy vấn được tự động áp dụng.

Bên cạnh đó, gọi điện thoại Dog::create($properties)sẽ tự động thiết lập typeđể 'dog'do sự savingmóc sự kiện (xem https://laravel.com/docs/6.x/eloquent#events ).

Lưu ý rằng cuộc gọi Animal::create($properties)không có mặc định, typevì vậy ở đây bạn cần đặt thủ công (điều này sẽ được mong đợi).


0

Mặc dù bạn đang sử dụng Laravel, nhưng trong trường hợp này, tôi nghĩ bạn không nên dính vào các phím tắt của Laravel.

Vấn đề này bạn đang cố gắng giải quyết là một vấn đề kinh điển mà nhiều ngôn ngữ / khung công tác khác giải quyết bằng cách sử dụng mẫu phương thức Factory ( https://en.wikipedia.org/wiki/Factory_method_potype ).

Nếu bạn muốn mã của mình dễ hiểu hơn và không có các thủ thuật ẩn, bạn nên sử dụng một mô hình nổi tiếng thay vì các thủ thuật ẩn / ma thuật dưới mui xe.


0

Cách dễ nhất là tạo phương thức trong lớp Thú

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

Mô hình giải quyết

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

Điều này sẽ trả về thể hiện của lớp Animal, Dog hoặc Cat tùy thuộc vào loại mô hình

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.