Symfony
SymfonyDay 3

SimpleApiBundleを作っている話

Symfony Advent Calendar 2018 3日目の記事です。
昨日は@77webさんのSymfony4のautowiringで狙ったinterfaceを確実に注入させる方法でした。

はじめに

一人でそこまで大規模ではないアプリケーションを作る事がおおいのですが、Symfonyで簡単なAPIを実装したくなる場合にどう実装したらいいのか?
結構悩むことが多いです。

要件的には以下のような感じです。

  • JSONでレスポンスする
  • POST時のrequestのbodyに{"name":"xxx","description":"yyy"}な形で入ってきたものを受け取る(bodyParser的なものが必要)
  • ControllerでAnnotationを使いAPIかどうかを指定したい

2つの方法

SymfonyでAPIな実装(jsonをレスポンスするような実装)をする場合に僕が思いつく方法が2つあります。

  1. JsonResponse()オブジェクトを返す
  2. FOSRestBundleを使う方法

JsonResponse()オブジェクトを返す

これは非常にシンプルな方法でコントローラで以下のように書くだけです。

<?php
class MainController
{
  public function index() 
  {
     return new JsonResponse(['message' => 'hello, world');
  }
}

確かにResponseをJSONで返すだけならこの方法で十分です。
しかし、POST時のデータの受取まで考えると、ちょっと微妙ですね。

FOSRestBundleを使う

SymfonyにはFOSRestBundleという素晴らしいBundleがあります。
https://github.com/FriendsOfSymfony/FOSRestBundle

ただ、今回の簡単なAPI実装ではちょっと機能が多すぎるのとAnnotationを活用出来ないのであまり積極的に使う気になれません。

SimpleApiBundle

polidog/SimpleApiBundle

最初に提示した3つの要件を満たすために作ったBundleです。

一番最初に求めたものは「@APIアノテーションを付けたら、jsonレスポンスする」ということでした。
@APIを外して@Templateに付け替えたら簡単にjsonからhtmlに切り替えられるみたいな。
すべてjsonで返すわけではなくて、一部はTwig使ってHTMLを返したいという要件がよくあって、それを簡単に実現したいのです。

使い方

README.mdを見ていただければすぐに分かると思いますが、単純にjsonのレスポンスを返すだけなら以下のとおりです。

// UserController.php

    /**
     * @Route("/user/{id}")
     * @Api()
     */
    public function me($id): array
    {
        $user = $this->userRepository->find($id);
        return [
            'id' => $user->getId(),
            'name' => $user->getUsername(),
            'avatar' => $user->getAvatar(),
        ];
    }

201レスポンス

annotationの引数statusCodeにステータスコードを指定するだけです。

    /**
     * @Route("/user/post", methods={"POST"})
     * @Api(statusCode=201)
     */
    public function post(Request $request): array
    {
        // TODO save logic.
        return [
            'status' => 'ok',
        ];
    }

ステータスコードの問題点

現状のSimpleApiBundleではannotationで宣言的に書かれているstatusCodeですが、動的に変化させたいケースもあります。
例えばバリデーションのチェックで失敗した場合は400でレスポンスコード返したいとか、実行時に決まるステータスコードのどう扱うべきか。

現状ではStatusCodeを変更する方法は一つしかなくてExceptionを発生させることです。
Exceptionのcodeが0以外は、ResponseオブジェクトのstatusCodeに反映するように実装しています。
POSTやGETを同じコントローラのアクションに記述するのは、アクションに複数の責務をもたせており、分離したほうがいいわけで、そう考えると、StatusCodeを変更するというのは、基本的にエラーが発生する場合のみであるべきなのではないでしょうか?

ということで現状は宣言的にStatusCodeを設定できるぐらいで十分な気もしてきました。

 最後に

Bundleを自分で作ると、Symfonyをより深く学ぶ事が出来ます。
SimpleApiBundleはかなりFOSRestBundleを参考にしていますし、僕はFOSRestBundleから多くのことを学びました。
年末年始にお時間のある方は小さなBundle作ってみてはいかがでしょか?