hyperf父类未被扫描导致子类@inject失效,因依赖注入基于启动时注解扫描而非运行时反射;需确保父类在scan.paths内、含有效注解、非final且属性为protected,否则注入属性为null。

父类没被扫描,子类@Inject就失效
Hyperf 的依赖注入不是靠运行时反射实时解析的,而是启动时扫描注解、生成代理类并缓存。如果父类没被扫描到,它的 @Inject 属性根本不会被容器识别,子类继承后自然也拿不到注入值——哪怕子类自己写了 @Inject,父类的字段仍是 null。
常见错误现象:
- 子类调用父类方法时抛出
TypeError: Call to a member function xxx() on null -
php bin/hyperf.php di:dump输出里找不到父类名 - 父类构造函数能执行,但
@Inject标记的属性始终未赋值
排查要点:
- 确认父类文件在
config/autoload/annotations.php的scan.paths列表中(例如app/Service) - 父类必须带有至少一个有效注解(如
#[Service]、#[Controller]或#[Inject]),否则扫描器会跳过它 - 父类不能是
final类(PHP 限制,Hyperf 无法为其生成代理) - 如果父类是抽象类,需确保它被某个具体子类触发扫描,或手动加
#[Service]显式注册
子类继承了@Inject属性,但运行时报错“Cannot access private property”
Hyperf 的 @Inject 注入只支持 public 和 protected 属性,不支持 private。而 PHP 的继承规则又要求子类无法访问父类的 private 成员——这就导致一个典型冲突:父类把 @Inject 写在 private 属性上,子类既不能覆盖它,也无法在运行时读取它。
正确做法:
- 把父类的注入属性改为
protected:#[Inject] protected UserService $userService; - 避免在父类中使用
private+@Inject组合 - 若必须封装,改用构造函数注入(支持
private参数):public function __construct(#[Inject] protected UserService $service) { ... }
父类用了@Value,子类继承后值为null
@Value 和 @Inject 一样,依赖注解扫描和代理生成。如果父类没被扫描,@Value("app.name") 就不会被解析,属性保持默认值(string 类型为 "",对象类型为 null)。
注意点:
-
@Value不需要类本身有#[Service]等注解,但类仍需出现在扫描路径中,且文件内至少有一个被扫描器识别的注解(哪怕只是#[Inject]) - 配置项路径必须存在,比如
@Value("database.default.host")要求config/autoload/database.php中有'default' => ['host' => '127.0.0.1'] - 修改了
@Value的键名后,必须执行php bin/hyperf.php di:clear && php bin/hyperf.php di:dump,否则旧缓存仍生效
为什么加了@Service的父类,子类还是注入失败?
#[Service] 只保证该类自身被注册进容器,不自动让子类继承其注入逻辑。Hyperf 不会递归扫描父类的属性,它只处理当前类文件中显式声明的注解。
也就是说,即使父类有:
#[Service]
class BaseService
{
#[Inject]
protected UserService $userService;
}
子类也得自己“认领”这个依赖:
#[Service]
class UserOrderService extends BaseService
{
// 这行必须写,否则 $userService 在子类实例中仍是 null
#[Inject]
protected UserService $userService;
}
更稳妥的做法是放弃属性注入,改用构造函数注入——它天然支持继承链传递:
- 父类构造函数接收并保存依赖
- 子类构造函数调用
parent::__construct(...) - 无需重复写
@Inject,也不依赖扫描是否覆盖父类
最易被忽略的一点:Hyperf 的扫描是文件粒度的,不是类继承关系粒度的。它不管你是谁的孩子,只看你这个文件有没有被扫到、里面有没有它认得的注解。











