Laravel - Eloquent - 动态定义关联关系

12

是否有可能动态设置模型之间的关系?例如,我有一个名为Page的模型,我想在不实际更改其文件的情况下添加关系banners()。那么是否存在类似以下方式:

Page::createRelationship('banners', function(){
    $this->hasMany('banners');
});

或者类似的东西?由于它们已经使用魔术方法获取,也许我可以动态添加关系?谢谢!

使用多态关系和特征来实现您想要的效果,在 Laravel 文档中查看相关内容。 - Sari Yono
我可以看到Model类上有一个setRelation方法,但它不是静态的,只能在实例上调用。是否有类似的方法可以自动设置所有创建的实例之间的关系? - Giedrius
如果是这种情况,你不能创建一个新的静态变量吗?它将以与静态变量相同的方式工作,以满足你的需求。 - Sari Yono
我已经扩展了基本的Eloquent Model类,以添加支持使用我在问题中发布的语法来实现动态关系的功能。 - Giedrius
@A.B.Developer仍在使用我在上面评论中发布的解决方案(扩展基类以支持动态关系)。 - Giedrius
显示剩余4条评论
5个回答

18
更新:现在可以在Laravel 7中使用Model::resolveRelationUsing()来实现此功能。

我已经为此添加了一个包i-rocky/eloquent-dynamic-relation

如果有人仍在寻找解决方案,这里有一个。如果你认为这是个不好的主意,请告诉我。

trait HasDynamicRelation
{
    /**
     * Store the relations
     *
     * @var array
     */
    private static $dynamic_relations = [];

    /**
     * Add a new relation
     *
     * @param $name
     * @param $closure
     */
    public static function addDynamicRelation($name, $closure)
    {
        static::$dynamic_relations[$name] = $closure;
    }

    /**
     * Determine if a relation exists in dynamic relationships list
     *
     * @param $name
     *
     * @return bool
     */
    public static function hasDynamicRelation($name)
    {
        return array_key_exists($name, static::$dynamic_relations);
    }

    /**
     * If the key exists in relations then
     * return call to relation or else
     * return the call to the parent
     *
     * @param $name
     *
     * @return mixed
     */
    public function __get($name)
    {
        if (static::hasDynamicRelation($name)) {
            // check the cache first
            if ($this->relationLoaded($name)) {
                return $this->relations[$name];
            }

            // load the relationship
            return $this->getRelationshipFromMethod($name);
        }

        return parent::__get($name);
    }

    /**
     * If the method exists in relations then
     * return the relation or else
     * return the call to the parent
     *
     * @param $name
     * @param $arguments
     *
     * @return mixed
     */
    public function __call($name, $arguments)
    {
        if (static::hasDynamicRelation($name)) {
            return call_user_func(static::$dynamic_relations[$name], $this);
        }

        return parent::__call($name, $arguments);
    }
}

将以下代码添加到您的模型中,以添加此特征。
class MyModel extends Model {
    use HasDynamicRelation;
}

现在你可以使用以下方法来添加新的关系
MyModel::addDynamicRelation('some_relation', function(MyModel $model) {
    return $model->hasMany(SomeRelatedModel::class);
});

我最终也为我的项目做了同样的事情,效果非常好!干得好,谢谢。 - Giedrius
自Laravel 8版本开始,此功能已经包含在内。 - gerardnll
我认为这很好,但在某些情况下,您无法访问模型MyModel,因为它位于外部,这就是为什么我认为最好的方法是在包中创建一个trait并在您的模型中使用它。 - Amir Hassan Azimi

4

2

您可以使用调用来处理您的动态关系,例如:

您应该在服务提供者的启动方法中编写此代码。

\Illuminate\Database\Eloquent\Builder::macro('yourRelation', function () {  
     return $this->getModel()->belongsTo('class'); 
});

这很棒。但是,您将失去使用访问器查询关系的能力。例如 Model::first()->yourRelation,您必须改用 Model::first()->yourRelation()->get() - Juan Rangel

1
您需要先有一个想法,Eloquent关系是关系数据库模型(例如MySQL)的模型。因此,我提出了两种方法。
好的方法:
如果您想要在数据库中实现具有索引和外键的完整功能的Eloquent关系,则可能需要动态更改SQL表。
例如,假设您已经创建了所有模型并且不想动态创建它们,则只需更改Page表,添加一个名为“banner_id”的新字段,将其索引并引用Banner表上的“banner_id”字段。
然后,您需要编写并支持将要使用的RDBMS。 之后,您可能希望包括对迁移的支持。如果是这种情况,您可以将这些表格更改存储在数据库中以进行进一步的回滚。
现在,对于Eloquent支持部分,您可以查看Eloquent Model Class

注意,对于每种关系,您都有一个底层模型(所有模型可以在这里找到),这实际上就是您在关系方法中返回的内容:

public function hasMany($related, $foreignKey = null, $localKey = null)
{
    $foreignKey = $foreignKey ?: $this->getForeignKey();
    $instance = new $related;
    $localKey = $localKey ?: $this->getKeyName();
    return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}

因此,您需要在模型中定义一个方法,该方法接受关系类型和模型,创建一个新的HasMany实例(如果hasMany是所需的关系),然后返回它。

这有点复杂,因此您可以使用:

简单方式

您可以创建一个中间模型(即PageRelationship),用于存储页面与其他模型之间的所有关系。可能的表结构如下:

+-------------+---------+------------------+-------------+
| relation_id | page_id | foreign_model_id | model_class |
+-------------+---------+------------------+-------------+
|           1 |       2 |              225 | Banner      |
|           2 |       2 |              223 | Banner      |
|           3 |       2 |               12 | Button      |
+-------------+---------+------------------+-------------+

然后,您可以检索给定页面的所有动态相关模型。问题在于,实际上没有任何真正的RDBMS模型和页面之间的关系,因此您可能需要进行多个和重量级的查询来加载相关模型,并且更糟糕的是,您必须自己管理数据库一致性(即删除或更新“225”横幅还应删除或更新页面关系表中的行)。反向关系也会带来麻烦。
结论:
如果项目很大并且取决于它,您无法通过继承或其他方式实现其他模型的模型,则应使用正确的方法。否则,您应该重新思考应用程序设计,然后决定选择或不选择第二种方法。

谢谢您详尽的回答。我能理解任一解决方案都可用,但如果我没弄错的话,两种解决方案都需要编辑“Page”模型类?由于我的系统完全模块化,我正在寻找一种模块扩展其他模块而不过度复杂化模型本身的方法。 - Giedrius
1
你需要修改它,使其能够处理动态关系功能,因为它不是本地实现的,但以后不必更改。模块化系统的正确方法是具有继承模型结构,因此Banner模型扩展PageElement模型,并在Page和PageElement之间建立关系。无论如何,使事情模块化会使您的模型变得复杂 - vpedrosa
1
我将要么拥有自己的Model类扩展,该扩展将被所有模型继承以具备自定义功能,要么将动态关系功能添加到Laravel框架中并提交拉取请求,以便更多人发现其有用。一旦找到明确的解决方案,我会发布答案。 - Giedrius
好的。您能告诉我更多关于您所需的模块化功能以及为什么需要它吗?我正在考虑几种情况,这些情况可能会导致您需要进行设计,并且所有这些情况都可以通过版本更新或代码编辑来解决。 - vpedrosa
基本上,系统上的所有功能都将通过模块实现。我们部署的每个系统实例都将启用不同的模块,并对某些模块进行一些更改。所有模块都将在主系统上进行版本控制,但是会有选项允许每个实例进行编辑。这些模块将由多个开发人员开发,因此我正在尝试找到一种解决方案,可以在对系统的其余部分影响最小的情况下,在不同的系统下安装不同的模块。 - Giedrius

1

如果有人正在寻找 Laravel 8 的答案:

假设我在模型的一个方法中定义了我的关系:

public function relationships()
{
    return [
        'user' => $this->belongsTo(User::class, 'user_id'),
    ];
}

现在,在我的应用程序服务提供者中,我可以使用resolveRelationUsing方法。我通过迭代模型文件夹并检查包含上述方法的所有模型来完成此操作:
    foreach ((new Filesystem)->allFiles(app_path('Models')) as $file) {
        $namespace = 'App\\Models\\' . str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname());
        $class = app($namespace);

        if (method_exists($class, 'relationships')) {
            foreach ($class->relationships() as $key => $relationship) {
                $class->resolveRelationUsing($key, function () use ($class, $key) {
                    return $class->relationships()[$key];
                });
            }
        }
    }

你好 @kdjion,你是否发现该解决方案存在性能问题?我想实现类似的东西,但有很多表/模型,想知道系统是否会变慢。 - mcartur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接