具有雄辩关系的空对象模式

时间:2015-10-12 15:55:24

标签: php laravel laravel-5 eloquent

通常情况下,某个雄辩模型的关系未被设置(即在书籍表中,author_id为空),因此调用$ model->之类的关系会返回null。

E.g。说Book模型有一个我可能想做的author()(hasOne)关系

$author = Book::find(1)->author->name;

如果第1册没有设置作者,则会抛出“试图获取非对象属性”错误。有没有办法避免这种情况,默认为空Author,所以无论是否为特定模型设置了关系,我都可以在其上调用name

基本上我想在调用其他方法/属性之前避免条件检查$book->author是否为实际Author。如果未设置关系,它应默认为新的Author实例。

我尝试过类似的事情:

public function getAuthorAttribute($author)
{
    return $author ?: new Author;
}
然而,这不起作用;即使在模型上设置了$author,也会将其作为null传入。大概是因为它是一种关系,而不是一本书的直接属性。我需要像

这样的东西
public function getAuthorAttribute()
{
    return $this->author()->first() ?: new Author;
}

看起来非常优雅,看起来它会覆盖任何急切的加载,导致性能不佳。

3 个答案:

答案 0 :(得分:7)

更新

从Laravel 5.3.23开始,现在有一种内置的方法可以实现这一目标(至少对于HasOne关系而言)。 withDefault()关系中添加了HasOne方法。对于Book / Author示例,您的代码如下所示:

public function author() {
    return $this->hasOne(Author::class)->withDefault();
}

如果在数据库中找不到记录,则此关系现在将返回相当空(键设置)Author模型。此外,如果您想要使用一些额外数据填充空模型,则可以传入一组属性,或者您可以传入一个Closure,它返回您希望将默认设置为(并不一定是Author模型。

直到有一天它进入文档,有关详细信息,您可以查看与更改相关的提取请求:1619816382

在撰写本文时,这只是针对HasOne关系实施的。它最终可能会迁移到BelongsToMorphOneMorphTo关系,但我无法肯定地说。

原始

我知道没有这样做的内置方式,但有几种解决方法。

使用存取器

使用访问器的问题,正如您所知,传递给访问者的$value始终为null,因为它是从属性数组中填充的该模型。这些属性数组不包括关系,无论它们是否已经加载。

如果您想尝试使用访问器解决此问题,您只需忽略传入的任何值,并自行检查关系。

public function getAuthorAttribute($value)
{
    $key = 'author';

    /**
     * If the relationship is already loaded, get the value. Otherwise, attempt
     * to load the value from the relationship method. This will also set the
     * key in $this->relations so that subsequent calls will find the key.
     */
    if (array_key_exists($key, $this->relations)) {
        $value = $this->relations[$key];
    } elseif (method_exists($this, $key)) {
        $value = $this->getRelationshipFromMethod($key);
    }

    $value = $value ?: new Author();

    /**
     * This line is optional. Do you want to set the relationship value to be
     * the new Author, or do you want to keep it null? Think of what you'd
     * want in your toArray/toJson output...
     */
    $this->setRelation($key, $value);

    return $value;
}

现在,在访问器中执行此操作的问题是您需要为每个模型上的每个hasOne / belongsTo关系定义一个访问器。

第二个较小的问题是访问者仅在访问属性时使用。因此,例如,如果您急切地加载关系,然后dd()toArray / toJson模型,它仍然会显示null的关系,而不是一个空的作者。

覆盖模型方法

第二个选项,而不是使用属性访问器,将覆盖Model上的某些方法。这解决了使用属性访问器的两个问题。

您可以创建自己的基础Model类,扩展Laravel Model并覆盖这些方法,然后所有其他模型将扩展您的基础Model类,而不是Laravel& #39; s Model类。

要处理急切加载的关系,您需要覆盖setRelation()方法。如果使用Laravel> = 5.2.30,这也将处理延迟加载的关系。如果使用Laravel< 5.2.30,您还需要覆盖延迟加载关系的getRelationshipFromMethod()方法。

<强> MyModel.php

class MyModel extends Model
{
    /**
     * Handle eager loaded relationships. Call chain:
     * Model::with() => Builder::with(): sets builder eager loads
     * Model::get() => Builder::get() => Builder::eagerLoadRelations() => Builder::loadRelation()
     *     =>Relation::initRelation() => Model::setRelation()
     *     =>Relation::match() =>Relation::matchOneOrMany() => Model::setRelation()
     */
    public function setRelation($relation, $value)
    {
        /**
         * Relationships to many records will always be a Collection, even when empty.
         * Relationships to one record will either be a Model or null. When attempting
         * to set to null, override with a new instance of the expected model.
         */
        if (is_null($value)) {
            // set the value to a new instance of the related model
            $value = $this->$relation()->getRelated()->newInstance();
        }

        $this->relations[$relation] = $value;

        return $this;
    }

    /**
     * This override is only needed in Laravel < 5.2.30. In Laravel
     * >= 5.2.30, this method calls the setRelation method, which
     * is already overridden and contains our logic above.
     *
     * Handle lazy loaded relationships. Call chain:
     * Model::__get() => Model::getAttribute() => Model::getRelationshipFromMethod();
     */
    protected function getRelationshipFromMethod($method)
    {
        $results = parent::getRelationshipFromMethod($method);

        /**
         * Relationships to many records will always be a Collection, even when empty.
         * Relationships to one record will either be a Model or null. When the
         * result is null, override with a new instance of the related model.
         */
        if (is_null($results)) {
            $results = $this->$method()->getRelated()->newInstance();
        }

        return $this->relations[$method] = $results;
    }
}

<强> book.php中

class Book extends MyModel
{
    //
}

答案 1 :(得分:0)

我的项目遇到了同样的问题。在我的观点中,有一些行从空关系访问dinamics属性,但是不是返回一个空字段,而是应用程序被抛弃和异常。

我刚刚在我的控制器中添加了一个foreach循环,作为一个临时解决方案,如果关系为null,则验证集合的每个值。如果这种情况属实,它会将期望模型的新实例分配给该值。

An exception occured in driver: could not find driver

这样当我在视图中访问 foreach ($shifts as $shift) { if (is_null($shift->productivity)) { $shift->productivity = new Productivity(); } } 时,如果未设置关系,我会得到一个空值而不是异常,而不会在我的视图中添加任何逻辑,也不会覆盖方法。

等待更好的解决方案自动执行此操作。

答案 2 :(得分:-1)

您可以使用模型工厂实现此目的。

在ModelFactory.php中定义作者工厂

$factory->define(App\Author::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->firstName, //or null
        'avatar' => $faker->imageUrl() //or null
    ];
});

为所有需要的属性添加值我正在使用Faker中的虚拟值,但您可以使用任何您想要的值。

然后在您的图书模型中,您可以返回像这样的作者实例:

public function getAuthorAttribute($author)
{
    return $author ?: factory(App\Author::class)->make();
}