Laravel

Laravel Sajyaとは?JSON-RPC 2.0サーバーの実装手順とエラー処理

Laravel Sajya(パッケージ名 sajya/server)は、LaravelアプリケーションにJSON-RPC 2.0仕様のサーバーを載せるためのパッケージです。WebSocketによるリアルタイム通信を実現するものでも、非同期タスクを実行するキューの代替でもありません。この記事では、2026年8月時点の最新版7.1.0のソースと公式ドキュメントを一次情報として、対応バージョンの境界、Procedureクラスの定義、バッチと通知の実装上の注意、エラーコードの変換ルールまでを整理します。

まとめ:Laravel Sajyaの要点

  • Sajyaの役割はJSON-RPC 2.0サーバーの実装です。1つのエンドポイントにPOSTされたJSONを解釈し、メソッド名に対応するPHPのメソッドへ振り分けて結果を返します。
  • 最新版は7.1.0(2026年3月26日公開)で、Laravel 13.xに対応したのはこのバージョンからです。7.0.0はLaravel 12.xまでしか受け付けません。
  • Laravel 11以降のスケルトンには routes/api.php が存在しません。公式クイックスタートはこの前提に触れていないため、先に php artisan install:api を実行する必要があります。
  • バッチリクエストの上限は同梱の config/sajya.php30件です。CHANGELOGの7.0.0の項は「50件」と記載しており、実際に配布されている設定値と食い違います。
  • Procedure内でバリデーションに失敗すると、Laravelの ValidationException が仕様どおりの -32602 Invalid params へ自動変換され、エラー内容が data に入ります。
  • Procedureまで到達したリクエストは、JSON-RPCのエラーを返す場合でもHTTPステータス200で応答します。ステータスコードで成否を判定する監視や中継の設計は、無改造では機能しません。

Laravel Sajyaの位置づけ:JSON-RPCサーバーを載せるパッケージ

提供範囲:リクエスト解釈・メソッド振り分け・レスポンス生成

JSON-RPCは、HTTPのパスやメソッドではなくJSON本体の method フィールドで呼び出し先を指定するプロトコルです。RESTのようにリソースごとのURLを設計せず、単一のエンドポイントへPOSTし続けます。Sajyaが担うのはこの受け口の実装で、具体的には次の3つです。

  • 受信したJSONを解釈し、JSON-RPC 2.0仕様のRequestオブジェクトへ変換する
  • method の値から呼び出すべきクラスとメソッドを特定し、パラメータをバインドして実行する
  • 戻り値または例外を、仕様どおりの resulterror を持つResponseへ整形する

パッケージが登録するのは Route::rpc() マクロ、make:proceduresajya:docs の2つのArtisanコマンド、そして RPC ファサードです。Laravelのルーティング・バリデーション・例外処理・DIコンテナは通常どおり働きます。

リアルタイム通信との混同回避:WebSocketはReverbの担当

「SajyaでリアルタイムUIを実現する」という説明を見かけたら誤りです。Sajyaはクライアントからのリクエストに応答するだけで、サーバーからクライアントへ能動的にデータを送る手段を持ちません。Laravelでサーバー起点の配信を行うなら、公式のWebSocketサーバーであるLaravel Reverbとブロードキャスト機構を使います。用途が重なる部分はありません。

関数呼び出しの感覚でAPIを設計するという発想自体は、gRPCやtRPCと共通します。方式ごとの違いはgRPCとは?定義・仕組み・RESTとの違いをまとめて解説で比較しています。

動作要件と対応バージョンの境界

7.1.0のcomposer.jsonが要求するのは ext-json とLaravel本体だけです。6.1.0までは doctrine/annotationsphpdocumentor/reflection-docblock に依存していましたが、7.0.0でPHPのアトリビュートへ移行した際に両方とも削除されました。

項目 内容
最新版 7.1.0(2026年3月26日)
対応Laravel 10.x・11.x・12.x・13.x
Laravel 13対応 7.1.0以降(7.0.0は12.xまで)
必須PHP拡張 ext-json
ライセンス MIT
累計ダウンロード 約204万(Packagist)

注意が必要なのは、composer.jsonの require ではなく conflict の指定です。7.1.0はLaravel 10.0.0から10.48.27まで、および11.0.0より上から11.42.0までを競合として除外しています。^10.0 と書かれていても、Laravel 10系で導入できるのは10.48.28以降、11系では11.0.0ちょうどか11.42.1以降だけです。古いパッチ版で止まっているプロジェクトでは composer require が依存解決の段階で失敗します。パッケージ側のテスト環境として orchestra/testbench の9.8.2未満も競合指定されているため、Sajyaを組み込んだパッケージを開発する場合はこちらも合わせて確認してください。

導入手順:インストールから疎通確認までの流れ

Composerでのインストールとルートファイルの用意

composer require sajya/server

公式クイックスタートは続けて「api.php にルートを定義する」と説明しますが、この記述はLaravel 10以前のスケルトンを前提にしています。Laravel 11以降の laravel/laravel には routes/api.php が同梱されておらず、bootstrap/app.phpwithRouting() にもAPIルートの指定がありません。先に次のコマンドを実行してください。

php artisan install:api

このコマンドは routes/api.php を生成し、あわせてLaravel Sanctumを導入します(--passport を付けるとPassportに切り替わります)。Sanctumのマイグレーション実行を確認するプロンプトが出るので、認証を後回しにしたい場合でもここでの選択は意識しておきます。ルートファイルさえ用意できれば web.php に定義しても動作しますが、CSRF保護の対象になるため、APIとして公開するなら api.php が適切です。

Procedureクラスの作成とname属性

処理の実体はProcedureクラスに書きます。コントローラとほぼ同じ感覚ですが、public static string $name の宣言が必須で、この値がJSON-RPCのメソッド名の前半になります。

php artisan make:procedure TennisProcedure

生成先は app/Http/Procedures です。$name を書き換え、公開メソッドとして処理を足します。

<?php

declare(strict_types=1);

namespace App\Http\Procedures;

use Sajya\Server\Procedure;

class TennisProcedure extends Procedure
{
    public static string $name = 'tennis';

    public function ping(): string
    {
        return 'pong';
    }
}

呼び出し対象になるのはpublicメソッドだけです。protectedやprivateにしたメソッドは、名前が一致していても -32601 Method not found が返ります。内部処理を切り出すときは可視性に注意してください。クラス名やプロパティ名の付け方はLaravelの命名規則一覧|テーブル・モデル・コントローラーからコーディング規約までの方針に揃えると、既存のコントローラ群と並べたときに読みやすくなります。

ルート登録と疎通確認

Sajyaが追加する Route::rpc() マクロに、URIと公開するProcedureクラスの配列を渡します。第2引数で公開範囲を明示する設計なので、後からバージョン違いのエンドポイントを増やしても衝突しません。

use Illuminate\Support\Facades\Route;
use App\Http\Procedures\TennisProcedure;

Route::rpc('/v1/endpoint', [TennisProcedure::class])
    ->name('rpc.endpoint');

メソッド名はクラスの $name とメソッド名を区切り文字でつないだ tennis@ping になります。php artisan serve で起動し、POSTで確認します。

curl --location --request POST 'http://127.0.0.1:8000/api/v1/endpoint' \
--header 'Content-Type: application/json' \
--data-raw '{
    "jsonrpc": "2.0",
    "method": "tennis@ping",
    "id": 1
}'
{
  "id": "1",
  "result": "pong",
  "jsonrpc": "2.0"
}

区切り文字の @config/sajya.phpdelimiter で変更できます。設定ファイルを書き換えたい場合は、ServiceProviderを指定して公開してください。パッケージ側の設定は起動時にマージされるため、既定値のままなら公開自体が不要です。

php artisan vendor:publish --provider="Sajya\Server\ServerServiceProvider"

認証を掛けるときは、通常のルートと同じくミドルウェアを重ねます。install:api で導入済みのSanctumを使うなら次のとおりです。JSON-RPCの params にAPIトークンを入れる設計は仕様の想定外なので、資格情報はヘッダーで渡してください。

Route::rpc('/v1/endpoint', [TennisProcedure::class])
    ->name('rpc.endpoint')
    ->middleware('auth:sanctum');

パラメータの受け取り方:Request注入と引数の自動バインディング

Requestオブジェクト経由での取得

メソッドの引数に Illuminate\Http\Request を型指定すると、params の中身が入ったリクエストが渡されます。Laravelの通常のリクエストと同じオブジェクトなので、validate() もフォームリクエストも同じ書き方で使えます。

<?php

namespace App\Http\Procedures;

use Illuminate\Http\Request;
use Sajya\Server\Procedure;

class TennisProcedure extends Procedure
{
    public static string $name = 'tennis';

    public function ping(Request $request): string
    {
        $validated = $request->validate([
            'innings' => 'required|string|max:255',
        ]);

        return $validated['innings'];
    }
}

引数名によるバインディングとネスト値のキャメルケース変換

引数名を params のキーに合わせると、Requestを経由せずに値を直接受け取れます。

public function subtract(int $minuend, int $subtrahend): int
{
    return $minuend - $subtrahend;
}

ネストした値は、ドット区切りではなくキャメルケースの引数名で受け取ります。params{"user": {"name": "Alex"}} なら引数名は $userName です。Binding::bindResolve() が引数名をそのままキーとして探し、見つからなければ Str::snake($key, '.') でドット区切りに変換して再検索する実装のためで、$user_name$name では変換後もキーが一致せず値が届きません。

解決に失敗した引数が消える挙動

バインディング結果は最後に filter() を引数なしで通してから返されます。PHPのfalsy判定で除去されるため、値が見つからなかった引数は配列から丸ごと消え、既定値のない必須引数はコンテナが解決できずに例外となります。nullが入った状態で処理が続くわけではありません。

より厄介なのは、この filter()0false・空文字・空配列も落とす点です。{"minuend": 0, "subtrahend": 2} を送ると $minuend だけが解決対象から外れます。数量や差分をゼロで送る可能性があるメソッドでは、引数バインディングを使わずRequest経由で $request->get('minuend') と取得してください。

解決ロジック自体を差し替えることもできます。RPC::bind() で任意のクロージャを、RPC::model() でモデルクラスを指定すると、後者は resolveRouteBinding() を通してモデルインスタンスを注入します。公式ドキュメントは登録先をRouteServiceProviderの boot() と説明していますが、このプロバイダはLaravel 11で廃止されました。11以降のスケルトンに残るプロバイダは AppServiceProvider だけなので、そちらの boot() に書きます。

エラー処理:標準エラーコードとHTTPステータスの扱い

JSON-RPC 2.0の標準エラーコード

仕様が定める事前定義コードは次のとおりです。Sajyaは対応する例外クラスを Sajya\Server\Exceptions 以下に持っています。

コード 意味 Sajyaの例外クラス
-32700 Parse error ParseErrorException
-32600 Invalid Request InvalidRequestException
-32601 Method not found MethodNotFound
-32602 Invalid params InvalidParams
-32603 Internal error InternalErrorException
-32000から-32099 実装定義のサーバーエラー MaxBatchSizeExceededException(-32000)

公式ドキュメントは、このうち -32700・-32600・-32601・-32602 の4種類がHTTPの404やCSRFトークン不正と同じ扱いでログに残らないと説明しています。実装を追うと対象はさらに広く、RpcExceptionreport() がtrueを返すため、Laravelの例外ハンドラは自前の報告処理が済んだものとみなして早期リターンします。つまりこの基底クラスを継承した例外はすべてログに記録されません。次項で作る自作の例外も同じです。不正リクエストの傾向やサーバー側エラーの発生数を追いたいなら、Procedureの中で明示的に記録する必要があります。

バリデーション例外の自動変換

Procedure内で投げられた例外は HandleProcedure が受け取り、種類に応じて変換します。ValidationExceptionInvalidParams(-32602)に置き換えられ、バリデータのエラーメッセージが error.data に配列で入ります。RESTの422相当の情報が、追加のコードなしで仕様準拠の形に整うということです。

それ以外の例外は次の順で処理されます。RpcException を継承していればその内容のまま返り、getStatusCode() または getCode() が500なら InternalErrorException、コードが整数でなければ -1 が割り当てられます。コードを指定せずに標準の Exception を投げると getCode() の既定値0がそのままエラーコードになるため、意図した番号を返したいなら明示してください。

独自例外の定義

同じエラーを複数箇所で返すなら、RpcException を継承した例外クラスを用意します。php artisan make:exception DivisionByZeroapp/Exceptions に雛形を作り、継承元を差し替えます。実装が必要な抽象メソッドは2つだけです。

<?php

namespace App\Exceptions;

use Sajya\Server\Exceptions\RpcException;

class DivisionByZero extends RpcException
{
    protected function getDefaultMessage(): string
    {
        return 'Division by zero.';
    }

    protected function getDefaultCode(): int
    {
        return -100;
    }
}

config/app.phpdebug が有効な間は、レスポンスの error にファイル名・行番号・スタックトレースが追加されます。本番環境で APP_DEBUG=false を徹底していないと、内部構造がクライアントへ露出します。

HTTPステータスは常に200

コントローラは response()->json($response, 200, ...) を固定で返します。メソッドが見つからなくても、パースに失敗しても、HTTPステータスは200です。ロードバランサやAPIゲートウェイでステータスコードを見てリトライやアラートを判断している場合、JSON-RPCのエラーは異常として検知されません。監視を組むならレスポンスボディの error フィールドを見る必要があります。RESTを前提にした運用基盤へ後から載せるときに最初に詰まる点です。

例外は2つあります。Route::rpc() にProcedureを継承していないクラスを渡した場合は500が返り、認証や流量制限のミドルウェアで弾かれた場合は401や429がそのとおり返ります。200固定になるのは、リクエストがProcedureの実行段階まで到達したときだけです。

バッチリクエストの上限:既定値30とCHANGELOG記載の食い違い

JSON-RPCでは、リクエストオブジェクトを配列で並べることで複数の呼び出しを1回のHTTPリクエストにまとめられます。レスポンスも配列で返り、順序は保証されないため id で突き合わせます。

[
  {"jsonrpc": "2.0", "method": "tennis@ping", "id": 1},
  {"jsonrpc": "2.0", "method": "tennis@ping", "id": 2}
]

ここで確認しておきたいのが上限値です。7.0.0のCHANGELOGには「バッチリクエストの既定上限を50件に設定した」と書かれていますが、7.0.0と7.1.0のどちらに同梱されている config/sajya.php'max_batch_size' => 30 です。ソース側の config('sajya.max_batch_size', 50) という記述の第2引数だけが50で、これは設定キーが存在しない場合のフォールバックにすぎません。ServiceProviderが必ず設定をマージするため、実際に効くのは30です。CHANGELOGの記載は7.0.0で更新が止まっており、7.1.0の項目自体が存在しない点も踏まえて、値は同梱の設定ファイルで確認してください。

上限を超えると、個々のリクエストは一切実行されず -32000 Maximum batch size exceeded. が1件だけ返ります。部分的に成功することはありません。50件まで通る前提でクライアントを作ると、本番投入後にまとめてエラーになります。PHP_INT_MAX を指定すれば無制限にできますが、1リクエストの処理時間も比例して伸びるため、実際の処理内容に合わせた値を設定してください。

通知(Notification)の実行タイミング:キューではなくレスポンス送出後

id を省略したリクエストは通知として扱われ、仕様上サーバーは応答を返しません。Sajyaも空のオブジェクトを返すだけです。

{
    "jsonrpc": "2.0",
    "method": "tennis@ping"
}

誤解されやすいのは実行される場所です。通知は HandleProcedure::dispatchAfterResponse() で処理されます。名前のとおりレスポンスをクライアントへ返した直後に、同じPHPプロセス内で実行される仕組みで、キューワーカーは使いません。5.0.2で「保留中のリクエストにキューを使わない」と明記されて以降、この挙動が維持されています。

この違いは運用に直結します。ワーカーを介さないためRedisやデータベースのキュー設定は不要ですが、失敗しても再試行はされず、failed_jobs テーブルにも残りません。失敗そのものは HandleProcedure が内部で report() を呼ぶため通常のログには出ますが、そこから再実行する手段はありません。処理が長引けばPHP-FPMのワーカーはその間解放されないため、同時接続数の上限にも影響します。確実に完了させたい処理を通知で投げるのは避け、Procedureの中から明示的にジョブをディスパッチしてください。

補助機能:テストトレイト・ドキュメント生成・gzip圧縮

テストでは ProceduralRequests トレイトを使います。setRpcRoute() でルート名を指定し、callProcedure() でメソッドを呼ぶと、ステータス200と Content-Type: application/json の検証まで済んだTestResponseが返ります。アサーションを挟まずに素のレスポンスがほしい場合は callHttpProcedure() です。

<?php

namespace Tests\Feature;

use Sajya\Server\Testing\ProceduralRequests;
use Tests\TestCase;

class PingPongTest extends TestCase
{
    use ProceduralRequests;

    public function testPingPong(): void
    {
        $this
            ->setRpcRoute('rpc.endpoint')
            ->callProcedure('tennis@ping')
            ->assertJsonFragment(['result' => 'pong']);
    }
}

APIドキュメントは #[RpcMethod] アトリビュートで説明・引数・戻り値を宣言し、php artisan sajya:docs にルート名を渡すとHTMLとして出力されます。出力先は既定ディスクの api/docs.html で、--path--name で変更できます。実ファイルの位置はLaravelのバージョンで変わる点に注意してください。公式ドキュメントは storage/app/api/docs.html と説明していますが、これはLaravel 10までの値です。11以降のスケルトンはlocalディスクのルートが storage_path('app/private') に変わったため、実際の出力先は storage/app/private/api/docs.html になります。単一エンドポイントの構成では利用できるメソッドの一覧が外から見えないため、この生成物が実質的な仕様書になります。

レスポンスを圧縮したいときは GzipCompress ミドルウェアをルートに付けます。Accept-Encoding: gzip が付いたリクエストにだけ適用されるので、ブラウザ以外のサーバー間通信で効果が出ます。実装は gzencode() を呼ぶだけなので ext-zlib が必要で、composer.jsonでも suggest として挙げられています。クライアント側はPHPなら別パッケージの sajya/client が用意されており、execute()batch()notify() でそれぞれ通常呼び出し・バッチ・通知を送れます。

Sajyaを避けるべき場面

次のいずれかに当てはまるなら、Sajyaは選ばないほうがよいと考えます。

ブラウザから直接叩く公開APIを作る場合。 単一エンドポイントへのPOSTのみという構成は、HTTPキャッシュもCDNのパス単位の制御も効きません。GETで取得できるリソースAPIが必要ならRESTを選ぶべきです。

多言語のクライアントに型定義を配りたい場合。 JSON-RPCにはスキーマ定義言語がなく、Sajyaのドキュメント生成もアトリビュートに人が書いた内容をHTML化するだけで、コード生成には対応していません。契約をコードで共有したいならgRPCConnect(connectrpc)、TypeScriptのモノレポに閉じるならtRPCのほうが目的に合います。

Laravel 10や11の古いパッチ版で止まっている場合。 前述のconflict指定により導入自体ができません。本体を10.48.28以降、または11.42.1以降へ上げることが先決です。

逆に、社内システム間の連携で、PHP側が処理の主体であり、呼び出し口を関数の集合として素直に表現したいのであれば適しています。RESTのようにリソース設計へ落とし込む手間がなく、Laravelのバリデーションと例外処理を流用しつつ仕様準拠のエラーを返せる点が実用的な価値です。

よくある質問(FAQ)

Laravel Sajyaでリアルタイム通信は実現できますか?

できません。日本語の解説記事にはSajyaをWebSocketやAjaxの代替として紹介しているものがありますが、パッケージの説明文もソースの実装もJSON-RPC 2.0サーバーであり、サーバーから能動的に送信する機能はありません。配信が必要ならLaravel Reverbとブロードキャスト機構を使ってください。

Laravel 13で使えますか?

7.1.0以降で使えます。ただしバージョン制約は上限だけでなく下限にも及び、Laravel 10系なら10.48.28以降、11系なら11.0.0ちょうどか11.42.1以降でなければcomposerが導入を拒否します。本体をパッチ版まで含めて確認してください。

routes/api.phpが存在しない場合はどうすればよいですか?

php artisan install:api を実行してください。Laravel 11以降のスケルトンにはAPIルートファイルが同梱されていないため、このコマンドで生成します。同時にLaravel Sanctumが導入されます。

メソッド名の区切り文字は変更できますか?

config/sajya.phpdelimiter を書き換えれば変更できます。既定値は @ で、ドットなど1文字であれば任意の文字を指定できます。ルートごとに変えたい場合は Route::rpc() の第3引数でも指定できます。

バリデーションエラーはどのような形式で返りますか?

フィールド名をキーにしたメッセージの配列が error.data に入ります。実際のレスポンスは次の形です。

{
  "id": "1",
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "innings": ["The innings field is required."]
    }
  },
  "jsonrpc": "2.0"
}

関連記事

資料請求

RELATED POSTS 関連記事