如何重构两个存在重复方法的Eloquent Model类?选抽象类还是Trait?
方案选择:优先使用Trait
核心原因
- 两个类职责完全不同:一个是负责数据读写的实体Model,一个是只读的SQL视图适配Model,不存在「是同一个父类的细分类型」的继承关系,强行抽象父类会违反单一职责和里氏替换原则
- Trait更适合横向复用功能:这种「提供过期状态/提示计算」是独立的能力,后续如果其他和供应商文档无关的Model(比如合同Model、资质Model)需要用到相同的过期计算逻辑,直接引入Trait即可,不需要强行继承同一个父类
- 对现有代码侵入性更低:不需要调整两个类现有的继承链,只需要把重复逻辑抽成Trait引入即可,也不会影响
SupplierDocument原本用的HasFactory、Taskable这些特性
具体实现方式
1. 抽离重复逻辑为Trait
遵循Laravel命名惯例,将Trait放在app/Models/Traits目录下,命名为HasDocumentExpiration:
<?php namespace App\Models\Traits; use Carbon\Carbon; use App\Enums\DocumentStatusEnum; trait HasDocumentExpiration { /** * Document expiration status. * * @return string */ public function getExpirationStatusAttribute() { // 兼容SQL视图预计算字段,有值直接返回提升性能 if (isset($this->attributes['expiration_status'])) { return $this->attributes['expiration_status']; } if (! $this->load('type')->type->is_expirable) { return DocumentStatusEnum::NotExpirable; } if (Carbon::parse($this->expires_at)->isPast()) { return DocumentStatusEnum::Expired; } if (Carbon::parse($this->expires_at)->diffInDays(now()) <= $this->type->expiration_window) { return DocumentStatusEnum::ExpiringSoon; } return DocumentStatusEnum::Active; } /** * Document days till expiration. * * @return int|null */ public function getDaysTillExpiryAttribute() { // 兼容SQL视图预计算字段 if (isset($this->attributes['days_till_expiry'])) { return $this->attributes['days_till_expiry']; } if ($this->expires_at) { return Carbon::parse($this->expires_at)->diffInDays(Carbon::now()->toDateString()); } return null; } /** * Document expiration message. * * @return string */ public function getExpirationMessageAttribute() { if ($this->expiration_status === DocumentStatusEnum::ExpiringSoon) { if ($this->days_till_expiry > 1) { return "Expiring in $this->days_till_expiry days"; } elseif ($this->days_till_expiry === 1) { return 'Expiring in 1 day'; } else { return 'Expires today'; } } if ($this->expiration_status === DocumentStatusEnum::Expired) { return "Expired $this->days_till_expiry days ago"; } elseif ($this->expiration_status === DocumentStatusEnum::NotExpirable) { return DocumentStatusEnum::NotExpirable; } return DocumentStatusEnum::Active; } }
2. 改造原有类
删除两个类中重复的访问器方法,引入Trait即可:
SupplierDocument 改造后
<?php class SupplierDocument extends Model { use HasFactory, Taskable, HasDocumentExpiration; protected $appends = ['days_till_expiry', 'expiration_status', 'expiration_message']; public function owner() { return $this->belongsTo(User::class, 'owner_id'); } public function facilities() { return $this->belongsToMany(Facility::class, 'supplier_document_facilities'); } }
SupplierDocumentView 改造后
<?php class SupplierDocumentView extends Model { use HasDocumentExpiration; protected $appends = ['expiration_message']; protected $table = 'supplier_documents_view'; public function owner() { return $this->belongsTo(User::class, 'owner_id'); } public function facilities() { return $this->belongsToMany(Facility::class, 'supplier_document_facilities', 'supplier_document_id'); } }
命名建议
- Trait命名:如果该逻辑仅用于供应商文档相关模型,保持
HasDocumentExpiration即可;如果后续要复用到其他模块的过期逻辑,可以改为更通用的HasExpirableAttribute - 模型命名:你当前的命名已经符合规范,
SupplierDocument对应操作真实业务表的可写模型,SupplierDocumentView对应SQL视图的只读模型,辨识度高无需调整
内容的提问来源于stack exchange,提问作者James Moore
相关产品推荐
相关产品推荐

