当前位置: 首页 > 后端技术 > PHP

善用Laravel的多态关联

时间:2023-03-29 21:03:47 PHP

前言在商业中,关联是最常用的场景。在开发过程中,我们一直强调数据库设计选择的解耦、简化和最小化。在这种开发环境下,传统的一张大表往往会拆分成多个小表,这时候关联就很重要了。MySQL为我们提供了innerjoin、leftjoin、rightjoin等关联方式,可以满足大部分需求。但是在实际开发中,我们还是会选择一些过程性的关系,让代码来处理这些关系。这些关系从简单的一对一、一对多,到复杂的多态关系、中间表关系等等,下面主要从源码的角度讲解Laravel中的多态关联。官方文档从Laravel官方文档的中文翻译中,我们可以找到关于多态关联的内容。一对一多态关联类似于简单的一对一关联;但是,目标模型可以从属于关联中的多个模型。例如,博客Post和User可能共享与Image模型的关系。使用一对一的多态关联允许将唯一图像的单个列表用于博客文章和用户帐户。官网的文档可能没有那么直观。这里推荐一篇文章帮助大家加深理解,这里就不展开了。单从文档来看,你的设计或者你之前的设计是否符合官方的要求和要求。*_type的值必须是关联模型的类名。很多时候,我们设计中的字体不一定是这么设计的,基本都是根据数字来设计的。虽然Laravel为我们提供了自定义类型的解决方案,但是并不能很好的解决数字作为类型的问题。我也搜索了类似的问题。那么我们来解决吧,现在有三张表。shopping_cart(购物车表)字段类型介绍idint主键product_typetinyint(1)关联商品类型1表示Tool,2表示Foodproduct_idint关联商品IDtool(工具表)字段类型介绍idint主键IDnamevarchar(20)namefood(食品表)字段类型介绍idint主键IDnamevarchar(20)name现在我们有了这三张表,在购物车表中根据product_type的不同值来关联不同的型号,这里需要用到多态关联,现在如果直接按照官方的写我们的Model文件的话,应该如下。类ShoppingCart扩展模型{constTABLE='shopping_cart';受保护的$table=self::TABLE;publicfunctionproduct(){返回$this->morphTo();}}classToolextendsModel{constTABLE='tool';受保护的$table=self::TABLE;publicfunctionproduct(){return$this->morphOne(ShoppingCart::class,'product');}}}classFoodextendsModel{constTABLE='food';受保护的$table=self::TABLE;publicfunctionproduct(){return$this->morphOne(ShoppingCart::class,'product');}}}根据官方文档:ModelAssociation|《Laravel 5.8 中文文档》|Laravel中国社区。我们的代码应该可以工作,但它可能无法按预期工作。不出意外,看到了“类名必须是有效的对象或字符”的错误信息,源码中也新增了一个$class,在IDE中打开,断点调试。此时$class为1,根据调用栈,一路向上查找,找到了一个有价值的方法。可以看到这里$type取自$this->dictionary,按住Ctrl+click到达属性定义的位置,然后按住Ctrl+click,选择上面的filter赋值操作,可以看到仅对于赋值操作,单击Go。走到赋值的位置后,打断点。看这里的调用栈,源码太多,就不解释了。说说重点吧。看到这个属性,$model->{$this->morphType},首先打印它的值$this->morphType,结果是product_type,然后点击外层的enter按钮,我们在model实例中输入了__get魔术方法。在官方手册中,__get的定义是:__get()会在读取到一个不可访问的属性的值时被调用。首先对于Model来说,没有product_type属性,所以触发,在方法内部调用了getAttribute。看getAttribute方法内部,第321行,使用了一个属性$this->attribute,执行表达式可以看到这是我们的数据结果。根据array_key_exists的判断,可以确定这个if为真,因为下面是||操作,即使后者为假,这个表达式也为真,但我们还是想在这里看看这个方法。这个方法只做了一件事,就是判断一个getter方法是否存在。这里Str::studly()的作用是将字符串从下划线命名规则转换为大驼峰命名规则。换句话说,访问者将在这里被检查。当然我们现在没有这个方法,那我们继续吧。果然第349~351行有这么一段逻辑,那我们回头看看Laravel文档中关于modifiers&accessors的介绍。总之,在访问该字段的值时,我们可以根据getter的规则,定义一个名为getProductTypeAttribute的访问器方法。在这个方法中,我们可以修改它的返回值,作为最终结果返回给访问者。这样我们就可以把accessor中我们原来的product_type的1修改为对应的需要实例化的类名,就这样,我们现在定义一下。publicfunctiongetProductTypeAttribute($val){$map=[1=>Tool::class,2=>Food::class,];返回$map[$val]??Tool::class;}根据文档我们可以了解到,在为已有字段添加访问器时,访问器方法可以接受一个值为原始值的参数。在这个方法中,我们写了一个$map,key是product_type字段的原始值$val,如果这个字段的原始值($val)和对应的key不存在,就会返回App\Models\默认工具模型类。现在够了吗?我们可以试试。果然代码可以运行了,也没有再报错了。此外,在relations属性中,我们还可以看到product是两个不同的模型。接下来,让我们使用toArray来查看结果。结果果然达到了我们的预期,但是我们发现product_type字段的值变成了字符串,而不是原来的数字1和2,怎么办呢?有两种方法。使用getter添加一个辅助字段来存储原始的product_type。遍历重新分配。让我们展示第二种方法。从上面的截图中,我们可以看到查询结果返回了一个Eloquent集合。现在我们使用transform方法来转换原始集合。$list=$cart->with(['product'])->get();$list->transform(function(ShoppingCart$item){$item->product_type_origin=$item->getOriginal('product_type');return$item;});转储($list->toArray());通过模型的getOriginal方法获取原始值。至此,问题已经解决了,那我们是不是可以自定义product、product_type、product_id这三个名称呢?这在Laravel文档中很少提及,答案是肯定的。我们传递了ShoppingCart模型的product方法,这里我们调用morphTo方法而不传递任何值。publicfunctionproduct(){return$this->morphTo();}接下来我们进入morphTo方法一探究竟。首先映入眼帘的是评论。这条注释的大意是,如果不指定$name,那么会从调用栈中取出第一个函数名作为$name,也就是最终挂载模型上的字段名。方法实现如下:protectedfunctionguessBelongsToRelation(){[$one,$two,$caller]=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS,3);return$caller['function'];}然后查看$type和$idprotectedfunctiongetMorphs($name,$type,$id){return[$type?:$name.'_type',$id?:$name.'_id'];}可以看到当我们不给$type和$id的时候,那么默认值分别是$name加上_type,_id后缀。后来补充2019-10-29发现经过以上操作,使用whereHasMorph方法过滤时,type的值变成了getAttribute的值。这时候只需要在关联的模型“Food”和“Tool”中重写getMorphClass,返回值就是类型映射前的值1和2。//ToolpublicfunctiongetMorphClass(){return1;}到此,文章的内容就结束了。本文主要涉及Laravel中关于多态关联和getter两个知识点的理解。本文使用的调试工具是PHPStorm和Xdebug。关于如何配置Xdebug,请点击查看我的另一篇文章。关于如何用好PHPStorm,欢迎查看我的另一篇文章。文中如有错误,还请不吝赐教。如果文章内容涉及到您的兴趣,请与我联系。参考资料php-laravel自定义多态关联类型字段string转int问题?-SegmentFault关于EloquentAssociation中多态关系的思考|Laravel中国社区模型协会|《Laravel 5.8 中文文档》|Laravel中国社区