hyperf 不支持 php 层面泛型注入,因运行时泛型信息被擦除;需通过工厂+显式类型标识+容器绑定模拟,如 cacheproxyfactory 根据 entityclass 返回对应实例。

Hyperf 原生不支持 PHP 层面的泛型类型参数注入(如 T、Repository<user></user>),因为 PHP 在运行时会擦除泛型信息,反射无法获取实际的模板类型。所谓“泛型注入”,本质是**用工厂 + 显式类型标识 + 容器绑定规则**模拟行为,不是语言级泛型。
为什么 @Inject 不能直接注入带泛型的类
PHP 的类型注解(包括 DocBlock 中的 @var Repository<user></user> 或构造函数参数类型 Repository $repo)在反射时只会返回 Repository,<user></user> 部分被完全丢弃。Hyperf 的 DI 容器依赖反射解析类型,因此:
-
private UserRepository $userRepo→ 容器能识别并注入UserRepository实例 -
private Repository<user> $repo</user>→ 反射只看到Repository,容器不知道该绑哪个具体实现,大概率抛出Entry "Repository" does not exist - 即使你手动
$container->bind(Repository::class, UserRepository::class),所有Repository请求都会返回UserRepository,丧失类型区分能力
用 factory 绑定 + 类型标识参数实现“泛型”效果
核心思路:不靠反射猜类型,而是把“类型意图”显式传入工厂函数,由工厂决定返回哪个具体实例。常见做法是用字符串标识或接口常量作为键。
例如实现一个通用的缓存代理 CacheProxy<t></t>,需根据目标模型动态选择序列化策略和 key 前缀:
// app/Cache/CacheProxyFactory.php
use Hyperf\Contract\ContainerInterface;
use App\Cache\RedisCacheProxy;
use App\Cache\MemoryCacheProxy;
class CacheProxyFactory
{
public function __invoke(ContainerInterface $container, string $entityClass): CacheProxyInterface
{
return match ($entityClass) {
User::class => new RedisCacheProxy($container->get(Redis::class), 'user:'),
Order::class => new RedisCacheProxy($container->get(Redis::class), 'order:'),
Config::class => new MemoryCacheProxy(),
default => throw new InvalidArgumentException("No cache proxy for {$entityClass}"),
};
}
}
然后在 dependencies.php 中绑定:
return [
CacheProxyInterface::class => \App\Cache\CacheProxyFactory::class,
];
使用时显式传参:
// Controller 或 Service 中
public function __construct(
private ContainerInterface $container
) {}
public function getUser(int $id)
{
$cache = $this->container->get(CacheProxyInterface::class, [User::class]);
return $cache->get("{$id}");
}
- 关键点:
$container->get()第二个参数是数组,会透传给工厂的__invoke方法 - 避免用
@Inject,因为注解无法携带运行时参数 - 接口
CacheProxyInterface是统一契约,不同实体走不同实现,逻辑隔离清晰
进阶:用 DefinitionSource 支持更复杂的类型推导
如果硬要让 @Inject 看起来像泛型(比如写 private CacheProxy<user> $userCache</user>),可借助 DefinitionSource 拦截解析过程,从类名中提取泛型实参。
步骤如下:
- 定义一个命名规范,如
UserCacheProxy表示CacheProxy<user></user> - 实现
Hyperf\Di\Definition\DefinitionInterface,在resolve()中解析类名前缀 - 在
ConfigProvider::getDefinitions()中注册该 Definition,匹配*CacheProxy类 - 内部仍调用工厂,只是把
User从类名中抠出来,转成User::class传入
但要注意:
- 这属于“约定大于配置”的黑魔法,可读性和维护性下降
- IDE 无法跳转、静态分析失效
- 一旦类名重构(如改成
UserRedisCache),逻辑就断了 - 真正需要多态的场景,建议直接上策略模式或工厂方法,比模拟泛型更稳
最易忽略的一点:协程上下文里别把泛型工厂结果存在单例属性中。比如 CacheProxyFactory 返回的实例若缓存了 $entityClass,又没做协程隔离,下一个请求拿过来可能还是上一个用户的 User::class —— 这类状态必须靠 Context::set() 或方法参数传递,不能赖在对象属性上。











