如何在Symfony的services.yaml中使用PHP-DI定义的服务并绑定事件监听器?
解决PHP-DI容器与Symfony EventDispatcher整合的服务引用问题
我之前在做类似的解耦架构项目时也碰到过一模一样的困扰,毕竟要在非Symfony DI的环境里用它的EventDispatcher确实有点别扭,不过有几个可行的方案能解决这个问题,咱们一个个来看:
方案1:完全绕开services.yaml,手动在PHP-DI中注册监听器
这是最直接且符合你解耦目标的方案——既然services.yaml没法访问PHP-DI的服务,那咱们就直接在PHP-DI的配置里搞定EventDispatcher的监听器绑定。
举个例子,在你的PHP-DI定义文件(比如config/di.php)里:
use Symfony\Component\EventDispatcher\EventDispatcher; use App\Listeners\UserCreatedListener; use App\Listeners\OrderUpdatedListener; return [ // 定义EventDispatcher服务,手动添加PHP-DI容器中的监听器 EventDispatcher::class => function (\Psr\Container\ContainerInterface $container) { $dispatcher = new EventDispatcher(); // 从PHP-DI获取监听器实例,绑定到指定事件 $dispatcher->addListener('user.created', [$container->get(UserCreatedListener::class), 'handle']); $dispatcher->addListener('order.updated', [$container->get(OrderUpdatedListener::class), 'onOrderUpdated'], 10); // 可选设置优先级 return $dispatcher; }, // 其他PHP-DI服务定义... ];
这个方案的好处是完全不需要碰Symfony的services.yaml,所有服务依赖都在PHP-DI体系内,完美契合你和框架解耦的需求。
方案2:用桥接容器让EventDispatcher能访问PHP-DI服务(适合保留services.yaml的场景)
如果你一定要用services.yaml来配置监听器,可以通过一个桥接类让Symfony的ContainerAwareEventDispatcher能读取PHP-DI的服务:
步骤1:实现Symfony容器到PHP-DI的桥接类
namespace App\Bridge; use Psr\Container\ContainerInterface as PsrContainer; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainer; class PhpDiSymfonyBridge implements SymfonyContainer { private $phpDiContainer; public function __construct(PsrContainer $phpDiContainer) { $this->phpDiContainer = $phpDiContainer; } public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) { try { return $this->phpDiContainer->get($id); } catch (\Psr\Container\NotFoundExceptionInterface $e) { if ($invalidBehavior === self::NULL_ON_INVALID_REFERENCE) { return null; } throw new \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException($id, null, $e); } } public function has($id) { return $this->phpDiContainer->has($id); } // 实现Symfony容器接口的其他方法,不需要的可以返回默认值或抛出异常 public function initialized($id) { return true; } public function set($id, $service) { throw new \RuntimeException('PHP-DI桥接容器不支持设置服务'); } }
步骤2:在PHP-DI中配置ContainerAwareEventDispatcher
use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; use App\Bridge\PhpDiSymfonyBridge; return [ ContainerAwareEventDispatcher::class => function (\Psr\Container\ContainerInterface $container) { $bridge = new PhpDiSymfonyBridge($container); return new ContainerAwareEventDispatcher($bridge); }, ];
步骤3:加载services.yaml并注册监听器
你需要用Symfony的DependencyInjection组件解析services.yaml,提取带kernel.event_listener标签的服务,再绑定到Dispatcher:
use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; // 在PHP-DI初始化流程中添加这段代码 $containerBuilder = new ContainerBuilder(); $loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); // 获取所有事件监听器标签 $listenerTags = $containerBuilder->findTaggedServiceIds('kernel.event_listener'); // 从PHP-DI获取Dispatcher并注册监听器 $dispatcher = $phpDiContainer->get(ContainerAwareEventDispatcher::class); foreach ($listenerTags as $serviceId => $tags) { foreach ($tags as $tag) { $event = $tag['event'] ?? 'unknown.event'; $method = $tag['method'] ?? 'on'.ucfirst($event); $priority = $tag['priority'] ?? 0; $dispatcher->addListener($event, [$serviceId, $method], $priority); } }
方案3:用PHP-DI注解自动注册监听器
这个方案更优雅,完全基于PHP-DI生态,不需要依赖Symfony的配置文件:
步骤1:定义自定义监听注解
namespace App\Annotation; use Attribute; #[Attribute(Attribute::TARGET_METHOD)] class ListensTo { public string $event; public int $priority = 0; public function __construct(string $event, int $priority = 0) { $this->event = $event; $this->priority = $priority; } }
步骤2:在监听器类中使用注解
namespace App\Listeners; use App\Annotation\ListensTo; use App\Events\UserCreatedEvent; class UserCreatedListener { #[ListensTo(event: 'user.created', priority: 5)] public function handle(UserCreatedEvent $event) { // 处理用户创建事件 } }
步骤3:在PHP-DI中扫描注解并注册
use Symfony\Component\EventDispatcher\EventDispatcher; use Doctrine\Common\Annotations\AnnotationReader; use App\Annotation\ListensTo; use Symfony\Component\Finder\Finder; return [ EventDispatcher::class => function (\Psr\Container\ContainerInterface $container) { $dispatcher = new EventDispatcher(); $reader = new AnnotationReader(); // 自动扫描src/Listeners目录下的所有类 $finder = new Finder(); $finder->files()->in(__DIR__.'/../src/Listeners')->name('*.php'); foreach ($finder as $file) { $className = 'App\\Listeners\\'.pathinfo($file->getFilename(), PATHINFO_FILENAME); $reflectionClass = new \ReflectionClass($className); foreach ($reflectionClass->getMethods() as $method) { $annotation = $reader->getMethodAnnotation($method, ListensTo::class); if ($annotation instanceof ListensTo) { $listener = $container->get($className); $dispatcher->addListener($annotation->event, [$listener, $method->getName()], $annotation->priority); } } } return $dispatcher; }, ];
方案推荐
如果你的核心目标是和Symfony框架解耦,方案1或方案3是最优选择——它们完全基于PHP-DI,不需要引入额外的Symfony配置依赖,维护起来也更简单。如果因为项目历史原因必须保留services.yaml配置,再考虑方案2。
内容的提问来源于stack exchange,提问作者automatix
相关产品推荐
相关产品推荐

