更加顺手的用好 Laravel 的多态关联

5

前言

在业务中,关联是我们最常用到的场景。在开发时我们始终都在强调对数据库设计选择可解耦,简洁化,最小化。在这种开发环境下,往往都会将传统的一个大表拆分成多个小表,这时候关联就显得很重要。

MySQL 为我们提供了像 inner joinleft joinright join 这些关联方式,满足了绝大部分需求。但是在实际开发中,我们还是会去选择一些程序上的关联关系,让代码去处理关联,这些关联从简单的一对一,一对多,再到复杂的多态关联、中间表关联,等等,下面主要从源码的角度去讲解一下 Laravel 中的多态关联

官方文档

从 Laravel 官方文档的中文翻译中,我们可以找到关于多态关联的内容。

一对一多态关联与简单的一对一关联类似;不过,目标模型能够在一个关联上从属于多个模型。例如,博客 Post 和 User 可能共享一个关联到 Image 模型的关系。使用一对一多态关联允许使用一个唯一图片列表同时用于博客文章和用户账户。

官网的文档可能不是那么的直观,这里推荐一个 文章 可以帮助你加深理解,这里就不展开了。

image.png

单从文档来说,如果你的设计或者你之前的设计符合官方的要求以及要求。 *_type 的值必须为被关联的模型的类名

开始操作

很多时候,我们的设计中 type 都不一定会那样设计,基本都是以数字为主,虽然 Laravel 为我们提供了自定义 type 的解决办法 ,但是也不能很好的解决关于数字作为 type 的问题,我还搜索到了一个一样的问题。那么我们就来解决一下,现在有三张表。

  • shopping_cart (购物车表)
字段 类型 介绍
id int 主键
product_type tinyint(1) 关联的产品类型 1 表示 Tool、2 表示 Food
product_id int 关联的产品的ID
  • tool (工具表)
字段 类型 介绍
id int 主键ID
name varchar(20) 名字
  • food (食品表)
字段 类型 介绍
id int 主键ID
name varchar(20) 名字

现在我们有了这三张表,购物车表中根据 product_type 的不同值去关联不同的模型,这里就要用到 多态关联,现在如果我们直接按照官方的文档来编写我们的 Model ,那么,应该是下面这样的。

class ShoppingCart extends Model
{
    const TABLE = 'shopping_cart';
    protected $table = self::TABLE;

    public function product()
    {
        return $this->morphTo();
    }
    
}
class Tool extends Model
{
    const TABLE = 'tool';
    protected $table = self::TABLE;

    public function product()
    {
        return $this->morphOne(ShoppingCart::class, 'product');
    }

}
class Food extends Model
{
    const TABLE = 'food';
    protected $table = self::TABLE;

    public function product()
    {
        return $this->morphOne(ShoppingCart::class, 'product');
    }
    
}

根据官方的文档:模型关联 |《Laravel 5.8 中文文档》| Laravel China 社区。我们的代码应该可以运行,但是可能不符合预期。

image-20191027150453093.png

不出意外的看到了错误信息 “类名必须是有效的对象或者字符”,源码中也是一个 new $class,到 IDE 中打开并断点调试。

image-20191027150847436.png

此时 $class 为 1 ,根据调用栈一路往上找,发现了一个有价值的方法。

image-20191027151420811.png

可以看到,这个 $type 是从这里 $this->dictionary取出来的,按住 Ctrl + 点击 后到了属性定义的位置,然后再按住 Ctrl + 点击 ,选择上面的筛选赋值操作,可以看到只有一处有赋值的操作,点击转到。

image-20191027151751484.png

转到赋值的位置后,打一个断点。

image-20191027151949597.png

看到这里调用栈,源码 过多,就不展开讲解。下面讲重点。
image-20191027152116492.png

看到这个属性,$model->{$this->morphType},先打印它的值$this->morphType,结果是 product_type,然后外层还有 点击进入按钮,我们进入到了模型实例中的 __get 魔术方法。

image-20191027152533661.png

image-20191027152622940.png

在官方手册中,关于 __get 的定义为:

读取不可访问属性的值时,__get() 会被调用。

首先,对于 Model 而言,是没有 product_type 属性的,所以触发了它,方法内部调用了 getAttribute

image-20191027152924822.png

看到 getAttribute 方法内部,第 321 行,使用了一个属性 $this->attribute ,执行表达式可以看到,这就是我们的数据结果。而根据 array_key_exists 的判断可以确定这个 if 是成立的,因为 后面的是 || 运算,即使后面是 false ,这个表达式也是成立,但是我们这里还是希望来看一下这个方法。

image-20191027153530439.png

这个方法只做了一件事,就是判断一个 getter 方法是否存在,这里的 Str::studly() 的作用是把 字符串从下划线命名规则转为大驼峰。也就是说,在这里会检查访问器 ,当然,现在我们是没有这个方法的,继续往下。
image-20191027153637551.png

果然,在 349 ~ 351 行,有着这样的一个逻辑,那么我们回过来看一下 Laravel 文档中关于 修改器 & 访问器 的介绍。
image-20191027153847195.png

简而言之就是,当在访问这个字段的值时,我们可以自己根据获取器的规则定一个名为 getProductTypeAttribute 的访问器方法,在这个方法中,我们可以修改其返回值,作为最终的结果返回给访问者。这样看来,我们就可以在访问器中修改我们原本的 product_type1 为对应的需要实例化的类名称,即可,现在开始定义一下。

public function getProductTypeAttribute($val)
{
    $map = [
        1 => Tool::class,
        2 => Food::class,
    ];
    return $map[$val] ?? Tool::class;
}

根据文档我们可以得知,在对一个已存在的字段添加访问器时,访问器方法可以接受一个参数,其值为原本值,在这个方法中,我们编写了一个 $map ,其 key 为 product_type 字段的原值$val,如果这个字段原值 ($val) ,对应的 key 不存在,就返回默认为 App\Models\Tool模型类,现在这样就够了吗?我们可以来试试。

image-20191027155028728.png

果然,代码可以工作了 ,不再报错,而且,在 relations 属性中我们还可以看到 product 分别是两个不同的模型,接下来我们 toArray 看一下结果。

image-20191027155216334.png

果然,结果已经达到了我们的预期,但是我们却发现 product_type 字段值变成了字符串,而不是原来的数字 1、2,该怎么办?两个办法。

  • 利用获取器添加一个辅助字段,来存储原来的 product_type 。
  • 遍历重新赋值。

下面来展示一下第二种方法,从上面的截图中可以了解到,查询结果给我们返回的是一个Eloquent 集合,现在我们使用其中的 transform,方法来转换原集合。

$list = $cart->with(['product'])->get();
$list->transform(function (ShoppingCart $item) {
    $item->product_type_origin = $item->getOriginal('product_type');
    return $item;
});
dump($list->toArray());

通过模型的 getOriginal 方法拿到了原有的值。
image-20191027160107439.png

到这里,问题已经解决了,那么我们可以自定义 productproduct_typeproduct_id 这三个的名字吗?这一点在 Laravel 文档中鲜有提到,在这里答案是可以的。

我们通过 ShoppingCart 模型的 product 方法,这里我们调用 morphTo 方法没有传递 任何的值。

public function product()
{
    return $this->morphTo();
}

接下来我们进入进入 morphTo 方法,一探究竟。

image-20191027161339955.png

首先映入眼帘的是一段注释,这段注释的 大概意思就是,如果没有指定 $name 那么就从调用栈中取第一条的 function名字作为 $name 也就是最终挂载的模型上的字段名字 方法实现如下

protected function guessBelongsToRelation()
{
    [$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);

    return $caller['function'];
}

接着往下看$type$id

protected function getMorphs($name, $type, $id)
{
    return [$type ?: $name.'_type', $id ?: $name.'_id'];
}

可以看到,当我们没有自己给定 $type$id 时,那么默认值即为 $name 分别加上 _type_id 后缀。

后期补充

2019-10-29

发现经过上面一番操作后,使用 whereHasMorph 方法进行筛选时,type 的值变成了 getAttribute 的值。这时候只需要在被关联的模型 、「Food」和「Tool」 中重写 getMorphClass ,返回值分别为其 type 映射前的值 1、2 即可。

// Tool
public function getMorphClass()
{
    return 1;
}

结束

至此,文章内容结束了。本文主要涉及 Laravel 中关于 多态关联获取器 两个知识点的了解。

文中所使用的调试工具为 PHPStorm 和 Xdebug 。

文中如有纰漏,请不吝赐教,如文中内容涉及到你的利益,请与我联系。

参考资料


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...