PHP 中实现类型安全的泛型容器:DRY 原则与静态类型模拟指南

PHP 中实现类型安全的泛型容器:DRY 原则与静态类型模拟指南

本文讲解如何在 php(无原生泛型支持)中通过 psalm 模板注解(`@template`)模拟泛型容器,兼顾 dry 原则与类型专用性,避免继承导致的 lsp 违反,并提升 ide 提示与静态分析准确性。

在 PHP 开发中,我们常需构建多种语义明确的“专用容器”——如 CookieBag、CandyBag 或 ConfigBag——它们共享相同的基础操作(增、删、查、过滤),但又要求对存储值进行严格的类型约束。若采用传统继承方式(如让 CookieBag extends AbstractBag 并重写 get()/set() 的参数与返回类型),虽看似直观,实则违反里氏替换原则(LSP):父类 AbstractBag 声明接受任意 mixed 类型,子类却强制限定为 Cookie,导致依赖 BagInterface 的通用代码(如 GrandMa::giveCookie())在传入非 CookieBag 实例时发生运行时错误,且无法被类型系统提前捕获。

根本出路:放弃“继承特化”,转向“模板化泛化”
PHP 虽不支持原生泛型(如 GenericBag),但可通过行业标准的 PHPDoc 模板注解(@template)配合静态分析工具(如 Psalm、PHPStan)实现等效效果。这种方式不改变运行时行为,却能为开发者提供强类型提示、方法签名一致性保障和编译期错误预警。

以下是一个生产就绪的实现范例:

 */
    private array $bag = [];

    public function has(string $key): bool
    {
        return array_key_exists($key, $this->bag);
    }

    /**
     * @param string $key
     * @param T|null $fallback
     * @return T
     */
    public function get(string $key, $fallback = null)
    {
        return $this->has($key) ? $this->bag[$key] : $fallback;
    }

    /**
     * @param string $key
     * @param T $value
     * @return static
     */
    public function set(string $key, $value): self
    {
        $this->bag[$key] = $value;
        return $this;
    }

    /**
     * @param string $key
     * @return void
     */
    public function del(string $key): void
    {
        unset($this->bag[$key]);
    }

    /**
     * @return array
     */
    public function all(): array
    {
        return $this->bag;
    }

    /**
     * @param callable(mixed, string): bool $callback
     * @return array
     */
    public function filter(callable $callback): array
    {
        return array_filter($this->bag, $callback, ARRAY_FILTER_USE_BOTH);
    }
}

使用时,无需创建子类,而是直接实例化并用 PHPDoc 明确类型参数:

 $cookieBag */
$cookieBag = new GenericBag();

// 此处 IDE 和 Psalm 将校验:只能存 Cookie 对象,返回值为 ?Cookie
$cookieBag->set('session', new Cookie('PHPSESSID', 'abc123'));
$cookie = $cookieBag->get('session'); // $cookie: ?Cookie

// 同理,CandyBag 复用同一类,零重复逻辑
/** @var GenericBag $candyBag */
$candyBag = new GenericBag();
$candyBag->set('choco', new Candy('Dark Chocolate'));

// 在依赖注入场景中精准声明类型
class GrandMa
{
    /**
     * @param GenericBag $bag
     * @return void
     */
    public function giveCookie(GenericBag $bag): void
    {
        $bag->set('gift', new Cookie('grandma-cookie', 'yum')); // ✅ 类型安全
        // $bag->set('oops', new DateTime()); // ❌ Psalm 报错:Expected Cookie, got DateTime
    }
}

优势总结

微信 WeLM

微信 WeLM

WeLM不是一个直接的对话机器人,而是一个补全用户输入信息的生成模型。

下载

立即学习PHP免费学习笔记(深入)”;

  • 真正 DRY:所有逻辑集中于 GenericBag,无重复方法体;
  • 类型安全:借助 @template + @var / @param 注解,实现接近泛型的开发体验;
  • LSP 兼容:GenericBag 是独立类型,不破坏接口契约;
  • 工具链友好:Psalm、PHPStan、IntelliJ/PhpStorm 均可识别并提供补全、跳转与错误检查;
  • 零运行时开销:注解仅用于静态分析,不参与执行。

⚠️ 注意事项

  • 确保项目已配置 Psalm(推荐 psalm.xml)或 PHPStan,并启用模板支持;
  • @template 必须声明在类级别,方法中通过 @param T / @return T 引用;
  • 数组键类型默认为 string,若需支持整数键,可扩展为 @template K of string|int;
  • 避免在 @var 注解中省略泛型参数(如 @var GenericBag $bag),否则将丢失类型特化能力。

通过这种模式,你既拥抱了 PHP 的动态本质,又借力现代工具链获得了强类型语言的严谨性与可维护性——这才是 PHP 生态中泛型思维的务实落地之道。

https://www.php.cn/faq/2034373.html

发表回复

Your email address will not be published. Required fields are marked *