Symfonyアドベントカレンダー 23日目の記事です :christmas_tree::crescent_moon:

こんにちは。カルテットコミュニケーションズ でフロントエンドエンジニアをしています、松岡です。

ここ数年はフロントエンド開発に没頭しているため Symfony の知識はほとんどありません…。ですが Symfony4+ で Server-Sent events が使えるらしい!という事を知って思わず飛びついてしまいました。

この記事では Server-Sent events をローカル開発環境で試すまでのステップを紹介させていただきます。

Symfony Gets Real-time Push Capabilities!
https://symfony.com/blog/symfony-gets-real-time-push-capabilities

Server-Sent events とは

平たく言えばバックエンドからフロントエンドに対してプッシュ通知する仕組みです。

データの更新を伴う処理は、画面での保存ボタンのクリックなどをトリガとして、フロントエンドからバックエンドに対して ajax でリクエストするのがよく使われる方法です。

エンティティの保存など、短時間かつ単独のプロセスで処理が完了するものについては 1回の ajax リクエストで完了したかどうかの判定をする事が多いですよね。ですがレポート生成など「いつ終わるか分からない」ものに対して ajax リクエストのレスポンスを待っていると数時間も待機状態になってしまう事があります。

このような場合は、数秒〜数分おきにステータス変更の問い合わせを ajax リクエストで行う ポーリング という手法をよく使っていました。ポーリングには、リクエストの間隔を短くしたり監視対象が増えたりすると ajax リクエストが爆発的に増えてしまうというデメリットがあります。

SSE(Server-Sent events) では、繰り返しの ajax リクエストは不要になり、ポーリングよりずっとスマートに処理を行う事ができます。

検証に使った環境とパッケージ

Symfony のローカルサーバーを立ち上げるまで

まずは Symfony のインストールから始めましょう。

参考サイト
【初心者向け】初めてのSymfony4
Symfony4+ が全く分からないので同僚エンジニアに入門記事を書いて!!と頼んで書いてもらった記事です。めちゃくちゃ参考になりました。

今回の SSE は Symfony 5系ではうまく動作しなかったので、4系 をインストールしました。

$ mkdir symfony-sse && cd $_
$ composer create-project symfony/website-skeleton:^4.4 symfony-sse

ローカル用の環境変数を設定します。

$ cp .env .env.local

# .env.local
# DATABASE_URL=mysql://{USER}:{PASSWORD}@127.0.0.1:3306/sample?serverVersion=5.3

今回は Symfony の Web アプリケーションとしての機能は使用しませんが、正常に動作している事を確認したいのでローカルサーバーをインストールします。

$ composer req server
$ php -S 127.0.0.1:8000 -t public/

bin/console server:start でサーバーは起動するもののブラウザで該当ページが Connecton refused になってしまうので php コマンドを使いました…。

ブラウザで http://127.0.0.1:8000 を開いて「Welcome to Symfony 4.4」が表示されればセットアップ完了です!

Symfony でプッシュ通知をする

バンドルのインストール

Symfony で SSE を実現するのは mercure(メルキュール)という外部のパッケージです。

コンポーネントとして symfony/mercure が公開されていて単体で利用する事もできますが、それをラップし設定を環境変数から読み込んでくれるバンドル symfony/mercure-bundle が便利です。今回はバンドルのほうをインストールします。

$ composer require mercure

インストールが完了すると .env.local に設定が追加されています。プッシュ通知の URL をローカル検証用のものに変更しておきます。

###> symfony/mercure-bundle ###
# See https://symfony.com/doc/current/mercure.html#configuration
- MERCURE_PUBLISH_URL=http://mercure/.well-known/mercure
+ MERCURE_PUBLISH_URL=http://localhost:3000/.well-known/mercure
# The default token is signed with the secret key: !ChangeMe!
MERCURE_JWT_TOKEN=...
###< symfony/mercure-bundle ###

Hub インストール

mercure は専用のプロトコルを使ってプッシュ通知を行います。専用のプロトコルでの GET / POST を処理する Hub と呼ばれるサーバーが必要なのでダウンロードします。私は Mac を使っているので _Darwin_x86_64 をダウンロードしました。

https://github.com/dunglas/mercure/releases/tag/v0.8.0

Hub サーバー起動

ダウンロードした圧縮ファイルを解凍して mercure のバイナリをターミナルから呼び出します。 プロダクション環境では jwt-keyorigin などもろもろ調整する必要がありそうですが、今回はローカルでの検証目的のためデフォルトをそのまま使う事にします。

$ cd {DOWNLOAD_DIR}

$ ./mercure --jwt-key='!ChangeMe!' \
  --addr=':3000' \
  --debug \
  --allow-anonymous \
  --cors-allowed-origins='*' \
  --publish-allowed-origins='http://localhost:3000'

macOS Catalina では初回のみ 開発元を検証できないため開けません というメッセージが表示されます。「システム環境設定 > セキュリティとプライバシー > ダウンロードしたアプリケーションの実行許可 > このまま許可」の操作を行ってください。

ブラウザで http://localhost:3000 を開くとこのような mercure のデバッグ用ページが表示されます。

デバッグ用ページで Pub/Sub を試す

mercure のデバッグ用ページではプッシュ通知(Publish)と待ち受け(Subscribe)を試す事ができます。

  • 「Subscribe」でトピックを入力し Subscribe ボタンを押します(青枠)
  • 「Publish」でトピックとデータを入力し Publish ボタンを押します(赤枠)
  • Publish ボタンを押すごとにプッシュ通知の内容が追加されます(オレンジ)

トピックはプッシュ通知のチャンネルのようなもので、チャンネル別にプッシュ通知や待ち受けを整理する事ができます。

Symfony コマンドの作成

公式ドキュメントでは コントローラを使用したプッシュ通知のサンプルコード が紹介されていますが、実際の運用ではバッチ処理の完了などがプッシュ通知の主なタイミングになると予想されます。そのためコンソールコマンドを経由したプッシュ通知を試す事にしました。

まずコンソールコマンドを作成します。

$ bin/console make:command push-message

作成したコマンドのソースコードを下記のように変更します。

<?php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Mercure\PublisherInterface;
use Symfony\Component\Mercure\Update;

class PushMessageCommand extends Command
{
    protected static $defaultName = 'app:push-message';

    private $publisher;

    public function __construct(PublisherInterface $publisher)
    {
        $this->publisher = $publisher;
        parent::__construct();
    }

    protected function configure()
    {
        $this
            ->setDescription('push message demo')
            ->addArgument('message', InputArgument::REQUIRED, 'Argument description')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $message = $input->getArgument('message');
        $this->push($message);
        $io->success(sprintf('%s pushed.', $message));

        return 0;
    }

    private function push($message)
    {
        $update = new Update('topic1', $message);
        call_user_func($this->publisher, $update);
    }
}

Symfony\Component\Mercure\PublisherInterface

<?php
public function __construct(PublisherInterface $publisher)
{
  ...

mercure コンポーネントを autowire で注入するとデバッグ画面で試した「Publish」と同じ動作を Symfony コードから実行する事ができます。公式のサンプルコードではクラスを注入していますが、mercure の新しいリリースによりインターフェースに代わっています。
https://github.com/symfony/mercure-bundle/releases/tag/v0.2.0

Symfony\Component\Mercure\Update

<?php
private function push($message)
{
    $update = new Update('topic1', $message);
    call_user_func($this->publisher, $update);

Update はペイロードを格納するクラスです。このクラスを Publisher に渡すと環境変数に設定された Hub サーバーにリクエストを送信するという仕組みになっているようです。

コマンドを経由してプッシュ通知を試す

実際にコマンドを叩いてプッシュ通知を送信してみましょう。

$ bin/console app:push-message "テスト!!!"

mercure のデバッグ用ページを見てみましょう。 新しいプッシュ通知を受信しているはずです。

percel で待ち受ける

mercure のデバッグ用ページを見ればプッシュ通知が正常に動作している事は一目瞭然ですが、せっかくなのでフロントエンドでの待ち受け処理も書いてみましょう。

最小最速で Web アプリケーションを作るには PARCEL がおすすめです。

$ mkdir my-app && cd $_
$ touch index.html
$ touch index.js
<!-- index.html -->
<html>
  <head>
    <script src="./index.js"></script>
  </head>
  <body>Hello my-app!</body>
</html>
$ npm install -g parcel-bundler
$ parcel ./index.html

ブラウザで http://localhost:1234 を開くと「Hello my-app!」と表示されます。 ビルドからサーバー起動までたったの1秒!速いですね。

待ち受け用のコード

// my-app/index.js

const es = new EventSource(
  'http://localhost:3000/.well-known/mercure?topic=topic1'
);

es.onmessage = e => {
    console.log(e.data);
}

JavaScript のコードはたったこれだけです。

ブラウザが EventSource に対応している必要がありますのでご注意ください。
MDN EventSource
https://developer.mozilla.org/ja/docs/Web/API/EventSource

Symfony のプッシュ通知を JavaScript でキャッチする

ここまでの手順で準備は全て完了しています。 Symfony のコマンドを叩いて JavaScript がキャッチする事をブラウザで確認してみてください。

終わりに

手順の紹介は以上です。 Symfony4+ であれば簡単に SSE が利用できるのでポーリングに代わる新しい方法として、ぜひプロダクション環境でも使ってみたいなと思います。

明日は kojirock5260 さんの記事です。 よろしくお願いします!