You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何重构两个存在重复方法的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.08 13:36:03