Edited at
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作ってみてはいかがでしょか?