空对象模式与雄辩的关系 [英] Null object pattern with Eloquent relations

查看:95
本文介绍了空对象模式与雄辩的关系的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



例如说一个书模型有一个作者()(hasOne)关系我可能想做

  $ author = Book :: find 1) - >作者 - >名称; 

如果Book 1没有作者设置它将抛出试图获取非对象的属性错误。有没有办法避免这种情况,并且默认为空白作者,所以我总是能够调用名称无论关系是否为特定模型设置?



本质上,我想避免条件来检查 $ book->作者在调用其他方法/属性之前,是一个实际的作者。如果没有设置关系,它应该默认为一个新的作者实例。






我尝试过如下:

  public function getAuthorAttribute($ author)
{
return $ author?
}

但这不行; $ author 正在传递为空,即使它在模型上设置。大概是因为它是一种关系,而不是一本书的直接财产。我需要像

  public function getAuthorAttribute()
{
return $ this-> author() - > first()?:new Author;
}

这似乎很漂亮,似乎会覆盖任何渴望加载导致差

解决方案

更新



截至Laravel 5.3.23,现在有一个内置的方法来完成这个(至少对于 HasOne 关系)。一个 withDefault()方法被添加到 HasOne 关系中。如果您的 / 作者示例,您的代码将如下所示:

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

此关系现在将返回一个相当空(设置密钥)作者模型,如果没有在数据库中找到记录。另外,如果你想用一些额外的数据填充你的空模型,你可以传递一个属性数组,或者你可以传递一个关闭,返回你想要的默认设置(没有作为一个作者模型)。



直到这使它成为文档的一天,有更多的信息,你可以查看与更改相关的拉动请求: 16198 16382



在撰写本文时,这只是为了 HasOne 关系而实现的。它可能最终迁移到 BelongsTo MorphOne MorphTo 关系,但我不能肯定地说。






原始



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



使用访问器



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



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

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

/ **
*如果关系已经加载,请获取该值。否则,尝试
*从关系方法加载值。这还将在$ this->关系中设置
*键,以便后续调用将找到密钥。
* /
if(array_key_exists($ key,$ this-> relations)){
$ value = $ this->关系[$ key];
} elseif(method_exists($ this,$ key)){
$ value = $ this-> getRelationshipFromMethod($ key);
}

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

/ **
*此行是可选的。您要将关系值设置为
*新作者,还是要保持为空?想想你的
*想要你的toArray / toJson输出...
* /
$ this-> setRelation($ key,$ value);

return $ value;
}

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



另一个较小的问题是访问器仅在访问该属性时使用。所以,例如,如果你想加载关系,然后 dd() toArray / toJson 该模型,它仍然会显示相对相对性的 null ,而不是一个空的作者。



覆盖模型方法



第二个选项,而不是使用属性访问器,将覆盖模型。这可以解决使用属性访问器的两个问题。



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



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



MyModel.php

  class MyModel extends Model 
{
/ **
*处理热切的关系。调用链:
* Model :: with()=> Builder :: with():set builder eager load
* Model :: get()=> Builder :: get()=> Builder :: eagerLoadRelations()=> Builder :: loadRelation()
* => Relation :: initRelation()=> Model :: setRelation()
* => Relation :: match()=> Relation :: matchOneOrMany()=>模型:: setRelation()
* /
public function setRelation($ relation,$ value)
{
/ **
*与许多记录的关系将始终为一个集合,即使是空的。
*与一个记录的关系将是Model或null。当尝试
*设置为null时,覆盖预期模型的新实例。
* /
if(is_null($ value)){
//将值设置为相关模型的新实例
$ value = $ this-> $ relation () - > getRelated() - >的newInstance();
}

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

return $ this;
}

/ **
*只有在Laravel& 5.2.30。在Laravel
*> = 5.2.30中,此方法调用setRelation方法,其中
*已被覆盖,并包含上述逻辑。
*
*处理懒惰关系。调用链:
* Model :: __ get()=> Model :: getAttribute()=>模型:: getRelationshipFromMethod();
* /
protected function getRelationshipFromMethod($ method)
{
$ results = parent :: getRelationshipFromMethod($ method);

/ **
*与许多记录的关系始终是一个集合,即使是空的。
*与一个记录的关系将是Model或null。当
*结果为空时,用相关模型的新实例覆盖。
* /
if(is_null($ results)){
$ results = $ this-> $ method() - > getRelated() - > newInstance()
}

返回$ this->关系[$ method] = $ results;
}
}

Book.php / p>

  class Book extends MyModel 
{
//
}


There is often the case where an certain eloquent model's relation is unset (i.e. in a books table, author_id is null) and thus calling something like $model->relation returns null.

E.g. say a Book model has an author() (hasOne) relation I might want to do

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

If Book 1 has no author set it will throw a "trying to get property of non object" error. Is there a way to avoid this and default to a blank Author so I'll always be able to call name on it regardless of whether the relation has been set for the specific model?

Essentially I want to avoid conditionals to check if $book->author is an actual Author before calling further methods/properties on it. It should default to a new Author instance if the relation isn't set.


I tried something like:

public function getAuthorAttribute($author)
{
    return $author ?: new Author;
}

however this doesn't work; $author is being passed in as null, even if it's set on the model. Presumably because it's a relation rather than a direct property of a book. I'd need something like

public function getAuthorAttribute()
{
    return $this->author()->first() ?: new Author;
}

which seems pretty inelegant and seems like it would override any eager loading resulting in poor performance.

解决方案

Update

As of Laravel 5.3.23, there is now a built in way to accomplish this (at least for HasOne relationships). A withDefault() method was added to the HasOne relationship. In the case of your Book/Author example, your code would look like:

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

This relationship will now return a fairly empty (keys are set) Author model if no record is found in the database. Additionally, you can pass in an array of attributes if you'd like to populate your empty model with some extra data, or you can pass in a Closure that returns what you'd like to have your default set to (doesn't have to be an Author model).

Until this makes it into the documentation one day, for more information you can check out the pull requests related to the change: 16198 and 16382.

At the time of this writing, this has only been implemented for the HasOne relationship. It may eventually migrate to the BelongsTo, MorphOne, and MorphTo relationships, but I can't say for sure.


Original

There's no built in way that I know of to do this, but there are a couple workarounds.

Using an Accessor

The problem with using an accessor, as you've found out, is that the $value passed to the accessor will always be null, since it is populated from the array of attributes on the model. This array of attributes does not include relationships, whether they're already loaded or not.

If you want to attempt to solve this with an accessor, you would just ignore whatever value is passed in, and check the relationship yourself.

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

Now, the problem with doing this in the accessor is that you need to define an accessor for every hasOne/belongsTo relationship on every model.

A second, smaller, issue is that the accessor is only used when accessing the attribute. So, for example, if you were to eager load the relationship, and then dd() or toArray/toJson the model, it would still show null for the relatioinship, instead of an empty Author.

Overriding Model Methods

A second option, instead of using attribute accessors, would be to override some methods on the Model. This solves both of the problems with using an attribute accessor.

You can create your own base Model class that extends the Laravel Model and overrides these methods, and then all of your other models will extend your base Model class, instead of Laravel's Model class.

To handle eager loaded relationships, you would need to override the setRelation() method. If using Laravel >= 5.2.30, this will also handle lazy loaded relationships. If using Laravel < 5.2.30, you will also need to override the getRelationshipFromMethod() method for lazy loaded relationships.

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

这篇关于空对象模式与雄辩的关系的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆