
本文讲解如何在 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
以下是一个生产就绪的实现范例:
*/
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
}
} ✅ 优势总结
立即学习“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 生态中泛型思维的务实落地之道。











