【ステートマシン型実装】

はじめに

このページでは、REST-API サーバー開発環境におけるステートマシン(状態機械)型の実装方法について解説します。
ステートマシン型は、複数の状態(UNIT)を順番に遷移させながら処理を進める方式で、段階的レスポンス・非同期処理・ストリーミングに適しています。

ステートマシン型のイベント処理クラスはスキャフォールディングで自動生成できます。
▶スキャフォールディング

なお、生成されたイベント処理クラスはメイン処理クラスの $classes['event'] にクラス名を設定することで適用されます
また、ルーティング定義の event キーにはイベント処理クラス内のメソッド名を指定します。

UNIT の基本概念やステータス遷移の仕組みについては、以下のページで詳しく解説しています。
▶イベントハンドラについて

SOCKET-MANAGER Framework の REST-API は、ステートマシン(状態機械)を内部に組み込んだイベントループを採用しています。
これにより、Chunked Transfer・SSE(再接続対応)・Range 送信といった段階的なレスポンス処理を安定して実行できます。

本ページでは、ステートマシン型実装の基本構造と、イベントハンドラ型では難しい処理をどのように実現するのかを解説します。

ステートマシン型の役割

ステートマシン型のイベント処理クラスは、REST-API の処理において以下のような役割を持ちます。

  • 複数の処理ステップ(UNIT)を順番に実行する
  • 各ステップの終了時に次のステータス名を返し、フレームワークが自動遷移する
  • レスポンスを段階的に返す(チャンク転送・SSE など)
  • 非同期処理(setTimeout)を自然に組み込める

スキャフォールディング(生成方法)

ステートマシン型のイベント処理クラスは以下のコマンドで生成できます。
php worker custom:state-machine <カスタム名>
                    

生成されるファイル例:
app/EventClass/<カスタム名>.php
                    

生成されたクラスはメイン処理クラスの $classes['event'] にクラス名を設定することで適用されます
ルーティング定義の event キーには、イベント処理クラス内のメソッド名を指定します。

カスタムコマンドの仕組みについては以下をご覧ください。
▶カスタムコマンド作成機能

生成されるクラスの構造

スキャフォールディングで生成されるステートマシンクラスは以下のような構造になります。
<?php

namespace App\EventClass;

use App\CommandUnits\CommandForStateMachine;

class <カスタム名> extends CommandForStateMachine
{
    protected function responseJson()
    {
        return [
            [
                'status' => 'start',
                'unit' => function($p_param): ?string
                {
                    $p_param->response()->json(['message' => 'Hello API']);
                    return null;
                }
            ]
        ];
    }
}
                    

● 特記事項

  • コマンドで指定した カスタム名 がクラス名になる
  • 生成先は app/EventClass 配下
  • CommandForStateMachine を継承することでステートマシン型として認識される
  • メイン処理クラスの $classes['event'] にクラス名を設定する
  • ルーティングの event キーにはメソッド名を設定する
  • responseJson() の戻り値が UNIT の配列定義となる
  • UNIT は status(ステータス名)と unit(クロージャ)で構成される
  • クロージャの引数 $p_param にはコンテキストインスタンスが渡される
  • 最初に呼ばれるステータスは start
  • クロージャの戻り値に次のステータス名を返す。null を返すと終了

チャンク転送(Chunked Transfer)の実装例

以下は、4 行のテキストを段階的に送信するチャンク転送の完全な UNIT 実装例です。
protected function responseChunked()
{
    return [
        [
            'status' => 'start',
            'unit' => function($p_param): ?string {
                $p_param->response()->chunked("1行目のデータです\n");
                return 'chunked1';
            }
        ],
        [
            'status' => 'chunked1',
            'unit' => function($p_param): ?string {
                $p_param->setTimeout(5000);
                $p_param->response()->chunked("2行目:少しずつ送信しています...\n");
                return 'chunked2';
            }
        ],
        [
            'status' => 'chunked2',
            'unit' => function($p_param): ?string {
                $p_param->setTimeout(5000);
                $p_param->response()->chunked("3行目:もう少しお待ちください...\n");
                return 'chunked3';
            }
        ],
        [
            'status' => 'chunked3',
            'unit' => function($p_param): ?string {
                $p_param->response()->chunked("4行目:最後のデータです\n");
                $p_param->response()->end();
                return null;
            }
        ],
    ];
}
                    


上記の実装例では、説明のために 行データ単位で UNIT を分割 していますが、 実際のサンプルサーバーでは処理をより効率化するため、 2つ目の UNIT に処理を集約し、ポーリングUNITと再利用UNITを組み合わせる構成 を採用しています。

これにより、逐次的な送信制御を維持しつつ、UNIT の定義を最小限に抑えた実装が可能になります。 実際のサーバー実装では、用途に応じて UNIT の分割粒度を調整してください。

SSE(Server-Sent Events)の実装例

以下は、4 回の SSE イベントを段階的に送信する完全な UNIT 実装例です。

SSE の標準仕様については、MDN の Server-Sent Events の解説も参考になります。
>> MDN: Server-Sent Events
protected function responseSse()
{
    return [
        [
            'status' => 'start',
            'unit' => function($p_param): ?string {
                $p_param->response()->event("1回目のデータです", p_id: "sse0");
                return 'sse1';
            }
        ],
        [
            'status' => 'sse1',
            'unit' => function($p_param): ?string {
                $p_param->setTimeout(5000);
                $p_param->response()->event("2回目:少しずつ送信しています...", p_id: "sse1");
                return 'sse2';
            }
        ],
        [
            'status' => 'sse2',
            'unit' => function($p_param): ?string {
                $p_param->setTimeout(5000);
                $p_param->response()->event("3回目:まだ続きます...", p_id: "sse2");
                return 'sse3';
            }
        ],
        [
            'status' => 'sse3',
            'unit' => function($p_param): ?string {
                $p_param->response()->event("done", p_event: "end");
                $p_param->response()->end();
                return null;
            }
        ],
    ];
}
                    


上記の SSE 実装例では、理解しやすいように イベントごとに UNIT を分割 していますが、 サンプルサーバーでは処理を簡潔にするため、 2つ目の UNIT にイベント送信処理を集約し、ポーリングUNITと再利用UNITを組み合わせる方式 を採用しています。

この構成により、複数回のイベント送信を柔軟に制御しつつ、UNIT の定義を最小限に抑えることができます。 実際の用途に応じて、UNIT の分割方法を選択してください。

Range送信の実装例

Range送信では、バイナリデータやファイルデータの全量をそのまま渡すだけで、 フレームワーク側が Range ヘッダの有無を自動判定し、必要な範囲の切り貼りを行ってレスポンスを返します。
デベロッパーは Range 処理を意識する必要がなく、通常のレスポンスと同じ感覚で扱えます。

Range リクエストの標準仕様については、MDN の解説も参考になります。
>> MDN: HTTP Range Requests

● rangeBinary

$p_param->response()->rangeBinary($binary_data);
                    
バイナリデータの全量を渡すだけで、Range 指定があればフレームワークが自動で切り出して返します。

● rangeFile

$p_param->response()->rangeFile('./path/file.txt');
                    
ファイルパスを渡すだけで、Range 指定に応じた部分送信をフレームワークが自動処理します。

おわりに

ステートマシン型は、REST-API サーバー開発において段階的処理・非同期処理・ストリーミングを自然に表現できる強力な実装方式です。
また、IPC(サーバー間通信/プロセス間通信)においても複雑なシーケンスに対応でき、整理された処理順序で記述できます。
スキャフォールディングを活用することで、イベント処理クラスの作成を効率化できます。

生成可能なクラス一覧については以下をご覧ください。
▶スキャフォールディング