使用Eloquent获取模型子类型的实例


22

我有一个Animal基于animal表格的模型。

该表包含一个type字段,可以包含诸如catdog的值。

我希望能够创建如下对象:

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

但是,能够像这样取出动物:

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

但是在哪里$animal可以作为实例DogCat取决于type字段,我可以检查使用instance of还是可以与类型提示方法一起使用。原因是90%的代码是共享的,但是一个可以吠叫,而另一个可以叫。

我知道我可以做到Dog::find($id),但这不是我想要的:我只能在提取对象后确定其类型。我也可以获取Animal,然后find()在正确的对象上运行,但是这样做是两个数据库调用,我显然不希望这样做。

我试图寻找一种方法来“手动”实例化Eloquent模型,例如Animal的Dog,但是我找不到对应的任何方法。我有什么想法或方法想念吗?


@ B001ᛦ当然,我的Dog或Cat类将具有相应的接口,在这里看不到它有什么帮助?
ClmentM

@ClmentM看起来像一对多多态性关系laravel.com/docs/6.x/...
vivek_23

@ vivek_23并非如此,在这种情况下,它有助于过滤给定类型的注释,但是您已经知道最后需要注释。在这里不适用。
ClmentM

@ClmentM我认为是的。动物可以是猫或狗。因此,当您从动物表中检索动物类型时,它会根据数据库中存储的内容为您提供“狗”或“猫”的实例。最后一行显示Comment模型上的Commentable关系将返回Post或Video实例,具体取决于拥有评论的模型类型。
vivek_23

@ vivek_23我进一步研究了文档并进行了尝试,但是Eloquent基于具有*_type名称的实际列来确定子类型模型。就我而言,我实际上只有一张桌子,因此虽然这是一个不错的功能,但就我而言却不是。
ClmentM

Answers:


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在运行时确定与其关联的模型的类型。

您可以按照给定的方式获取Dog或Cat模型,

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

之后,您可以$anim使用来检查对象的类instanceof

如果在应用程序中添加其他动物类型(例如狐狸或狮子),则此方法将帮助您将来进行扩展。它无需更改代码库即可工作。这是达到要求的正确方法。但是,没有其他方法可以实现多态并渴望在不使用多态关系的情况下一起加载。如果不使用Polymorphic关系,则最终将进行多个数据库调用。但是,如果只有一个区分模式类型的列,则可能是结构错误的架构。如果您也想简化它以便将来开发,我建议您进行改进。

重写模型的内部newInstance()newFromBuilder()是不是一个好/推荐的方式,你必须返工一次,你会得到从框架的更新。


1
他在对问题的评论中说,他只有一张桌子,而在OP的情况下,多态功能不可用。
shock_gone_wild

3
我只是在说,给定的场景是什么样的。我个人也将使用多态关系;)
shock_gone_wild

1
@KiranManiya感谢您的详细回答。我对更多背景感兴趣。您能否详细说明为什么(1)提问者数据库模型错误,以及(2)扩展公共/受保护的成员函数不好/不推荐?
Christoph Kluge

1
@ChristophKluge,您已经知道了。(1)在laravel设计模式下,DB模型是错误的。如果要遵循laravel定义的设计模式,则应根据它具有数据库模式。(2)这是您建议重写的框架内部方法。如果遇到此问题,我不会这样做。Laravel框架具有内置的多态性支持,所以为什么不使用它来重新发明轮子呢?您在答案中提供了一个很好的线索,但是我从不偏爱劣势代码,相反,我们可以编写有助于简化将来扩展的代码。
Kiran Maniya

2
但是...整个问题不是关于Laravel设计模式的。同样,我们有一个给定的场景(也许数据库是由外部应用程序创建的)。每个人都会同意,如果您从头开始构建,多态性将是必经之路。实际上,您的回答在技术上不会回答原始问题。
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)将引发错误:“ undefined index type”。还是我错过了什么?
shock_gone_wild

@shock_gone_wild数据库中是否有一个命名的列type
克里斯·尼尔

是的,我有。但是如果执行dd($ attritubutes),则$ attributes数组为空。这确实很有意义。在实际示例中如何使用它?
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

正如OP在他的评论中所述:数据库设计已经设置,因此Laravel的多态关系在这里似乎不是一个选择。

喜欢Chris Neal的答案,因为最近我不得不做类似的事情(编写我自己的数据库驱动程序来为dbase / DBF文件支持Eloquent),并且在Laravel的Eloquent ORM内部获得了很多经验。

我添加了个人风格,以使代码更动态,同时保持每个模型的显式映射。

我快速测试过的支持功能:

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

只是出于好奇。这也适用于Animal :: all()生成的集合是“狗”和“猫”的混合物吗?
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,只需替换常量)

全局查询范围用作默认查询修改,因此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快捷方式。

您要解决的此问题是许多其他语言/框架使用Factory方法模式(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();

这将根据模型类型返回动物,狗或猫类的实例

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.