Laravel4、URIサフィックスによるレスポンス形式変換のヒント

タグ: Laravel4  

URLの最後にファイルの拡張子のようにサフィックスが付いている場合、自動的にその形式に出力を変換したいという要望が時々ツイッターや本家フォーラムに上がります。先日も、ツイートされていました。

Laravelのコアにもバージョン3の頃から、たまに要望が上がりますが、今のところ受け入れられません。

他のフレームワークには存在しているものもあります。Laravelの場合は「今の時代、JSON形式だけがあれば事が足りるだろう。」という考え方です。ですから、JSON形式を返すには便利なフレームワークになっているのです。レスポンスを作成するために、Respons::json()が存在しています。Eloquent ORMで取得したコレクションをレスポンスの代わりにReturnすれば、JSONのレスポンスが返されます。

私も今はこの考え方に賛成しています。Laravelに移った当時は、サフィックスによるレスポンスの変換機能が存在しないことが物足りませんでしたが、JSON以外を必要とすることは今のところ無いため、Laravelの考え方はシンプルで良いと考えています。

この機能を実現したい方のためにヒントとなるサンプルコードを紹介します。ただし、自分自身では使用しないため、完全なものではありません。

自動変換する

グローバルBeforeフィルターを使用します。app/filters.phpで定義されています。デフォルトでは何も指定されていません。

通常のフィルターでは、その中から値を返さない、もしくはnullを返した場合、以降の処理を続行し、レスポンスなどを返した場合は、ルートの処理を行いません。グローバルBeforeフィルターは、全リクエストの一番最初にチェックされるフィルターです。

サンプルコードは以下の通りです。

app::before( function( $request )
{
    // アイデアだけを示すため、必要なチェックは省きます。
    if( preg_match( '/(.+).json/', $request->path(), $matches ) )
    {
        // URI最後の".json"を除いたURIで、新しいリクエストを作成する。
        $request = Request::create($matches[1], 'GET', $_GET, $_COOKIE, $_FILES, $_SERVER, $request->getContent());

        // 作成したリクエストでルーティングを実行する。
        $response = Route::dispatchToRoute( $request );

        // レスポンスから配列を拾得、$respons->getContent()でコンテンツを取得できる。
        $data = ['content' => $response->getContent() ];

        // 返ってきたレスポンスのコンテンツをjsonレスポンスで送信。
        return Response::json( $data );
    }
} );

Request::create()の引数は、多分こんなものだろうと思いますが、詳しく調べていません。レスポンスオブジェクトのURIをpath()で取得し、サフィックスが".json"であれば、それを取り除いたURIで新しいリクエストを作成します。(HTTP変数ごとに指定する内容が異なるかも知れません。また、これでhttpsを使用時やクエリー文字列がきちんと渡されるかなども調べていません。)

そのリクエストでルーティング処理を行います。ルーティングを実行し、コントローラーなり、クロージャールートなりの内容が実行されると、レスポンスオブジェクトが返ってきます。

レスポンスオブジェクトの内容はgetContent()で取得できますので、それを利用し適当に配列を組み立てます。(その変換処理は、".json"抜きのルートが何を返すのかにより異なるでしょうから、ここではダミーの配列を渡しています。)それをRespons::json()で新しいJSONレスポンスオブジェクトを組み立てリターンします。レスポンスをリターンしていますので、それ以降のルーティング処理は行われません。(ここでも手を抜いています、本来はレスポンスオブジェクトのレスポンスコードを返すべきかも知れません。逆に、このままデフォルトの200を返す考えで良いのかも知れません。)

これでルートを定義し、".json"付きで呼び出せば、一応JSONレスポンスが返ります。(一応と言っているのは、ロジックを正しく入れているわけでも、テストしているわけでもないからです。Beforeフィルターも、Afterフィルターも無視されます。多分、全てうまく行うには、Laravelのルータークラス(Illuminate\Routing\Router)を拡張し、必要な処理を入れ、URIに拡張子が指定された場合は、その拡張したルータークラスのメソッドを呼び出すのが一番簡単だと思われます。)

例えば:

Route::get( 'aaa', function()
{
    return "aaa entered";
} );

これでURIの'aaa'にアクセスすれば、"aaa entered"が返されます。'aaa.json'にアクセスすれば、{"content": "aaa entered"}が返ってきます。

ルートを分ける

ルートを分ける方法は、見つけられませんでした。ルーターを拡張するのが一番早いかなと思います。

もしくは、登録済みの全ルートに、どこかのタイミングで".json"付きURIのルートを自動的に追加するかでしょう。

いずれの方法でも、実用レベルのコードを見つけましたら、このページからリンクします。

Laravel的手法

Laravelの機能をそのまま活かすのであれば、一番簡単な方法は同じルートでリクエストがJSON形式なら、レスポンスもJSONで返すことでしょう。

Route::get( 'aaa', function()
{
    if( Request::isJson() )
    {
        return Response::json( ['content' => 'aaa entered' ] );
    }
    return "aaa entered";
} );

WebブラウザからURIの'aaa'にアクセスすれば、文字列が表示されます。アクセス時のコンテンツタイプがJSONと推測される場合は、JSON形式で返ってきます。例えば:

$> curl localhost:8000/aaa -H "Content-Type: something/json"

{"content":"aaa entered"}

私は、個人的にはこの方法が一番スマートで気に入っています。(追記)もっと好きなのはAPI系は別のURIにすることです。