RitoLabo

CakePHP3でイベントリスナーを用いた処理の実装(&メールやSlackでの通知)

  • 公開:
  • カテゴリ: PHP CakePHP
  • タグ: PHP,Events,Listeners,ObserverPattern,DesignPattern,CakePHP,3.5,3.6

今回は、CakePHPのイベントリスナでメールやSlackへ通知を行います。いわゆるObserverパターンを用いたイベント処理になります。

アジェンダ
  1. 開発環境
  2. 通知コンポーネント
    1. メールコンポーネント
    2. Slackコンポーネント
    3. execコンポーネント
  3. イベントリスナ
  4. コントローラ
  5. 動作確認

開発環境

今回の開発環境は以下の通りです。

  • Linux CentOS 7
  • Apache 2.4
  • PHP 7.2/7.1
  • CakePHP 3.6/3.5

CakePHPのルートディレクトリを「cakephp/」とします。

通知コンポーネント

まずは元となる、通知を行うコンポーネントを作成してしまいます。ちなみに、必ずコンポーネントである必要はないので、適宜必要なところに必要な形で作成してください。今回はひとまず、コンポーネントで作成します。

メールコンポーネント

メールを送信するコンポーネントを作成します。Bakeコマンドでファイルを生成します。

# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp

# SendMailコンポーネントの生成
bin/cake bake component SendMail

# 実行結果
[demo@localhost cakephp]$ bin/cake bake component SendMail

Creating file /path/to/cakephp/src/Controller/Component/SendMailComponent.php
Wrote `/path/to/cakephp/src/Controller/Component/SendMailComponent.php`

Baking test case for App\Controller\Component\SendMailComponent ...

Creating file /path/to/cakephp/tests/TestCase/Controller/Component/SendMailComponentTest.php
Wrote `/path/to/cakephp/tests/TestCase/Controller/Component/SendMailComponentTest.php`

cakephp/src/Controller/Component 配下に SendMailComponent.php が生成されます。

cakephp
├─ src
│   ├─ Controller
│   │   ├─ Component

│   │   │   └─
SendMailComponent.php

実装は以下の通りです。

cakephp/src/Controller/Component/SendMailComponent.php
<?php
namespace App\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;
use Cake\Mailer\Email;

class SendMailComponent extends Component
{
protected $_defaultConfig = [];

public function initialize(array $config)
{
$this->email = new Email('default');
}

public function send($message)
{
$this->email->from(['from@example.com' => 'CakePHP'])
->to('to@example.com')
->subject('cakephp test')
->send($message);
}
}

シンプルにメッセージ(本文)を受け取って送信している処理になります。

Slackコンポーネント

次に、Slackコンポーネントを作成します。これもBakeコマンドでファイルを生成します。

# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp

# Slackコンポーネントの生成
bin/cake bake component Slack

# 実行結果
[demo@localhost cakephp]$ bin/cake bake component Slack

Creating file /path/to/cakephp/src/Controller/Component/SlackComponent.php
Wrote `/path/to/cakephp/src/Controller/Component/SlackComponent.php`

Baking test case for App\Controller\Component\SlackComponent ...

Creating file /path/to/cakephp/tests/TestCase/Controller/Component/SlackComponentTest.php
Wrote `/path/to/cakephp/tests/TestCase/Controller/Component/SlackComponentTest.php`

cakephp/src/Controller/Component 配下に SlackComponent.php が生成されます。

cakephp
├─ src
│   ├─ Controller
│   │   ├─ Component

│   │   │   ├─
SendMailComponent.php
│   │   │   └─
SlackComponent.php

実装は以下の通りです。

cakephp/src/Controller/Component/SlackComponent.php
<?php
namespace App\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;

class SlackComponent extends Component
{
protected $_defaultConfig = [];

public $components = ['Exec'];

protected $endpoint;
protected $channel;
protected $body;

public function initialize(array $config)
{
$this->endpoint = getenv("SLACK_WEBHOOK_URL");
$this->channel = getenv("SLACK_CHANNEL");
$this->body = [
"channel" => $this->channel,
];
}

/**
* 送信ハンドラ
* @param $message
*/
public function handle($message)
{
$this->getBody($message);
$this->send();
}

/**
* Slack通知処理
*/
public function send()
{
$command = sprintf(
"curl -X POST -H 'Content-type: application/json' --data '%s' %s",
$this->messageEncodeJson(),
$this->endpoint
);
$this->Exec->exec($command);
}

/**
* 通知メッセージのエンコード
* @return string
*/
private function messageEncodeJson()
{
return json_encode($this->body, JSON_UNESCAPED_UNICODE);
}

/**
* 通知メッセージ作成
* @param $param
*/
public function getBody($message)
{
$this->body = array_merge($this->body, [
"text" => $message,
]);
}

/**
* メッセージフォーマット
* @return array
* @see https://api.slack.com/methods/chat.postMessage
*/
private function getFormat()
{
return [
"channel" => "",
"icon_url" => "",
"icon_emoji" => "",
"username" => "", // ボットのユーザー名を設定します。 as_userをfalseに設定して使用必要があります。それ以外の場合は無視します。
"as_user" => false, // ボットとしてではなく正式なユーザーとしてメッセージを投稿するにはtrueを渡します。 デフォルトはfalseです。
"text" => "",
"footer_icon" => "",
"attachments" => [
[
"color" => "",
"footer" => "",
"ts" => "",
"fields" => [
[
"title" => "",
"value" => "",
"short" => true
],
[
"title" => "",
"value" => "",
"short" => true
],
[
"title" => "",
"value" => "",
"short" => false
]
],

]
]
];
}

}

getFormat()メソッドは項目参照の為だけに定義しているので今回は使用していません。

execコンポーネント

本編からは少し外れますが、Slackへ送信する際にLinuxのcurlコマンドを叩いているので、SlackComponentではexec()メソッドの為のコンポーネントを挿しています。

cakephp/src/Controller/Component/ExecComponent.php
<?php
namespace App\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;
use Cake\Core\Exception\Exception;

/**
* Exec component
*/
class ExecComponent extends Component
{
protected $_defaultConfig = [];

/**
* exec関数を実行する
* @param $command
* @return mixed
*/
public function exec($command)
{
$command = sprintf('%s 2>&1', $command);
exec($command,$output, $ret);
if($ret) {
$message = sprintf("Command Failed [Command] %s [Error] %s", $command, $output[0]);
throw new Exception($message);
}
return $output;
}
}

これは必須ではないので、とりあえずSlack通知を試したい場合はSlackComponentを以下のように変更してください。

// 変更1
public $components = ['Exec'];
// ↓ コメントアウト、もしくは削除
// public $components = ['Exec'];

// 変更2
public function send()
{
$command = sprintf(
"curl -X POST -H 'Content-type: application/json' --data '%s' %s",
$this->messageEncodeJson(),
$this->endpoint
);
//$this->Exec->exec($command);
// ↓ 変更
exec($command, $output, $result);
}

イベントリスナ

それではこれらをリスナーに登録していきます。

cakephp/src 配下に Event ディレクトリを作成し、そこへ NotificationListener.php を作成します。

cakephp
├─
src
│   ├─
Event
│   │   └─
NotificationListener.php

そして以下のように実装していきます。

cakephp/src/Event/NotificationListener.php
<?php
namespace App\Event;

use Cake\Event\EventListenerInterface;
use Cake\Controller\ComponentRegistry;
use App\Controller\Component\SendMailComponent;
use App\Controller\Component\SlackComponent;

/**
* Class NotificationListener
* @package App\Event
*/
class NotificationListener implements EventListenerInterface
{
protected $Email;
protected $Slack;

public function __construct()
{
$this->Email = new SendMailComponent(new ComponentRegistry());
$this->Slack = new SlackComponent(new ComponentRegistry());
}

public function implementedEvents()
{
return [
'Notification.E-Mail' => 'mailNotification',
'Notification.Slack' => 'slackNotification',
];
}

/**
* E-Mail通知処理
* @param $event
* @param $message
*/
public function mailNotification($event, $message)
{
$this->Email->send($message);
}

/**
* Slack通知処理
* @param $event
* @param $message
*/
public function slackNotification($event, $message)
{
$this->Slack->handle($message);
}

}

コンストラクタにてそれぞれのコンポーネントをインスタンス化しメンバ変数へ格納、implementedEvents()メソッドで各イベントに対する処理(アクション)を登録しています。識別子には任意のイベント名(重複なしの形)を、値の部分には紐づけるアクションをセットします。残り2つのアクションはそれぞれ、メール送信処理を行うアクションとSlack通知処理を行うアクションであり、それぞれ、コンポーネントで送信処理を行っています。

コントローラ

最後に、コントローラの中でイベントを発火させて通知処理を行います。Bakeコマンドでファイルを生成します。

# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp

# SampleEventコントローラの生成
bin/cake bake controller SampleEvent

# 実行結果
[demo@localhost cakephp]$ bin/cake bake controller SampleEvent

Baking controller class for SampleEvent...

Creating file /path/to/cakephp/src/Controller/SampleEventController.php
Wrote `/path/to/cakephp/src/Controller/SampleEventController.php`
Bake is detecting possible fixtures...

Baking test case for App\Controller\SampleEventController ...

Creating file /path/to/cakephp/tests/TestCase/Controller/SampleEventControllerTest.php
Wrote `/path/to/cakephp/tests/TestCase/Controller/SampleEventControllerTest.php`

cakephp/src/Controller 配下に SampleEventController.php が生成されます。

cakephp
├─ src
│   ├─ Controller
│   │   └─
SampleEventController.php

コントローラは以下のように実装します。

cakephp/src/Controller/SampleEventController.php
<?php
namespace App\Controller;

use App\Controller\AppController;
use Cake\Event\Event;
use Cake\Event\EventManager;
use App\Event\NotificationListener;

class SampleEventController extends AppController
{
public function initialize()
{
parent::initialize();
$this->autoRender = false;

$this->Notification = new NotificationListener();
EventManager::instance()->attach($this->Notification);
}

public function mail()
{
$message = "Hello world!!";

$event = new Event('Notification.E-Mail', $this, [
'message' => $message
]);
$this->getEventManager()->dispatch($event);
}

public function slack()
{
$message = "hello world!";

$event = new Event('Notification.Slack', $this, [
'message' => $message
]);
$this->getEventManager()->dispatch($event);
}

}

肝の部分だけ解説します。

$this->Notification = new NotificationListener();
EventManager::instance()->attach($this->Notification);

コンストラクタにて、インスタンス化したリスナーをイベントマネージャーへ渡しイベントを登録しています。

$event = new Event('Notification.E-Mail', $this, [
'message' => $message
]);
$this->getEventManager()->dispatch($event);

イベントマネージャーにイベントを渡しイベントを発火させています。インスタンス化の際の引数には、それぞれ、発火させるイベント名、イベントに関連付けられているオブジェクト(通常は$this)、そしてイベントリスナへ渡す情報を配列で渡しています。

ちなみにコンポーネント内でイベントを発火させる場合は以下のようにします。

$this->_registry->getEventManager()->dispatch($event);

動作確認

全ての実装が完了したので、ブラウザからアクセスし動作確認を行います。それぞれ以下へアクセスすると、メール、Slackでの通知処理が行われます。

http://YOUR-DOMAIN/sample-event/mail

メール受信画面

http://YOUR-DOMAIN/sample-event/slack

Slack受信画面

送信が行われた事を確認できました。

まとめ

作業は以上で終了です。イベント処理を使う事によってロジックの分離(オブジェクトの疎結合)も進み、メンテナンス性の高いアプリケーションの構築が行えるようになるので是非試してみてください。