MadelineProto中EventHandler依赖注入异常问题
代码示例
<?php declare(strict_types=1); namespace App\Service; // ... (省略命名空间导入,以简洁展示) class BasicEventHandler extends SimpleEventHandler { public const ADMIN = '@me'; public function __construct(private \App\Repository\ProviderRepository $providerRepository) {} // ... (省略其他方法,以简洁展示) #[Handler] public function handleMessage(Incoming&Message $message): void { $provider = $this->providerRepository->findOneBy(['id' => 1]); // ... } } BasicEventHandler::startAndLoop('bot.madeline');
问题描述
在Symfony应用中使用BasicEventHandler类时,通过依赖注入引入了ProviderRepository。该仓库在构造函数中可正常访问并已正确实例化,但当收到消息触发handleMessage方法时,出现PHP错误,提示ProviderRepository未初始化,错误信息如下:
Typed property App\Service\BasicEventHandler::$providerRepository must not be accessed before initialization
该问题似乎源于事件处理机制的序列化行为,未保留依赖项的初始化状态。
重现步骤
- 搭建带有依赖注入功能的Symfony应用。
- 定义一个接收
ProviderRepository作为参数的BasicEventHandler服务。 - 通过Symfony的依赖注入机制注入
ProviderRepository。 - 调用
startAndLoop启动BasicEventHandler循环。 - 向机器人发送消息,触发
handleMessage方法。 - 观察到提示
ProviderRepository未初始化的PHP错误。
预期行为
handleMessage方法能够访问已注入并完成初始化的ProviderRepository,以处理收到的消息。
实际行为
抛出错误,提示providerRepository属性在初始化前被访问,似乎该属性未随事件处理器的状态一同进行序列化/反序列化。
额外上下文
- Symfony版本:6.3
danog/MadelineProto版本:8.0@beta- PHP版本:8.1
请问在参与序列化的事件处理器中,处理依赖注入的推荐方案是什么?或者在此场景下处理消息时,是否应采用其他方式获取依赖项?
解决方案
方案一:通过序列化回调处理依赖
MadelineProto在序列化/反序列化对象时支持回调,可重写__sleep和__wakeup方法规避依赖序列化问题:
class BasicEventHandler extends SimpleEventHandler { public const ADMIN = '@me'; private \App\Repository\ProviderRepository $providerRepository; public function __construct(\App\Repository\ProviderRepository $providerRepository) { $this->providerRepository = $providerRepository; } // 序列化时排除无法序列化的依赖 public function __sleep(): array { // 仅序列化必要属性,排除providerRepository return array_diff(array_keys(get_object_vars($this)), ['providerRepository']); } // 反序列化后重新获取依赖 public function __wakeup(): void { // 从Symfony容器中重新获取仓库实例 $this->providerRepository = \Symfony\Component\DependencyInjection\ContainerInterface::getContainer()->get(\App\Repository\ProviderRepository::class); } #[Handler] public function handleMessage(Incoming&Message $message): void { $provider = $this->providerRepository->findOneBy(['id' => 1]); // ... } }
注:若Symfony禁用全局容器访问,需调整获取方式,比如提前注入容器或使用静态服务定位器。
方案二:直接在方法内获取依赖
无需将依赖存在类属性中,处理消息时直接从容器获取仓库实例:
#[Handler] public function handleMessage(Incoming&Message $message): void { $providerRepository = \Symfony\Component\DependencyInjection\ContainerInterface::getContainer()->get(\App\Repository\ProviderRepository::class); $provider = $providerRepository->findOneBy(['id' => 1]); // ... }
该方式避免序列化问题,但代码耦合度略高,适合简单场景。
方案三:利用MadelineProto的DI启动机制
MadelineProto 8.0支持启动时传递依赖,可在Symfony服务中获取依赖后传入:
// 在Symfony服务中获取ProviderRepository实例 $providerRepository = $container->get(\App\Repository\ProviderRepository::class); // 启动时注入依赖 BasicEventHandler::startAndLoop('bot.madeline', $providerRepository); // 修改BasicEventHandler的启动逻辑 class BasicEventHandler extends SimpleEventHandler { public static function startAndLoop(string $session, \App\Repository\ProviderRepository $providerRepository): void { $handler = new self($providerRepository); parent::startAndLoop($session, $handler); } // ... 其余代码不变 }
此方式符合Symfony依赖注入规范,彻底规避序列化问题。
问题根源
MadelineProto会序列化事件处理器对象以保存状态,但Symfony的Repository、EntityManager等服务包含数据库连接等不可序列化资源,序列化时会丢失这些依赖,反序列化后属性处于未初始化状态,触发PHP类型属性错误。
内容的提问来源于stack exchange,提问作者a-kbv

