1. Home
  2. PHP
  3. CakePHP
  4. CakePHP3でイベントリスナーを用いた処理の実装(&メールやSlackでの通知)

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

  • 公開日
  • 更新日
  • カテゴリ:CakePHP
  • タグ:PHP,Events,Listeners,ObserverPattern,DesignPattern,CakePHP
CakePHP3でイベントリスナーを用いた処理の実装(&メールやSlackでの通知)

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

Contents

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

開発環境

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

  • PHP 7.2
  • CakePHP 3.6

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

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

まとめ

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

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級