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

如何遵守协变/逆变规则同时避免编写重复的PHP业务代码?

解决方案

核心思路是通过泛型约束+运行时校验在不改动原有架构的前提下满足所有要求,所有语法都是PHP原生支持的规范写法,无hack逻辑。

改动点

  • 把BaseBarClass改造为泛型基类,约束关联的Foo子类类型
  • 新增抽象方法要求每个Bar子类返回对应Foo类的完全限定名,替代原有字符串拼接类名的不安全写法
  • 在基类getFooBase中增加运行时类型校验,保证返回结果类型准确
  • 子类getFoo方法仅需做参数处理和类型声明,无需重复公共逻辑

改造后完整代码

class BaseFooClass {
    protected $keys = [];
    private $map = [];
    public function __construct($keyValuePairs) {
        foreach($this->keys as $key => $value) {
            $this->map[$key] = $keyValuePairs[$key] ?? null;
        }
    }
}

class ChildFooClass1 extends BaseFooClass {
    protected $keys = ['foo1_a', 'foo1_b'];
}

class ChildFooClass2 extends BaseFooClass {
    protected $keys = ['foo2_a', 'foo2_b', 'foo2_c'];
}

/**
 * 泛型声明:约束当前Bar类关联的Foo类必须是BaseFooClass的子类
 * @template T of BaseFooClass
 */
abstract class BaseBarClass {
    /**
     * 子类实现,返回对应Foo类的完全限定名
     * @return class-string<T>
     */
    abstract protected function getFooClassName(): string;
    /**
     * 子类实现,返回对应的classIndex
     */
    abstract protected function getClassIndex(): int;

    /**
     * 公共逻辑全部保留在基类,子类无需重复实现
     * @return ?T
     */
    protected function getFooBase(int $dataIndex) : ?BaseFooClass 
    {
        $keyValuePairs = GetRemoteData($this->getClassIndex(), $dataIndex);
        if (checkDataIntegrity($keyValuePairs)) {
            $className = $this->getFooClassName();
            $instance = new $className($keyValuePairs);
            // 运行时类型校验,保证返回结果完全符合类型要求
            if ($instance instanceof $className) {
                return $instance;
            }
        }
        return null;
    }
}

class ChildBarClass1 extends BaseBarClass {
    protected function getFooClassName(): string
    {
        return ChildFooClass1::class;
    }

    protected function getClassIndex(): int
    {
        return 1;
    }

    public function getFoo(int $dataIndex) : ?ChildFooClass1 
    {
        $result = $this->getFooBase($dataIndex);
        // 可选:如果不需要兼容老版本静态分析工具,下面的判断可以省略,加一行@var注解即可
        if ($result instanceof ChildFooClass1 || is_null($result)) {
            return $result;
        }
        throw new \RuntimeException('类型不匹配');
    }
}

class ChildBarClass2 extends BaseBarClass {
    protected function getFooClassName(): string
    {
        return ChildFooClass2::class;
    }

    protected function getClassIndex(): int
    {
        return 2;
    }

    public function getFoo($someInput) : ?ChildFooClass2
    {
        $dataIndex = $this->calculateDataIndex($someInput);
        $result = $this->getFooBase($dataIndex);
        if ($result instanceof ChildFooClass2 || is_null($result)) {
            return $result;
        }
        throw new \RuntimeException('类型不匹配');
    }

    private function calculateDataIndex($someInput): int
    {
        // 原有计算逻辑
        return 0;
    }
}

符合要求的说明

  1. 严格限制返回类型:
    • 静态层面:泛型约束+class-string类型声明,所有主流静态分析工具(PHPStan、Psalm、PHPStorm)都可以准确识别返回类型,不会出现类型不匹配提示
    • 运行时层面:基类和子类都有类型校验,不可能返回不符合声明的实例
  2. 避免重复代码:所有公共逻辑(远程拉取、数据校验、实例化)全部保留在基类getFooBase中,新增逻辑只需要修改基类即可,子类不需要重复实现
  3. 严格遵守协变/逆变规则:基类返回BaseFooClass,子类返回对应的Foo子类,属于合法的返回类型协变,完全符合PHP的类型系统规则,没有任何语法违规

内容的提问来源于stack exchange,提问作者cr001

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 01:36:10