Php Laravel动态关系-访问负载上的模型属性

Php Laravel动态关系-访问负载上的模型属性,php,laravel,laravel-5,eloquent,eager-loading,Php,Laravel,Laravel 5,Eloquent,Eager Loading,在我的Laravel模型上,我有一个雄辩的关系,它是动态的——也就是说,特定数据库字段的值决定了哪个模型将被加载。当我第一次实例化模型实例,然后引用关系时,我能够很好地加载这个关系,但是当我急于加载那个关系时,它不起作用 具体来说,我有一个产品模型。该产品可能是也可能不是其他产品的父产品。如果产品的parent\u id设置为0,则该产品被视为父零件(无论是否有子零件)。如果父项id设置为不同产品的id,则该产品为子项。我需要能够访问Product::with('parent'),并知道pare

在我的Laravel模型上,我有一个雄辩的关系,它是动态的——也就是说,特定数据库字段的值决定了哪个模型将被加载。当我第一次实例化模型实例,然后引用关系时,我能够很好地加载这个关系,但是当我急于加载那个关系时,它不起作用

具体来说,我有一个
产品
模型。该产品可能是也可能不是其他产品的父产品。如果产品的
parent\u id
设置为
0
,则该产品被视为父零件(无论是否有子零件)。如果父项id设置为不同产品的id,则该产品为子项。我需要能够访问
Product::with('parent')
,并知道
parent
关系将返回自身(是的,重复数据)或其他产品(如果是子产品)

以下是我到目前为止的关系:

public function parent()
{
    if ($this->parent_id > 0) {
        return $this->belongsTo('App\Product', 'parent_id', 'id');
    } else {
        return $this->belongsTo('App\Product', 'id', 'id');
    }
}
当我急于加载时,
$this->parent\u id
总是未定义的,因此即使它实际上是父产品,这个关系也只会返回它自己

在加载关系之前,有没有办法访问模型的属性?在返回关系之前,我考虑过使用一个单独的查询,但我意识到我甚至无法访问产品的id来运行该查询

如果这是不可能的,有什么其他方法来解决这类问题?这似乎不能通过传统的多态关系来解决。我只有两个可能的想法:

  • belongsTo
    关系中添加某种约束,在该关系中动态确定外键
  • 根据不同的数据库字段创建使用外键的自定义关系
老实说,我不知道我将如何实现这两个目标。我这样做对吗?有什么东西是我忽略的吗



仔细考虑后,我认为最简单的解决问题的方法是:是否有任何方法可以在运行时为关系本身内部的关系动态选择外键?我的用例不允许我在调用关系时使用即时加载约束-这些约束需要应用于关系本身。

由于即时加载的工作方式,您无法对正在运行的SQL执行任何操作来完成您想要的操作

执行
Product::with('parent')->get()
时,它会运行两个查询

首先,它运行查询以获取所有产品:

select * from `products`
接下来,它运行一个查询,以获取急切加载的父级:

select * from `products` where `products`.`id` in (?, ?, ?)
参数的数量(
)对应于第一次查询的结果数量。检索到第二组模型后,将使用
match()
函数将对象彼此关联起来

为了做你想做的事情,你必须创建一个新的关系并覆盖
match()
方法。这将处理急切加载方面。此外,您需要重写
addConstraints
方法来处理延迟加载特性

首先,创建一个自定义关系类:

class CustomBelongsTo extends BelongsTo
{
    // Override the addConstraints method for the lazy loaded relationship.
    // If the foreign key of the model is 0, change the foreign key to the
    // model's own key, so it will load itself as the related model.

    /**
     * Set the base constraints on the relation query.
     *
     * @return void
     */
    public function addConstraints()
    {
        if (static::$constraints) {
            // For belongs to relationships, which are essentially the inverse of has one
            // or has many relationships, we need to actually query on the primary key
            // of the related models matching on the foreign key that's on a parent.
            $table = $this->related->getTable();

            $key = $this->parent->{$this->foreignKey} == 0 ? $this->otherKey : $this->foreignKey;

            $this->query->where($table.'.'.$this->otherKey, '=', $this->parent->{$key});
        }
    }

    // Override the match method for the eager loaded relationship.
    // Most of this is copied from the original method. The custom
    // logic is in the elseif.

    /**
     * Match the eagerly loaded results to their parents.
     *
     * @param  array   $models
     * @param  \Illuminate\Database\Eloquent\Collection  $results
     * @param  string  $relation
     * @return array
     */
    public function match(array $models, Collection $results, $relation)
    {
        $foreign = $this->foreignKey;

        $other = $this->otherKey;

        // First we will get to build a dictionary of the child models by their primary
        // key of the relationship, then we can easily match the children back onto
        // the parents using that dictionary and the primary key of the children.
        $dictionary = [];

        foreach ($results as $result) {
            $dictionary[$result->getAttribute($other)] = $result;
        }

        // Once we have the dictionary constructed, we can loop through all the parents
        // and match back onto their children using these keys of the dictionary and
        // the primary key of the children to map them onto the correct instances.
        foreach ($models as $model) {
            if (isset($dictionary[$model->$foreign])) {
                $model->setRelation($relation, $dictionary[$model->$foreign]);
            }
            // If the foreign key is 0, set the relation to a copy of the model
            elseif($model->$foreign == 0) {
                // Make a copy of the model.
                // You don't want recursion in your relationships.
                $copy = clone $model;

                // Empty out any existing relationships on the copy to avoid
                // any accidental recursion there.
                $copy->setRelations([]);

                // Set the relation on the model to the copy of itself.
                $model->setRelation($relation, $copy);
            }
        }

        return $models;
    }
}
创建自定义关系类后,需要更新模型以使用此自定义关系。在模型上创建一个新方法,该方法将使用新的
CustomBelongsTo
关系,并更新
parent()
关系方法以使用此新方法,而不是基本
belongsTo()
方法

class Product extends Model
{

    // Update the parent() relationship to use the custom belongsto relationship
    public function parent()
    {
        return $this->customBelongsTo('App\Product', 'parent_id', 'id');
    }

    // Add the method to create the CustomBelongsTo relationship. This is
    // basically a copy of the base belongsTo method, but it returns
    // a new CustomBelongsTo relationship instead of the original BelongsTo relationship
    public function customBelongsTo($related, $foreignKey = null, $otherKey = null, $relation = null)
    {
        // If no relation name was given, we will use this debug backtrace to extract
        // the calling method's name and use that as the relationship name as most
        // of the time this will be what we desire to use for the relationships.
        if (is_null($relation)) {
            list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

            $relation = $caller['function'];
        }

        // If no foreign key was supplied, we can use a backtrace to guess the proper
        // foreign key name by using the name of the relationship function, which
        // when combined with an "_id" should conventionally match the columns.
        if (is_null($foreignKey)) {
            $foreignKey = Str::snake($relation).'_id';
        }

        $instance = new $related;

        // Once we have the foreign key names, we'll just create a new Eloquent query
        // for the related models and returns the relationship instance which will
        // actually be responsible for retrieving and hydrating every relations.
        $query = $instance->newQuery();

        $otherKey = $otherKey ?: $instance->getKeyName();

        return new CustomBelongsTo($query, $this, $foreignKey, $otherKey, $relation);
    }
}

公平的警告,这些都没有经过测试。

如果产品不是父项,为什么不将产品自己的id放在字段中?如果我从零开始,我想我会这样做,但不幸的是,我正在为一个巨大的遗留代码库/数据库创建一个Laravel API服务。如果我修改该字段的工作方式,它将在其他地方破坏大量代码。我曾考虑添加一个新的字段来实现这一点,但我必须在遗留站点中可以输入产品的每个位置添加大量遗留代码——这是一个真正的噩梦项目。我希望通过某种方式能够使用Laravel实现这一点。如果每一个选择都不可能,我会打电话给你,让你回答这个问题,这样至少你的回答可能会帮助其他试图做同样事情的人——即使这对我没有帮助。在我放弃之前,我会给这个问题多一点时间。有趣的是,你应该提到,目前正在从事一个类似的项目。我会再考虑一下。拉威尔的关系比我希望的要差一点。也许试着用变形关系来处理它?哇,非常感谢!我以前从未有幸建立起自己的关系,但这正是我所需要的!然而,我想指出的是,在一个特定的用例中,这将不起作用:如果您在嵌套的
whereHas
查询中使用它,即
Product::whereHas('parent.location',function($q){//something})->find(1234)
。然而,我认为这更多地是由于这样一个缺点,即它在自身上构建子查询,而需要其他字段作为主键,而不是这种关系本身(如果有意义的话)。只是想把它加在这里,以防别人需要。谢谢大家!@AndyNoelker Laravel对自我关系方面的
has
where has
存在问题。实际上,我最近提交了5.1和5.2版本的PRs,这两个版本被合并到修复此问题中。如果您使用的是5.1,那么5.1.30版在大约40分钟前刚刚被标记,并已修复。如果您使用的是5.2版本,那么5.2.15版本在5天前被标记为已修复。如果您需要此
功能,您需要composer更新Laravel.@AndyNoelker如果您关心血淋淋的细节,您可以签出。您是一个传奇!是的,更新完全修复了