logo

パレット - カラフルに🎨

Palette — ビジュアルページビルダー、デザインの専門知識は不要です。

ライブデモ パレットをダウンロード

Scroll

Event SubscriberとEvent Dispatcher。Drupalのイベント処理システム。

26/04/2020, by maria

Menu

イベントシステムの概要

イベントシステムは、拡張機能がシステムの動作を変更できるようにする方法として、多くの複雑なアプリケーションで使用されています。イベントシステムはさまざまな方法で実装できますが、一般的に、システムを構成する概念とコンポーネントは同じです。

  • イベントサブスクライバー(Event Subscribers) - 「リスナー」とも呼ばれ、イベントレジストリ全体にディスパッチされるイベントに応答する呼び出し可能なメソッドまたは関数です。
  • イベントレジストリ(Event Registry) - イベントサブスクライバーが収集され、ソートされる場所です。
  • イベントディスパッチャー(Event Dispatcher) - イベントが開始され、システム全体に「ディスパッチ」されるメカニズムです。
  • イベントコンテキスト(Event Context) - 多くのイベントには、イベントサブスクライバーにとって重要な特定のデータセットが必要です。これは、イベントサブスクライバーに渡される単純な値の場合もあれば、関連データを含む特別に作成されたクラスのような複雑なものの場合もあります。

 

Drupalフック(Hooks)

Drupalはその存在のほとんどを通じて、「フック」による原始的なイベントシステムを持っていました。「フック」の概念がイベントシステムのこれら4つの要素にどのように分解されるかを見てみましょう。

  • イベントサブスクライバー - Drupalフックは、特定の名前を持つ関数を定義することでシステムに登録されます。たとえば、「hook_my_event_name」という準備されたイベントにサブスクライブするには、myprefix_my_event_name()という名前の新しい関数を定義する必要があります。ここで「myprefix」はモジュールまたはテーマの名前です。

 

  • イベントレジストリ - Drupalフックは「cache_bootstrap」バケットに「module_implements」識別子で保存されます。これは、フック自体の名前で示されるフックを実装するモジュールの配列にすぎません。

 

  • イベントディスパッチャー - フックは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は特定のキャッシュを構築するときにのみ新しいフックを探します。つまり、サイトに新しいフックを実装したい場合、実装するフックに応じてさまざまなキャッシュを再構築する必要があります。

  • 各イベントにモジュールごとに1回しか応答できません。

これらのイベントは非常に特定の関数名を定義することで実装されるため、各モジュールまたはテーマにイベントの実装は1つしか存在できません。これは他のイベントシステムと比較して恣意的な制限です。

  • イベントの順序を簡単に決定できません。

Drupalは、より大きなシステムで重み付けされたモジュールの順序を使用して、イベントサブスクライバーの順序を決定します。Drupalのモジュールとテーマはシステム内で「ウェイト」を持っています。この「ウェイト」はモジュールの読み込み順序を決定し、したがって、注文イベントがサブスクライバーにディスパッチされます。この問題の回避策は、Drupal 7で後から「hook_module_implements_alter」によって追加されました。これは、モジュールのウェイトを変更せずにフックの実行順序を変更したい場合にモジュールがサブスクライブする必要がある2番目のイベントです。

Drupal 8のSymfony基盤により、現在は別のイベントシステムが存在します。ほとんどの場合、より優れたイベントシステムです。Drupal 8コアでは多くのイベントがディスパッチされませんが、多くのモジュールがこのシステムを使い始めています。

Drupal 8のイベント

Drupal 8のイベントはSymfonyのイベントに非常に似ています。これがイベントシステムのコンポーネントリストにどのように分解されるかを見てみましょう。

  • イベントサブスクライバー - \Symfony\Component\EventDispatcher\EventSubscriberInterfaceを実装するクラスです。
  • イベントディスパッチャー - \Symfony\Component\EventDispatcher\EventDispatcherInterfaceを実装するクラスです。通常、少なくとも1つのイベントディスパッチャーインスタンスがサービスクラスとしてシステムに提供されますが、必要に応じて他のディスパッチャーを作成できます。
  • イベントレジストリ。サブスクライバーのレジストリは、イベント名とイベントの優先度(順序)を含む配列として「イベントディスパッチャー」オブジェクトに保存されます。イベントがサービスとして登録されると(例を参照)、そのイベントはグローバルにアクセス可能なディスパッチャーに登録されます。
  • イベントコンテキスト - \Symfony\Component\EventDispatcher\Eventクラスを拡張するクラスです。通常、独自のイベントをディスパッチする各拡張機能は、サブスクライバーに必要な関連データを含む新しいタイプのEventクラスを作成します。

Drupal 8のイベントの使用方法を学ぶことは、カスタムモジュールでの開発をより深く理解するのに役立ち、イベントが(願わくば)フックに取って代わる未来に備えることになります。それでは、Drupal 8でこれらの各イベントコンポーネントを使用する方法を示すカスタムモジュールを作成しましょう。

Drupal 8の最初のイベントサブスクライバー

いくつかの基本イベントを使用して、Drupal 8で最初のイベントサブスクライバーを作成しましょう。最初は非常に簡単なものを作るのが個人的に好きなので、Configオブジェクトが保存または削除されたときにユーザーにメッセージを表示するイベントサブスクライバーを作成します。

まず必要なのは、作業を行うモジュールです。私はcustom_eventsと名付けました。

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

次のステップでは、Drupalに新しいイベントサブスクライバーを登録します。そのためには、custom_events.services.ymlを作成する必要があります。Drupal 7から来ていてフックシステムに詳しい場合は、このステップをモジュールまたはテーマに「hook_my_event_name」関数を書くのと同じように考えることができます。

services:
  # Name of this service.
  my_config_events_subscriber:
    # Event subscriber class that will listen for the events.
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
    # Tagged as an event_subscriber to register this subscriber with the event_dispatch service.
    tags:
      - { name: 'event_subscriber' }

これはかなり単純ですが、少し詳しく見てみましょう。

1) 「my_config_events_subscriber」という名前の新しいサービスを定義します
2) その「class」プロパティを、作成する新しいPHPクラスのグローバル名に設定します
3) 「tags」プロパティを定義し、「event_subscriber」という名前のタグを提供します。これが、サービスがイベントサブスクライバーとしてシステムに登録される方法です。

あるいは、イベントサブスクライバーのPHPクラス(バックスラッシュなし)をサービス名として使用し、「class」プロパティを省略することもできます。例:

services:
  # Name of this service, using the event subscriber class that will listen for the events.
  Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber:
    tags:
      - { name: 'event_subscriber' }

次に必要なのは、イベントサブスクライバークラスを書くことだけです。このクラスには、満たしたいいくつかの要件があります:

1. EventSubscriberInterfaceクラスを実装する必要があります。
2. 配列を返すgetSubscribedEvents()メソッドが必要です。配列のキーはサブスクライブするイベント名になり、これらのキーの値はイベントサブスクライバーオブジェクトのメソッド名になります。

これがイベントサブスクライバークラスです。ConfigEventsクラスのイベントにサブスクライブし、各イベントに対してローカルメソッドを実行します。

src/EventSubscriber/ConfigEventsSubscriber.php
<?php

namespace Drupal\custom_events\EventSubscriber;

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

/**
 * Class EntityTypeSubscriber.
 *
 * @package Drupal\custom_events\EventSubscriber
 */
class ConfigEventsSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   *
   * @return array
   *   The event names to listen for, and the methods that should be executed.
   */
  public static function getSubscribedEvents() {
    return [
      ConfigEvents::SAVE => 'configSave',
      ConfigEvents::DELETE => 'configDelete',
    ];
  }

  /**
   * React to a config object being saved.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configSave(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    \Drupal::messenger()->addStatus('Saved config: ' . $config->getName());
  }

  /**
   * React to a config object being deleted.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configDelete(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    \Drupal::messenger()->addStatus('Deleted config: ' . $config->getName());
  }

}

これで完了です!かなり単純に見えますが、重要な点に注意しながら見ていきましょう:

  • EventSubscriberInterfaceクラスを実装します。
  • getSubscribedEvents()メソッドを実装します。このメソッドは、イベント => メソッド名の名前/値ペアの配列を返します。
  • configSave()とconfigDelete()の両方で、ConfigCrudEvent型のオブジェクトを期待します。このオブジェクトには、そのイベントのConfigオブジェクトを返すgetConfig()メソッドがあります。

そして、鋭い観察者なら抱くかもしれないいくつかの質問:

  • ConfigEvents::SAVEとは何か、それはどこから来たのか?

通常、新しいイベントを定義するとき、値がイベント名であるグローバルにアクセス可能な定数を作成します。この場合、\Drupal\Core\Config\ConfigEventsには定数SAVEがあり、その値は「config.save」です。

  • なぜConfigCrudEventオブジェクトを期待したのか、どうやってそれを知ったのか?

新しいイベントを定義するときは、イベントに固有で、必要なデータを含み、そのデータのためのシンプルなAPIを持つ新しいタイプのオブジェクトを作成することも慣例です。現時点では、コードベースと公開APIドキュメントを調べることで、期待されるイベントオブジェクトを最もよく判断できます。

モジュールを有効化してこのイベントをテストする準備ができたと思います。期待することは、Drupalが設定オブジェクトを保存または削除するたびに、設定オブジェクトの名前を含むメッセージが表示されるはずだということです。

設定オブジェクトはDrupal 8で非常に一般的であるため、これは試すのがかなり簡単です。ほとんどのモジュールは設定オブジェクトで設定を管理しているため、モジュールをインストールしてアンインストールするだけで、インストール中にどの設定オブジェクトを保存し、アンインストール中に削除するかを確認できます。

1. 「custom_events」モジュールを自分でインストールします。
2. 「統計」モジュールをインストールします

events-1-installed

統計モジュールのインストール後のメッセージ。

 
2つの設定オブジェクトが保存されたようです!最初は、インストールされたモジュールとテーマを管理するcore.extension設定オブジェクトです。次に、statistics.settings設定オブジェクトです。

3. 「統計」モジュールをアンインストールします

events-2-uninstalled

統計モジュールのアンインストール後のメッセージ。

今回はSAVEとDELETEの両方のイベントが見えます。statistics.settings設定オブジェクトが削除され、core.extension設定オブジェクトが保存されたことがわかります。

これは成功と言えるでしょう!2つの主要なDrupalイベントへのサブスクライブに成功しました。

次に、独自のイベントを作成し、他のモジュールが使用できるようにディスパッチする方法を見てみましょう。

Drupal 8の最初のイベントとイベントのディスパッチ

最初に決定する必要があるのは、どのタイプのイベントをいつディスパッチするかです。コアにまだイベントがないDrupalフック「hook_user_login」用のイベントを作成します。

Eventを拡張する新しいクラスを作成することから始めましょう。新しいクラスをUserLoginEventと名付けます。また、サブスクライバーがグローバルにアクセスできるイベント名を提供することも確認しましょう。

src/Event/UserLoginEvent.php
<?php

namespace Drupal\custom_events\Event;

use Drupal\user\UserInterface;
use Symfony\Component\EventDispatcher\Event;

/**
 * Event that is fired when a user logs in.
 */
class UserLoginEvent extends Event {

  const EVENT_NAME = 'custom_events_user_login';

  /**
   * The user account.
   *
   * @var \Drupal\user\UserInterface
   */
  public $account;

  /**
   * Constructs the object.
   *
   * @param \Drupal\user\UserInterface $account
   *   The account of the user logged in.
   */
  public function __construct(UserInterface $account) {
    $this->account = $account;
  }

}
  • UserLoginEvent::EVENT_NAMEは「custom_events_user_login」という値の定数です。これは新しいカスタムイベントの名前です。
  • このイベントのコンストラクターはUserInterfaceオブジェクトを期待し、それをイベントのプロパティとして保存します。これにより、$accountオブジェクトがこのイベントのサブスクライバーに利用可能になります。

これで終わりです!

次に、新しいイベントをディスパッチするだけです。「hook_user_login」中に行います。custom_events.moduleの作成から始めます。

<?php

/**
 * @file
 * Contains custom_events.module.
 */

use Drupal\custom_events\Event\UserLoginEvent;

/**
 * Implements hook_user_login().
 */
function custom_events_user_login($account) {
  // Instantiate our event.
  $event = new UserLoginEvent($account);

  // Get the event_dispatcher service and dispatch the event.
  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);
}

「hook_user_login」の実装内では、新しいイベントをディスパッチするためにいくつかのことを行うだけです:

1. UserLoginEventという名前の新しいカスタムイベントオブジェクトを作成し、そのコンストラクターにフックで利用可能な$accountオブジェクトを提供します。
2. event_dispatcherサービスを取得します。
3. event_dispatcherサービスでdispatch()メソッドを実行します。ディスパッチするイベントの名前(UserLoginEvent::EVENT_NAME)と、作成したばかりのイベントオブジェクト($event)を指定します。

これで完了です!ユーザーがDrupalにログインするときにカスタムイベントをディスパッチします。

次に、新しいイベント用のイベントサブスクライバーを作成して、例を完成させましょう。まず、services.ymlファイルを更新して、これから書くイベントサブスクライバーを含める必要があります。

services:
  # Name of this service.
  my_config_events_subscriber:
    # Event subscriber class that will listen for the events.
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
    # Tagged as an event_subscriber to register this subscriber with the event_dispatch service.
    tags:
      - { name: 'event_subscriber' }

  # Subscriber to the event we dispatch in hook_user_login.
  custom_events_user_login:
    class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber'
    tags:
      - { name: 'event_subscriber' }

以前と同じです。新しいサービスを定義し、それをevent_subscriberとしてタグ付けします。次に、このEventSubscriberクラスを書く必要があります。

src/EventSubscriber/UserLoginSubscriber.php
<?php

namespace Drupal\custom_events\EventSubscriber;

use Drupal\custom_events\Event\UserLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class UserLoginSubscriber.
 *
 * @package Drupal\custom_events\EventSubscriber
 */
class UserLoginSubscriber implements EventSubscriberInterface {

  /**
   * Database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * Date formatter.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      // Static class constant => method on this class.
      UserLoginEvent::EVENT_NAME => 'onUserLogin',
    ];
  }

  /**
   * Subscribe to the user login event dispatched.
   *
   * @param \Drupal\custom_events\Event\UserLoginEvent $event
   *   Dat event object yo.
   */
  public function onUserLogin(UserLoginEvent $event) {
    $database = \Drupal::database();
    $dateFormatter = \Drupal::service('date.formatter');

    $account_created = $database->select('users_field_data', 'ud')
      ->fields('ud', ['created'])
      ->condition('ud.uid', $event->account->id())
      ->execute()
      ->fetchField();

    \Drupal::messenger()->addStatus(t('Welcome, your account was created on %created_date.', [
      '%created_date' => $dateFormatter->format($account_created, 'short'),
    ]));
  }

}

説明:

1. onUserLogin()メソッド(作成したメソッド名)を使用して、UserLoginEvent::EVENT_NAMEという名前のイベントにサブスクライブします。
2. onUserLogin中に、$eventオブジェクトの$accountプロパティ(ログインしたばかりのユーザー)にアクセスし、それで何かを行います。
3. ユーザーがログインすると、サイトに参加した日時を示すメッセージが表示されるはずです。

events-3-login

ログイン後のメッセージ。

出来上がり!新しいカスタムイベントのディスパッチと、そのイベントへのサブスクライブの両方ができました。私たちは素晴らしいです!

イベントサブスクライバーの優先度

「イベント」システムのもう1つの素晴らしい機能は、サブスクライバーがモジュール全体の実行ウェイトを変更したり、別のフックを使用して優先度を変更したりする(フックの場合のように)代わりに、サブスクライバー自体の内部で独自の優先度を設定できることです。

これは非常に簡単ですが、これを最もよく示すには、既にサブスクライバーがいる場所に別のイベントサブスクライバーを書く必要があります。「AnotherConfigEventSubscriber」を書き、そのリスナーの優先度を設定しましょう。

まず、services.ymlファイルに新しいイベントサブスクライバーを登録します:

services:
  # Name of this service.
  my_config_events_subscriber:
    # Event subscriber class that will listen for the events.
    class: '\Drupal\custom_events\EventSubscriber\ConfigEventsSubscriber'
    # Tagged as an event_subscriber to register this subscriber with the event_dispatch service.
    tags:
      - { name: 'event_subscriber' }

  # Subscriber to the event we dispatch in hook_user_login.
  custom_events_user_login:
    class: '\Drupal\custom_events\EventSubscriber\UserLoginSubscriber'
    tags:
      - { name: 'event_subscriber' }

  another_config_events_subscriber:
    class: '\Drupal\custom_events\EventSubscriber\AnotherConfigEventsSubscriber'
    tags:
      - { name: 'event_subscriber' }

次に、AnotherConfigEventSubscriber.phpを書きます:

<?php

namespace Drupal\custom_events\EventSubscriber;

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

/**
 * Class EntityTypeSubscriber.
 *
 * @package Drupal\custom_events\EventSubscriber
 */
class AnotherConfigEventsSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   *
   * @return array
   *   The event names to listen for, and the methods that should be executed.
   */
  public static function getSubscribedEvents() {
    return [
      ConfigEvents::SAVE => ['configSave', 100],
      ConfigEvents::DELETE => ['configDelete', -100],
    ];
  }

  /**
   * React to a config object being saved.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configSave(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    \Drupal::messenger()->addStatus('(Another) Saved config: ' . $config->getName());
  }

  /**
   * React to a config object being deleted.
   *
   * @param \Drupal\Core\Config\ConfigCrudEvent $event
   *   Config crud event.
   */
  public function configDelete(ConfigCrudEvent $event) {
    $config = $event->getConfig();
    \Drupal::messenger()->addStatus('(Another) Deleted config: ' . $config->getName());
  }

}

実質的に唯一の重要な違いは、getSubscribedEvents()メソッドの戻り値の配列を変更したことです。特定のイベントの値がローカルメソッド名の文字列である代わりに、現在は配列になり、配列の最初の要素はローカルメソッド名、2番目の要素はそのリスナーの優先度です。

つまり、これを変更しました:

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

次のように:

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

期待される結果:

  • AnotherConfigEventSubscriber::configSave()は非常に高い優先度を持つため、ConfigEventsSubscriber::configSave()の前に実行されるはずです。
  • AnotherConfigEventSubscriber::configDelete()は非常に低い優先度を持つため、ConfigEventsSubscriber::configDelete()の後に実行されるはずです。

統計モジュールを再度有効化して、SAVEイベントの動作を見てみましょう。

events-4-installed-priorities

統計モジュールのインストールとメッセージの表示。

素晴らしい!ConfigEvents::SAVEの新しいイベントリスナーは、以前書いたものより先に発生しました。次に、統計モジュールをアンインストールして、DELETEイベントで何が起こるかを見てみましょう。

events-5-uninstalled-priorities

統計モジュールのアンインストールとメッセージの表示。

こちらも素晴らしい!ConfigEvents::DELETEの新しいイベントリスナーは、非常に低い優先度を持つため、もう1つのリスナーより後に実行されました。

注記。優先度を指定せずにイベントサブスクライバーを登録すると、デフォルトで値0が設定されます。

リンク: