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

Clé*_*let 22 php laravel eloquent

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

此表包含一个type字段,该字段可以包含catdog等值。

我希望能够创建对象,例如:

class Animal extends Model { }
class Dog extends Animal { }
class Cat extends Animal { }
Run Code Online (Sandbox Code Playgroud)

然而,能够像这样获取动物:

$animal = Animal::find($id);
Run Code Online (Sandbox Code Playgroud)

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

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

我试图寻找一种方法来“手动”实例化像 Dog from Animal 这样的 Eloquent 模型,但我找不到任何相应的方法。我错过了任何想法或方法吗?

Chr*_*uge 8

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

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

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

我快速测试的支持功能:

  • Animal::find(1) 按您的问题要求工作
  • Animal::all() 也能用
  • Animal::where(['type' => 'dog'])->get()AnimalDog-objects 作为集合返回
  • 每个使用此特征的 eloquent-class 的动态对象映射
  • 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 {}
Run Code Online (Sandbox Code Playgroud)

这是一个如何使用它的示例,并在其各自的结果下方:

$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);
Run Code Online (Sandbox Code Playgroud)

结果如下:

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
Run Code Online (Sandbox Code Playgroud)

如果你想使用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);
    }
}

Run Code Online (Sandbox Code Playgroud)

  • 修改 laravel 的内置函数不是正确的方法。一旦我们更新了 laravel,所有的改变都会丢失,这会让一切变得混乱。意识到。 (2认同)
  • 嘿,纳文,谢谢您提到这一点,但它已经在我的答案中明确指出是缺点。反问:那么正确的方法是什么? (2认同)

Kir*_*iya 8

您可以按照 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');
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,您需要表中的两列animals,第一列是animable_type,另一列是animable_id在运行时确定附加到它的模型的类型。

您可以获取指定的 Dog 或 Cat 模型,

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

之后,您可以$anim使用instanceof.

如果您在应用程序中添加另一种动物类型(即狐狸或狮子),这种方法将有助于您未来的扩展。它将在不更改您的代码库的情况下工作。这是实现您的要求的正确方法。但是,没有其他方法可以在不使用多态关系的情况下实现多态性和预先加载。如果您不使用多态关系,您最终会得到不止一个数据库调用。但是,如果您有一个单独的列来区分模态类型,那么您的结构化模式可能有误。如果您还想为未来的开发简化它,我建议您改进它。

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

  • 我只是说明给定的场景是什么样的。我个人也会使用多态关系;) (3认同)
  • 但是......整个问题不在于 Laravel 设计模式。同样,我们有一个给定的场景(也许数据库是由外部应用程序创建的)。每个人都会同意,如果从头开始构建,多态性将是可行的方法。事实上,从技术上讲,您的答案并没有回答原来的问题。 (2认同)

小智 5

I think you could override the newInstance method on the Animal model, and check the type from the attributes and then init the corresponding 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;
    }
Run Code Online (Sandbox Code Playgroud)

You'll also need to override the newFromBuilder method.


    /**
     * 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;
    }
Run Code Online (Sandbox Code Playgroud)


sho*_*ild 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);
    }
}
Run Code Online (Sandbox Code Playgroud)