logo

额外区块类型 (EBT) - 全新的布局构建器体验❗

额外区块类型 (EBT) - 样式化、可定制的区块类型:幻灯片、标签页、卡片、手风琴等更多类型。内置背景、DOM Box、JavaScript 插件的设置。立即体验布局构建的未来。

演示 EBT 模块 下载 EBT 模块

❗额外段落类型 (EPT) - 全新的 Paragraphs 体验

额外段落类型 (EPT) - 类似的基于 Paragraph 的模块集合。

演示 EPT 模块 滚动

滚动

事件订阅器 (Event Subscriber) 与 事件调度器 (Event Dispatcher)。Drupal 的事件系统。

03/10/2025, by Ivan

事件系统概览

事件系统在许多复杂的应用程序中被用作允许扩展修改系统工作的方式。事件系统可以通过不同方式实现,但总体上组成系统的概念和组件是相同的。

  • 事件订阅器 (Event Subscribers) —— 有时称为“监听器”,是可以被调用的方法或函数,它们会对在整个事件注册表中传播的事件作出响应。
  • 事件注册表 (Event Registry) —— 用于收集和排序事件订阅器。
  • 事件调度器 (Event Dispatcher) —— 触发或“分发”事件到整个系统的机制。
  • 事件上下文 (Event Context) —— 许多事件需要一个特定的数据集,这对事件订阅器很重要。这可以是一个简单的值,也可以是一个复杂的对象,例如专门为事件创建的包含相关数据的类。

Drupal 钩子 (Hooks)

在其大部分发展历程中,Drupal 通过“钩子”拥有一种原始的事件系统。让我们看看“钩子”的概念如何映射到事件系统的 4 个元素。

  • 事件订阅器 —— Drupal 的钩子通过定义特定名称的函数来注册。例如,如果你想订阅一个名为 “hook_my_event_name” 的事件,你必须定义一个函数 myprefix_my_event_name(),其中 “myprefix” 是你的模块或主题的名称。
  • 事件注册表 —— Drupal 钩子存储在标识符为 “module_implements” 的 “cache_bootstrap” 缓存中。它只是一个实现该钩子的模块数组。
  • 事件调度器 —— 钩子在 Drupal 7 和 Drupal 8 中的分发方式不同:

1) Drupal 7:使用 module_invoke_all()
2) Drupal 8:通过服务方法 \Drupal::moduleHandler()->invokeAll()。

  • 事件上下文 —— 上下文通过参数传递给订阅器。例如,以下分发会调用所有 “hook_my_event_name” 的实现并传递 $some_arbitrary_parameter:

1) Drupal 7:module_invoke_all('my_event_name', $some_arbitrary_parameter);
2) Drupal 8:\Drupal::moduleHandler()->invokeAll('my_event_name', [$some_arbitrary_parameter]);

钩子方法的一些缺点:

  • 事件仅在缓存重建时注册。
  • 每个模块只能对同一事件作出一次响应。
  • 很难定义事件执行顺序。

在 Drupal 8 中引入 Symfony 之后,有了另一套事件系统,在大多数情况下更好。虽然 Drupal 8 核心中没有太多事件分发,但许多模块已经开始使用该系统。

Drupal 8 事件

Drupal 8 事件与 Symfony 的事件非常相似。让我们看看它们如何映射到事件系统的各个组件。

  • 事件订阅器 —— 实现 \Symfony\Component\EventDispatcher\EventSubscriberInterface 的类。
  • 事件调度器 —— 实现 \Symfony\Component\EventDispatcher\EventDispatcherInterface 的类。至少会有一个作为服务提供的事件调度器实例。
  • 事件注册表 —— 存储在事件调度器对象中,以事件名和优先级为键值。
  • 事件上下文 —— 继承自 \Symfony\Component\EventDispatcher\Event 的类。每个扩展通常都会为其自定义事件创建新的 Event 类。

学习使用 Drupal 8 的事件将帮助你更好地理解自定义模块开发,并为未来(事件将逐渐取代钩子)做好准备。

我的第一个 Drupal 8 事件订阅器

我们将创建一个事件订阅器,它会在配置对象保存或删除时向用户显示消息。

name: Custom Events
type: module
description: Custom/Example event work.
core: 8.x
package: Custom

接下来,我们在 custom_events.services.yml 中注册一个订阅器:

services:
  my_config_events_subscriber:
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
    tags:
      - { name: 'event_subscriber' }

然后编写事件订阅器类:

namespace Drupal\custom_events\EventSubscriber;

use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ConfigEventsSubscriber implements EventSubscriberInterface {
  public static function getSubscribedEvents() {
    return [
      ConfigEvents::SAVE => 'configSave',
      ConfigEvents::DELETE => 'configDelete',
    ];
  }

  public function configSave(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    \Drupal::messenger()->addStatus('Saved config: ' . $config->getName());
  }

  public function configDelete(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    \Drupal::messenger()->addStatus('Deleted config: ' . $config->getName());
  }
}

当配置保存或删除时,你会看到提示信息。

我的第一个 Drupal 8 自定义事件与分发

我们将创建一个用户登录事件 UserLoginEvent。

class UserLoginEvent extends Event {
  const EVENT_NAME = 'custom_events_user_login';
  public $account;
  public function __construct(UserInterface $account) {
    $this->account = $account;
  }
}

在 custom_events.module 的 hook_user_login 中分发它:

function custom_events_user_login($account) {
  $event = new UserLoginEvent($account);
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);
}

然后编写 UserLoginSubscriber 订阅该事件。

事件订阅器优先级

在 Drupal 8 中,可以为事件订阅器设置优先级,而不必依赖模块的“权重”。

在 getSubscribedEvents() 中返回值时,可以使用数组指定优先级:

public static function getSubscribedEvents() {
  return [
    ConfigEvents::SAVE => ['configSave', 100],
    ConfigEvents::DELETE => ['configDelete', -100],
  ];
}

这样,高优先级监听器会更早执行,低优先级监听器会更晚执行。

提示:如果没有指定优先级,默认值是 0。

参考资料: