共计 3955 个字符,预计需要花费 10 分钟才能阅读完成。
前言
在业务中,关联是我们最常用到的场景。在开发时我们始终都在强调对数据库设计选择可解耦,简洁化,最小化。在这种开发环境下,往往都会将传统的一个大表拆分成多个小表,这时候关联就显得很重要。
MySQL 为我们提供了像 inner join
、left join
、right join
这些关联方式,满足了绝大部分需求。但是在实际开发中,我们还是会去选择一些程序上的关联关系,让代码去处理关联,这些关联从简单的一对一,一对多,再到复杂的多态关联、中间表关联,等等,下面主要从源码的角度去讲解一下 Laravel 中的多态关联。
官方文档
从 Laravel 官方文档的中文翻译中,我们可以找到关于多态关联的内容。
一对一多态关联与简单的一对一关联类似;不过,目标模型能够在一个关联上从属于多个模型。例如,博客 Post 和 User 可能共享一个关联到 Image 模型的关系。使用一对一多态关联允许使用一个唯一图片列表同时用于博客文章和用户账户。
官网的文档可能不是那么的直观,这里推荐一个 文章 可以帮助你加深理解,这里就不展开了。
单从文档来说,如果你的设计或者你之前的设计符合官方的要求以及要求。 *_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 社区。我们的代码应该可以运行,但是可能不符合预期。
不出意外的看到了错误信息“类名必须是有效的对象或者字符”,源码中也是一个 new $class,到 IDE 中打开并断点调试。
此时 $class 为 1,根据调用栈一路往上找,发现了一个有价值的方法。
可以看到,这个 $type
是从这里 $this->dictionary 取出来的,按住 Ctrl
+ 点击
后到了属性定义的位置, 然后再按住 Ctrl
+ 点击
,选择上面的筛选赋值操作,可以看到只有一处有赋值的操作,点击转到。
转到赋值的位置后,打一个断点。
看到这里调用栈,源码 过多,就不展开讲解。下面讲重点。
看到这个属性,$model->{$this->morphType}
, 先打印它的值$this->morphType
,结果是 product_type
,然后外层还有 点击进入按钮,我们进入到了模型实例中的 __get
魔术方法。
在官方手册中,关于 __get
的定义为:
读取不可访问属性的值时,__get() 会被调用。
首先,对于 Model
而言,是没有 product_type
属性的,所以触发了它,方法内部调用了 getAttribute。
看到 getAttribute
方法内部,第 321 行,使用了一个属性 $this->attribute
,执行表达式可以看到,这就是我们的数据结果。而根据 array_key_exists
的判断可以确定这个 if 是成立的,因为 后面的是 ||
运算,即使后面是 false,这个表达式也是成立,但是我们这里还是希望来看一下这个方法。
这个方法只做了一件事,就是判断一个 getter 方法是否存在,这里的 Str::studly() 的作用是把 字符串从下划线命名规则转为大驼峰 。也就是说,在这里会检查 访问器
,当然,现在我们是没有这个方法的,继续往下。
果然,在 349 ~ 351 行,有着这样的一个逻辑,那么我们回过来看一下 Laravel 文档中关于 修改器 & 访问器 的介绍。
简而言之就是,当在访问这个字段的值时,我们可以自己根据获取器的规则定一个名为 getProductTypeAttribute
的访问器方法,在这个方法中,我们可以修改其返回值,作为最终的结果返回给访问者。这样看来,我们就可以在访问器中修改我们原本的 product_type
的 1
为对应的需要实例化的类名称,即可,现在开始定义一下。
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
模型类,现在这样就够了吗?我们可以来试试。
果然,代码可以工作了,不再报错,而且,在 relations
属性中我们还可以看到 product 分别是两个不同的模型,接下来我们 toArray
看一下结果。
果然,结果已经达到了我们的预期,但是我们却发现 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
方法拿到了原有的值。
到这里,问题已经解决了,那么我们可以自定义 product
、product_type
、product_id
这三个的名字吗?这一点在 Laravel 文档中鲜有提到,在这里答案是可以的。
我们通过 ShoppingCart
模型的 product
方法,这里我们调用 morphTo 方法没有传递 任何的值。
public function product()
{return $this->morphTo();
}
接下来我们进入进入 morphTo
方法,一探究竟。
首先映入眼帘的是一段注释,这段注释的 大概意思就是,如果没有指定 $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
后缀。
结束
至此,文章内容结束了。本文主要涉及 Laravel 中关于 多态关联
、 获取器
两个知识点的了解。
文中所使用的调试工具为 PHPStorm 和 Xdebug。
- 关于 Xdebug 如何配置,欢迎点击查看我的另一篇文章。
- 关于 如何用好 PHPStorm,也欢迎查看我的另一篇文章。
文中如有纰漏,请不吝赐教,如文中内容涉及到你的利益,请与我联系。
参考资料
- php – laravel 自定义多态关联的类型字段 string 转 int 的问题?– SegmentFault 思否
- 介绍 Eloquent 关联中的多态关联(Polymorphic Relations)| Laravel China 社区
- 模型关联 |《Laravel 5.8 中文文档》| Laravel China 社区