Emitter

イベントエミッター

AdonisJSには、emitteryの上に作られた組み込みのイベントエミッターがあります。Emitteryはイベントを非同期でディスパッチし、Node.jsのデフォルトのイベントエミッターの多くの一般的な問題を修正します。

AdonisJSは、追加の機能を備えたemitteryをさらに強化しています。

  • イベントとそれに関連するデータ型のリストを定義することで、静的な型安全性を提供します。
  • クラスベースのイベントとリスナーのサポート。リスナーを専用のファイルに移動することで、コードベースを整理し、テストしやすくします。
  • テスト中にイベントを偽装する機能。

基本的な使用法

イベントリスナーはstart/events.tsファイル内で定義されます。make:preloadエースコマンドを使用してこのファイルを作成できます。

node ace make:preload events

イベントをリッスンするにはemitter.onを使用する必要があります。このメソッドは、最初の引数としてイベントの名前、2番目の引数としてリスナーを受け入れます。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
emitter.on('user:registered', function (user) {
console.log(user)
})

イベントリスナーを定義したら、emitter.emitメソッドを使用してuser:registeredイベントを発行できます。このメソッドは、最初の引数としてイベント名、2番目の引数としてイベントデータを受け入れます。

import emitter from '@adonisjs/core/services/emitter'
export default class UsersController {
async store() {
const user = await User.create(data)
emitter.emit('user:registered', user)
}
}

イベントを一度だけリッスンするには、emitter.onceを使用できます。

emitter.once('user:registered', function (user) {
console.log(user)
})

イベントの型安全性の確保

AdonisJSでは、アプリケーション内で発行するすべてのイベントに対して静的な型を定義必須です。これらの型はtypes/events.tsファイルに登録されます。

次の例では、Userモデルをuser:registeredイベントのデータ型として登録しています。

すべてのイベントに対して型を定義するのが煩雑な場合は、クラスベースのイベントに切り替えることもできます。

import User from '#models/User'
declare module '@adonisjs/core/types' {
interface EventsList {
'user:registered': User
}
}

クラスベースのリスナー

HTTPコントローラーと同様に、リスナークラスはインラインのイベントリスナーを専用のファイルに移動するための抽象化レイヤーを提供します。リスナークラスはapp/listenersディレクトリに保存され、make:listenerコマンドを使用して新しいリスナーを作成できます。

参照: リスナーの作成コマンド

node ace make:listener sendVerificationEmail

リスナーファイルはclass宣言とhandleメソッドでスキャフォールディングされます。このクラスでは、必要に応じて複数のイベントをリッスンするための追加のメソッドを定義できます。

import User from '#models/user'
export default class SendVerificationEmail {
handle(user: User) {
// メールを送信する
}
}

最後のステップとして、リスナークラスをstart/events.tsファイル内のイベントにバインドする必要があります。#listenersエイリアスを使用してリスナーをインポートできます。エイリアスは、Node.jsのサブパスインポート機能を使用して定義されます。

start/events.ts
import emitter from '@adonisjs/core/services/emitter'
import SendVerificationEmail from '#listeners/send_verification_email'
emitter.on('user:registered', [SendVerificationEmail, 'handle'])

遅延ロードリスナー

アプリケーションの起動フェーズを高速化するために、リスナーを遅延ロードすることをオススメします。遅延ロードは、インポートステートメントを関数の後ろに移動し、動的インポートを使用するだけの簡単な操作です。

import emitter from '@adonisjs/core/services/emitter'
import SendVerificationEmail from '#listeners/send_verification_email'
const SendVerificationEmail = () => import('#listeners/send_verification_email')
emitter.on('user:registered', [SendVerificationEmail, 'handle'])

依存性の注入

リスナークラス内でHttpContextをインジェクトすることはできません。イベントは非同期で処理されるため、リスナーはHTTPリクエストが終了した後に実行される可能性があります。

リスナークラスはIoCコンテナを使用してインスタンス化されるため、クラスのコンストラクターまたはイベントを処理するメソッド内で依存関係を型ヒントできます。

次の例では、TokensServiceをコンストラクターの引数として型ヒントしています。このリスナーを呼び出すとき、IoCコンテナはTokensServiceクラスのインスタンスをインジェクトします。

コンストラクターのインジェクション
import { inject } from '@adonisjs/core'
import TokensService from '#services/tokens_service'
@inject()
export default class SendVerificationEmail {
constructor(protected tokensService: TokensService) {}
handle(user: User) {
const token = this.tokensService.generate(user.email)
}
}

次の例では、TokensServicehandleメソッド内でインジェクトしています。ただし、最初の引数は常にイベントペイロードになることに注意してください。

メソッドのインジェクション
import { inject } from '@adonisjs/core'
import TokensService from '#services/tokens_service'
import UserRegistered from '#events/user_registered'
export default class SendVerificationEmail {
@inject()
handle(event: UserRegistered, tokensService: TokensService) {
const token = tokensService.generate(event.user.email)
}
}

クラスベースのイベント

イベントは、イベント識別子(通常は文字列ベースのイベント名)と関連するデータの組み合わせです。

例:

  • user:registeredはイベント識別子(またはイベント名)です。
  • Userモデルのインスタンスはイベントデータです。

クラスベースのイベントは、イベント識別子とイベントデータを同じクラス内にカプセル化します。クラスのコンストラクターは識別子として機能し、クラスのインスタンスはイベントデータを保持します。

make:eventコマンドを使用してイベントクラスを作成できます。

参照: イベントの作成コマンド

node ace make:event UserRegistered

イベントクラスには動作はありません。代わりに、イベントのデータコンテナとなります。イベントデータをクラスのコンストラクターを介して受け入れ、インスタンスプロパティとして利用できるようにできます。

app/events/user_registered.ts
import { BaseEvent } from '@adonisjs/core/events'
import User from '#models/user'
export default class UserRegistered extends BaseEvent {
constructor(public user: User) {}
}

クラスベースのイベントのリスニング

emitter.onメソッドを使用してクラスベースのイベントにリスナーをアタッチできます。最初の引数はイベントクラスの参照であり、2番目の引数はリスナーです。

import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
emitter.on(UserRegistered, function (event) {
console.log(event.user)
})

次の例では、クラスベースのイベントとリスナーの両方を使用しています。

import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
const SendVerificationEmail = () => import('#listeners/send_verification_email')
emitter.on(UserRegistered, [SendVerificationEmail])

クラスベースのイベントの発行

static dispatchメソッドを使用してクラスベースのイベントを発行できます。dispatchメソッドは、イベントクラスのコンストラクターが受け入れる引数を取ります。

import User from '#models/user'
import UserRegistered from '#events/user_registered'
export default class UsersController {
async store() {
const user = await User.create(data)
/**
* イベントをディスパッチ/発行する
*/
UserRegistered.dispatch(user)
}
}

イベントリスニングの簡素化

クラスベースのイベントとリスナーを使用することを決めた場合、emitter.listenメソッドを使用してリスナーをバインドするプロセスを簡素化できます。

emitter.listenメソッドは、最初の引数としてイベントクラス、2番目の引数としてクラスベースのリスナーの配列を受け入れます。また、登録されたリスナーはhandleメソッドを持つ必要があります。

import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
emitter.listen(UserRegistered, [
() => import('#listeners/send_verification_email'),
() => import('#listeners/register_with_payment_provider'),
() => import('#listeners/provision_account')
])

エラーのハンドリング

デフォルトでは、リスナーが発生させる例外はunhandledRejectionによって処理されます。したがって、emitter.onErrorメソッドを使用してエラーを自己キャプチャして処理することをオススメします。

emitter.onErrorメソッドは、イベント名、エラー、およびイベントデータを受け取るコールバックを受け入れます。

import emitter from '@adonisjs/core/services/emitter'
emitter.onError((event, error, eventData) => {
})

すべてのイベントをリッスンする

emitter.onAnyメソッドを使用すると、すべてのイベントをリッスンできます。メソッドは、リスナーコールバックを唯一のパラメータとして受け入れます。

import emitter from '@adonisjs/core/services/emitter'
emitter.onAny((name, event) => {
console.log(name)
console.log(event)
})

イベントの購読解除

emitter.onメソッドは、イベントリスナーの購読を解除するために呼び出すことができる購読解除関数を返します。

import emitter from '@adonisjs/core/services/emitter'
const unsubscribe = emitter.on('user:registered', () => {})
// リスナーを削除する
unsubscribe()

また、emitter.offメソッドを使用してイベントリスナーの購読を解除することもできます。メソッドは、最初の引数としてイベント名/クラス、2番目の引数としてリスナーへの参照を受け入れます。

import emitter from '@adonisjs/core/services/emitter'
function sendEmail () {}
// イベントをリッスンする
emitter.on('user:registered', sendEmail)
// リスナーを削除する
emitter.off('user:registered', sendEmail)

emitter.offAny

emitter.offAnyは、ワイルドカードリスナーを削除します。

emitter.offAny(callback)

emitter.clearListeners

emitter.clearListenersメソッドは、指定されたイベントのすべてのリスナーを削除します。

//文字列ベースのイベント
emitter.clearListeners('user:registered')
//クラスベースのイベント
emitter.clearListeners(UserRegistered)

emitter.clearAllListeners

emitter.clearAllListenersメソッドは、すべてのイベントのすべてのリスナーを削除します。

emitter.clearAllListeners()

利用可能なイベントの一覧

利用可能なイベントの一覧については、イベントリファレンスガイドを参照してください。

テスト中のイベントの偽装

イベントリスナーは、特定のアクションの後に副作用を実行するためによく使用されます。たとえば、新しく登録されたユーザーにメールを送信したり、注文のステータス更新後に通知を送信したりします。

テストを書く際には、テスト中に作成されたユーザーにメールを送信するのを避けたい場合があります。

イベントエミッターを偽装することで、イベントリスナーを無効にできます。次の例では、ユーザーのサインアップのHTTPリクエストを行う前にemitter.fakeを呼び出しています。リクエストの後、events.assertEmittedメソッドを使用してSignupControllerが特定のイベントを発行することを確認しています。

import emitter from '@adonisjs/core/services/emitter'
import UserRegistered from '#events/user_registered'
test.group('ユーザーのサインアップ', () => {
test('ユーザーアカウントを作成する', async ({ client, cleanup }) => {
const events = emitter.fake()
cleanup(() => {
emitter.restore()
})
await client
.post('signup')
.form({
email: 'foo@bar.com',
password: 'secret',
})
})
// イベントが発行されたことをアサートする
events.assertEmitted(UserRegistered)
})
  • event.fakeメソッドはEventBufferクラスのインスタンスを返し、アサーションや発行されたイベントの検索に使用できます。
  • emitter.restoreメソッドは偽装を元に戻します。偽装を元に戻した後、イベントは通常通り発行されます。

特定のイベントの偽装

emitter.fakeメソッドは、引数なしでメソッドを呼び出すとすべてのイベントを偽装します。特定のイベントを偽装する場合は、最初の引数としてイベント名またはクラスを渡します。

// user:registeredイベントのみを偽装する
emitter.fake('user:registered')
// 複数のイベントを偽装する
emitter.fake([UserRegistered, OrderUpdated])

emitter.fakeメソッドを複数回呼び出すと、古い偽装が削除されます。

イベントのアサーション

偽装されたイベントに対してアサーションを行うために、assertEmittedassertNotEmittedassertNoneEmittedassertEmittedCountメソッドを使用できます。

assertEmittedメソッドとassertNotEmittedメソッドは、最初の引数としてイベント名またはクラスのコンストラクターを受け入れ、オプションのファインダー関数を受け入れます。ファインダー関数は、イベントが発行されたことを示すために真偽値を返す必要があります。

const events = emitter.fake()
events.assertEmitted('user:registered')
events.assertNotEmitted(OrderUpdated)
コールバックを使用したアサーション
events.assertEmitted(OrderUpdated, ({ data }) => {
/**
* orderIdが一致する場合にのみ
* イベントが発行されたとみなす
*/
return data.order.id === orderId
})
イベントの数をアサートする
// 特定のイベントの数をアサートする
events.assertEmittedCount(OrderUpdated, 1)
// イベントが発行されなかったことをアサートする
events.assertNoneEmitted()