Laravel 实现条件动作场景的复杂多态多对多关联
Laravel 多态条件/动作关联实现方案
核心规则回顾
- 单个
Method关联多个Condition,单个Condition关联多个Action - 不同业务类型的条件、动作独立建表,通过
conditions、actions两张元表存储多态映射与外键关联 - 原
belongsToMany+wherePivot方案需要为每类条件单独写关联方法,复用性差;自行实现的多态关联因为关系方向定义错误,无法查询到结果
第一步:修正模型关联定义
之前实现的核心错误是把conditions/actions表当成了中间 pivot 表,且多态关联的方向写反:存储*_type和*_id字段的模型,要定义morphTo关联,具体业务模型侧定义反向的morphOne关联。
1. Condition 模型(对应conditions表)
不要继承MorphPivot,直接继承普通Model:
class Condition extends Model { public $timestamps = false; // 关联所属的Method public function method() { return $this->belongsTo(Method::class); } // 多态关联,指向具体的条件类型模型(订单金额、订单重量等) public function conditionable() { return $this->morphTo(__FUNCTION__, 'condition_type', 'condition_id'); } // 关联当前条件下的所有动作 public function actions() { return $this->hasMany(Action::class); } }
2. 具体条件类型模型(以订单金额条件为例,其余条件类型写法完全一致)
class ConditionTypeOrderAmount extends Model { public $timestamps = false; protected $fillable = ['min_amount', 'max_amount']; // 反向关联到Condition元表 public function condition() { return $this->morphOne(Condition::class, 'conditionable', 'condition_type', 'condition_id'); } }
ConditionTypeOrderWeight等其他条件模型,替换对应类名、可写字段即可,无需调整关联逻辑。
3. Action 模型(对应actions表)
逻辑和Condition模型完全对齐:
class Action extends Model { public $timestamps = false; // 关联所属的Condition public function condition() { return $this->belongsTo(Condition::class); } // 多态关联,指向具体的动作类型模型(呼叫主管、发邮件等) public function actionable() { return $this->morphTo(__FUNCTION__, 'action_type', 'action_id'); } }
4. 具体动作类型模型(以呼叫主管动作为例)
class ActionTypeCallSupervisor extends Model { public $timestamps = false; protected $fillable = ['supervisor_id', 'call_anytime']; // 反向关联到Action元表 public function action() { return $this->morphOne(Action::class, 'actionable', 'action_type', 'action_id'); } }
后续新增发邮件等其他动作类型,按相同逻辑新建模型即可,无需修改核心关联代码。
5. Method 模型
class Method extends Model { // 关联所有条件元数据 public function conditions() { return $this->hasMany(Condition::class); } }
第二步:配置自动属性合并,输出期望结构
在Condition模型中添加如下配置,自动预加载关联、合并具体类型的属性,输出时直接得到需要的嵌套结构,不需要额外处理数据:
// 全局预加载条件类型、动作、动作类型,避免N+1问题 protected $with = ['conditionable', 'actions.actionable']; // 追加type字段到输出 protected $appends = ['type']; // 返回当前条件的类型命名空间 public function getTypeAttribute() { return $this->condition_type; } // 序列化时自动合并具体类型属性,移除冗余关联字段 public function toArray() { $data = parent::toArray(); // 合并具体条件的属性 if ($this->conditionable) { $data = array_merge($data, $this->conditionable->toArray()); } // 处理下属动作的属性合并 $data['actions'] = collect($data['actions'])->map(function ($actionItem) { if (isset($actionItem['actionable'])) { $actionItem = array_merge($actionItem, $actionItem['actionable']); $actionItem['type'] = $actionItem['action_type']; // 移除内部关联字段 unset($actionItem['actionable'], $actionItem['action_type'], $actionItem['action_id'], $actionItem['condition_id']); } return $actionItem; })->toArray(); // 移除条件自身的冗余内部字段 unset($data['conditionable'], $data['condition_type'], $data['condition_id'], $data['method_id']); return $data; }
注意:提前修正类名拼写不一致问题,比如示例JSON中写的
ConditionalTypeOrderAmount和表中存储的ConditionTypeOrderAmount命名要统一,否则morphTo会找不到对应类。
第三步:查询调用
直接执行如下查询,就能返回期望结构的JSON数据:
$method = Method::with('conditions')->find(1); return response()->json([ 'method' => $method ]);
后续新增任意类型的条件、动作,只需要创建对应数据表和模型,不需要修改Method、Condition、Action的核心代码,复用性满足要求。
内容的提问来源于stack exchange,提问作者Programmeur
相关产品推荐
相关产品推荐

