Slim3における拡張子付きURIのマッピング

CoolなURIでJSONを返すURLはどう表現するか

内部的には「/user/get?id=12345&type=json」となるようなURI(idが12345のユーザ情報のJSON形式)を、Cool URIではどのように表現するのが適切か悩んでいた。「/user/json/12345」や「/user/12345/json」はなんか違う気がするし。
そしたら「Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESSプラスシリーズ)」にその答えが書いてあった。

例えば1つのリソースをHTMLとテキストとJSONで表現できる場合には、それぞれ「.html」「.txt」「.json」という拡張子を付けてここの表現を分けると良いでしょう。

「Webを支える技術」 p.062

先述の例で言えば、「/user/12345.json」となる。なるほど確かにこれはしっくりくる。ということで、txtとjsonの拡張子に対応したURIの実装を、Slim3で試した。

URIマッピングはRouterImpl#addRouting()で設定

まず、Slim3でのURLマッピングは、RouterImplを実装したAppRouter内のaddRouting(from, to)で下記のように設定する。

   public AppRouter() {
       addRouting("/entry/delete/{key}", "/entry/delete?key={key}");
   }

※注意点として、fromとtoにはそれぞれひとつ以上の{xxx}を含める必要があるみたい。ひとつも含まないと正しくマッピングが行われなかった。

ただし、拡張子を含んだURIは、Slim3はデフォルトで静的リクエストとみなすので、マッピングが行われない。そこで、AppRouterにさらに手を加える必要がある。

拡張子を含むURIはIsStatic()をオーバーライドする

拡張子つきURIが静的リクエストと見なされるのを回避するには、RouterImpl#IsStatic()をオーバーライドして、処理を記述する。拡張子が.jsonのURIを静的とみなさないようにするには、下記のようなコードを書けば良い。

   @Override
   public boolean isStatic(String path) throws NullPointerException {
       boolean isStatic = super.isStatic(path);

       if("json".equals(RequestUtil.getExtension(path)) || "txt".equals(RequestUtil.getExtension(path))) {
           return false;
       } else {
           return isStatic;
       }
   }

そのうえで、addRouting()は下記のように設定。

   public AppRouter() {
       addRouting(
               "/entry/{id}\\.{extension}",
               "/entry/get?id={id}&extension={extension}");

   }


上記によりマッピングされるGetControllerは以下のような感じ。

public class GetController extends Controller {

   @Override
   protected Navigation run() throws Exception {
       String extension = request.getParameter("extension");
       String id = request.getParameter("id");
       if ("json".equals(extension)) {
           response.setContentType("application/json");
           // responseにJSONを書き込む処理
       } else if("txt".equals(extension)) {
           response.setContentType("text/plain");
           // responseにテキストの内容を書き込む処理
       } else {
           // エラー
       }
       return null;
   }
}

これで、「/user/12345.json」ならjson形式で、「/user/12345.txt」ならテキスト形式でid=12345のユーザ情報が取得できる。対応する拡張子が増えたら、AppRouter#IsStatic()とGetControtter#run()の分岐に追加すればいい。

拡張子によってControllerを分ける場合

run()の中で分岐せず、拡張子によってマッピングするコントローラ自体を切り替えたいなら、下記のようにaddRouting()すればできる。

addRouting("/entry/{id}\\.{extension}", "/entry/get{extension}?id={id}");

そのうえで、GetjsonControllerやGettxtControllerを実装すればいい。この方法だとrun()内で分岐しないのでコードがすっきりするけど、マッピングの都合上、コントローラのクラス名がPascal形式(GetJsonControllerとかGetTxtController)にできないのが惜しい。(Getつけなければいいんだけど)

次は、httpメソッドに応じてマッピング先のControllerを切り替える実装について試行錯誤する予定。Controller#isGet()とかを使わずに。