3. リマインダー
いよいよ、Hubサイトの作成です。最初はリマインダー機能です。登録しておいた時間にメッセージを送ってくれます。すでに、Reminderイベントも作成していますしね。
今回、起動側は他のWebサービスを利用してイベントを発行しません。Laravel自身のAritsanスケジューラーの時間起動機能をそのまま利用しましょう。
送信先は一般的なサービスです。メールで自分のアドレスに通知します。他の人のアドレスに送っちゃ嫌がらせメールです。忘れちゃいけません、メールも立派なWebサービスですよ。面倒な送信プロトコルはLaravelに任せられますし、なにせ私達全員馴染み深いシステムですからね。最初に扱うには、ぴったりな主題です。
3.1 reminderコマンド
以降に作成するコマンドは、起動されると特定のWebサービスにアクセスし、新しいメールが来ているかとか、特定の情報があるかとか、通知すべきものがあるのかチェックします。
今回リマインダーに使用するreminderコマンドは、このようなチェックは一切せず、fire:reminderと同じようにイベントを通知するだけです。指定時間にArtisanスケジューラーから起動されます。機能的には一番単純なコマンドです。app/HubConnections/Commands/Reminder.phpです。
<?php
namespace App\HubConnections\Commands;
use App\Console\Commands\BaseCommand;
use App\HubConnections\Notifiers\Reminder as Notifire;
/**
* リマインダー通知コマンド
*
* スケジューラーにより起動され
* メッセージをリマインダーイベントでディスパッチする。
*/
class Reminder extends BaseCommand
{
protected $signature = 'hub:reminder '
.'{message : Reminder message}';
protected $description = 'Notify a reminder message.';
/** @var Notifire * */
private $notifire;
public function __construct(Notifire $notifire)
{
$this->notifire = $notifire;
parent::__construct();
}
public function handle()
{
// 必要な場合、ここで引数やオプションのチェックを行う
// ビジネスロジックは別の責務として分離する
$this->notifire->run($this->argument('message'));
// 終了コード
return 0;
}
}
Artisanコマンドは既にお手の物でしょう。本体部分はhandleメソッドでした。注目しましょう。
コンストラクターはサービスコンテナの働きで、タイプヒントされたクラスのインスタンスを自動的に依存解決し、渡してくれるんでしたよね。特に依存の解決方法を指定していなくても、インスタンス可能なクラスであれば、それを生成してくれます。
このコマンドで必要なのは、通知ロジックを実装しているNotifireクラスです。具体的にはApp\HubConnections\Notifiers\Reminderクラスです。それをサービスコンテナによる自動インスタンス注入機能により、受け取っています。
では、この通知ロジッククラスを見てみましょう。
<?php
namespace App\HubConnections\Notifiers;
use App\HubConnections\Events\Reminder as Event;
use Illuminate\Contracts\Events\Dispatcher;
/**
* リマインダー通知ロジッククラス
*/
class Reminder
{
/** @var Dispatcher * */
private $dispacher;
/** @var Event * */
private $event;
/**
* コンストラクター
*
* @param Dispatcher $dispatcher
* @param Event $event
*/
public function __construct(Dispatcher $dispatcher, Event $event)
{
$this->dispacher = $dispatcher;
$this->event = $event;
}
/**
* リマインダー通知ビジネスロジック
*
* @param string $message
*/
public function run($message)
{
// イベントにメッセージを設定
$this->event->message = $message;
// イベント発行
$this->dispacher->fire($this->event);
}
}
コマンドのコンストラクターでタイプヒントを指定し、この通知クラスは自動注入されました。自動注入されるクラスのコンストラクターにタイプヒントが記述されていると、それらも再帰的に自動注入してくれます。そのため、このクラスでもイベントデスパッチャーとReminderイベントのインスタンスが取得できます。
デスパッチャーをよく見ると、Illuminate\Contracts\Events\Dispatcherです。これ、実はインターフェイスです。
「さすがララベル、インターフェイスまで解決してくれるなんて、魔法みたいだな。」
実のところインターフェイスも解決できます。ですがインターフェイスの場合、「このインターフェイスを解決するときは、このクラスをインスタンス化する」という指定が予め必要です。
Illuminate\Contractsの下には、システムが定義しているContract、つまり「契約」が置かれています。Laravelが提供している契約インターフェイスは起動時の初期処理により、サービスコンテナにその具象クラスが登録されます。それにより、契約としてのインターフェイス名を指定しても、その契約を実装している具象クラスのインスタンスを取得できます。
ではIlluminate\Contracts\Events\Dispatcherインターフェイスで取得できる実体のクラスとは何でしょう。それは今までイベントを発行する時に利用していた\Eventファサードクラスの実体と同じものです。Illuminate\Events\Dispatcherクラスです。実体クラスを確認するには公式ドキュメントのファサードと契約のページを参照してください。もしくは、契約のインターフェイスと実体クラスを結合しているIlluminate\Foundation\Applicationクラスを読んでください。
Laravelのファサードは静的メソッド記法を提供していますが、実際は静的メソッドではなくインスタンス化して利用する通常のクラスです。Laravelが必要に応じてインスタンス化し、静的記法のメソッドに対しマジックメソッドを使用し、staticではない通常のメソッドを呼び出しています。
ですから、\Event::fire()と今回のコード中に存在している$this->dispacher->fire()は、同じメソッドを呼び出しており、「イベントの発行」という同じ動作をします。
3.2 Reminderイベントクラス
イベントクラスです。単純化しています。
<<?php
namespace App\HubConnections\Events;
/**
* リマインダーイベント
*/
class Reminder extends HubConnectionBaseEvent
{
/** @var string * */
public $message = '';
/**
* イベントの文字列変換
*
* @return string
*/
public function __toString()
{
return $this->message;
}
}
今までのイベントクラスと大差ありません。app/HubConnections/Eventsディレクトリーに保管していることに注意を払ってください。
3.3 MailSenderイベントリスナー
イベントリスナーは、App\HubConnections\Listeners\MailSenderです。名前が示している通り、メールを送信する役目です。
<?php
namespace App\HubConnections\Listeners;
use App\HubConnections\Events\HubConnectionBaseEvent;
class MailSender
{
/**
* 受け取ったイベントをメールで送信する
*/
public function handle(HubConnectionBaseEvent $event)
{
// 実行確認のため、ダミーコードとしてログ出力する
\Log::info($event);
}
}
今のところ、ダミーです。とりあえずうまく動作しているかチェックするため、イベントをログへ書き込んでいます。イベントが間違いなく到達することを確認してから、内容を実装しましょう。
Log::infoメソッドの引数は文字列です。イベントクラスインスタンスを直接渡しています。イベントが持っている情報は、それぞれ異なります。リスナー側でイベントに依存する情報を編集すると、取り扱うイベントごとにロジックが必要になります。そこで、イベントのことはイベントに任せています。リスナーとしてはイベントに「文字列化のためのメソッド」が実装されていれば事が足ります。
文字列化のためのメソッドである__toStringが実装されていることを保証したいために、StringizableInterfaceインターフェイスをimplimentsし、実装を強要するHubConnectionBaseEventベースクラスをタイプヒントし、イベントを受け取っています。もしこの__toStringを実装していないクラスインスタンスが渡されると、実行時にエラーになります。
この実行時エラーは「引数に渡された内容が間違っている」ということを表します。誤りがあるまま素通りさせるのではなく、「これ間違っているよ」と叱ってもらうのです。素通りさせてしまうと、他の箇所でエラーになったりするため、原因を探さなくてはなりません。今回は単純なコードですが、複雑なロジックの場合、原因から離れた場所でトラブルになると解決するのが面倒になります。ですから、PHPの仕組みを使い、あらかじめ予防策を取っておきます。
では、EventServideProviderで、イベントとリスナーを結びつけましょう。
<?php
namespace App\Providers;
use App\HubConnections\Events\Reminder;
use App\HubConnections\Listeners\MailSender;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServicePr\
ovider;
class EventServiceProvider extends ServiceProvider
{
/**
* アプリケーションのイベントリスナーのマップ
*
* @var array
*/
protected $listen = [
Reminder::class => [MailSender::class],
];
/**
* アプリケーションのその他のイベントの登録
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
}
}
ここでは、結びつけているだけです。
では、Reminder Artisanコマンドを登録し、実行しましょう。App\Console\Kernelを変更します。
<?php
namespace App\Console;
use App\HubConnections\Commands\Reminder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
Reminder::class,
];
protected function schedule(Schedule $schedule)
{
}
}
これでコマンドが登録できました。実行してください。
php artisan hub:reminder テスト実行!
適当なメッセージを渡して、ログファイルに書き込まれるか確認してください。
うまく行ったら、MailSenderリスナークラスへメール送信のコードを入れましょう。
<?php
namespace App\Handlers\Events;
use App\Events\StringizableInterface;
use Illuminate\Contracts\Mail\Mailer;
class MailSender
{
/** @var Mailer */
private $mailer;
public function __construct( Mailer $mailer )
{
$this->mailer = $mailer;
}
public function handle( StringizableInterface $event )
{
$this->mailer->raw( $event, function ($m)
{
$m->to( '自分のメールアドレス', '自分' )
->subject( 'Hubサイトのメール通知' );
} );
}
}
‘自分のメールアドレス’は自分のメールアドレスに書き換えてください。そうしないとエラーが発生します。(たとえlogメールドライバーを使用していてもです。メールアドレスとして有効な文字列にしてください。)
リスナーのhandleメソッドは、イベントを受け取るものと決まっています。そのため、サービスコンテナによる自動依存注入は行われません。コンストラクターの自動注入により、メーラーのインスタンスを受け取っています。
Mailファサードを使う方法もあります。Mailファサードを使用すれば、コンストラクターは必要なくなります。$this->mailer->rawを\Mail::rawと短く書けます。
ファサードはインスタンス化のコードが不必要ですし、ファサード名=使用するコア機能のため、直感的で読みやすいのです。一方の自動依存注入を利用したコードはエディターやIDEの補完などの手助けがフル活用でき便利です。どちらでもお好きな方をご利用ください。
3.4 使ってみる
さて、いつまでも自分でコマンドを叩いていたのでは全然便利ではありません。早速スケジューラーに登録しましょう。
<?php
namespace App\Console;
use App\HubConnections\Commands\Reminder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
Reminder::class,
];
protected function schedule(Schedule $schedule)
{
$schedule
->command('hub:reminder "燃えるゴミ、鳥よけカゴ出し"')
->dailyAt('08:00')
->days(1, 3, 5); // 月水金、配列でもOK
$schedule
->command('hub:reminder "川瀬さんの誕生日、プレゼントは現金でOK!Paypal可!!"')
->cron('0 8 4 7 *');
}
}
起動間隔の指定に利用できるメソッドは、Illuminate\Console\Scheduling\Eventクラスを確認してみるとよいでしょう。メソッドのコードを読んでみると気がつくことがあります。
内部的にはexpressionプロパティーとして、cron形式の日時時間が保持されています。(時間部分5つ+何に使用しているか不明1つ、たぶん実行ユーザーを指定するためのもの)cron指定の5つのフィールドをそれぞれ設定できるspliceIntoPositionメソッドが用意されていますが、ほとんどのメソッドで活用されていません。そのため、以下のようなことが起きます。
// これは思い通りに動く
$schedule->daily()->weekdays();
// これはweekdayが無視される
$schedule->weekdays()->daily();
dailyメソッドは単にexpressionプロパティーに、’0 0 * * * *‘をセットするだけです。weekdaysメソッドは曜日を表す5番目のフィールドだけを’1-5’に置き換えます。そのため、指定する順番により意図通りに動かないことになります。
確かにメソッドを使用したほうが意図が読み取りやすくなりますが、そのメソッドが値をただセットするのか、それとも特定のフィールドを置き換えるのかを全部覚えるのはやや大変です。
ところでcronの時間指定は面倒でしたか?ここまでやってきて、さほど難しくはないことが理解できたのではないでしょうか。cronの指定形式に慣れてしまえば、直接cronメソッドで記述してしまうほうが簡単です。私のスケジュールのゴミかご出しは以下のように書き直せます。
$schedule
->command( 'reminder "燃えるゴミ、鳥よけカゴ出し"' )
->cron('0 8 * * 1,3,5');
これも好みです。皆さんも自分で選択してください。
3.5 拡張
現在、起動側とリスナーは1対1の関係です。しかし、これからいろいろな条件で起動されたイベントで、メール送信を利用されるかも知れません。
そうすると、メールで指定するsubjectの内容は、通知する内容に応じたタイトルにできたほうが分かりやすいですよね。では、どうしましょうか?subjectごとにリスナーを作成しましょうか?それともコマンドで指定できるようにし、イベントに情報を含めましょうか?すると、その情報をどうやってリスナーで取得しましょうか?いろいろと考慮するしなくてはなりません。
実際に拡張をしなくても少し考えてみましょう。設計の練習になります。
同様にメールアドレスの変更をできるようにするには、どんな方法で実現しますか?
もし、このリマインダーをWebサイトから指定できるようにするなら、どう実現しますか?コマンドで追加できるようにするとしたらどうしましょうか?
どう拡張するか少し考えてみるだけでも、勉強になります。