(24日の記事に時間がかかるという事で急遽追加しました!)
はじめに
今年BEAR.SundayのAPI開発プロジェクトに参加しました。その時のBEAR.Sunday APIアプリケーションの構成を共有します。
- バックエンド - BEAR.Sunday
- フロントエンド - React Native
- APIアーキテクチャ - Hypermedia API (HAL)
フレームワーク作者がやる事だからといって、これがベストだという事はありません。
チームの方針に基づいて自分たちの問題を、自分たちの方法で解決するのが大切だと思います。1 BEAR.Sundayはそのためのフレームワークです。
以下、参考になるところは参考にしてください!
ルーター
Aura.Router
は使いませんでした。WebRouter
のみです。
例えばユーザーなら
/users/koriym
ではなく
/users/?id=koriym
と表しました。前者がPHPの関数にような順番引数で、後者はパラメータ名を指定する名前付き引数です。一般的なプラクティスでは無いですが、特に困ったことも無いばかりか、メリットが多くありました。
- パスがクラス、パラメータが引数にマップされ、その解釈に定義が不要
- URIのパースの手間が要らないのと、URIの組み立てが単純
- ルーター分のCPUコスト削減
Webの記事のURIなどエントリーポイントになるところではともかく、特にAPIではクエリーパラメータで良いと考えました。
順番による関係性(yy/mm/dd)などをURIで表すことはできませんが、単純で簡単になるメリットを実感しました。
ResourceInject の不使用
全てのResourecOjbect
は@Embed
で"インジェクト"して$this-resource->get
という風に外部から取得はしませんでした。
読み込み(GET)リソースの時だけではなく、それ以外のメソッドの時も@Embed
でインジェクトして利用した後にunset
しました。
リソースがどのリソースに関係しているかコードを読む前にアノテーションに示されています。
セッションの不使用
Auth0
を使いセッションを使いませんでした。
セッション不使用なのでログインしてページでも/mypage
ではなく/mypage/?id=koriym
などのように必ずIDをURIに付けました。これはログに有利に働きました。
Ray.Queryの使用、 PDO(Aura.Sql)の不使用
リソースオブジェクトにPDO
オブジェクトをインジェクトしてDB操作をする代わりに、PDO
オブジェクトがインジェクトされたクエリーオブジェクトを用いました。クエリーオブジェクトはSQLがSQL実行オブジェクトに変換されたものです。レポジトリーパターンでメソッドが1つしかなく、SQLから変換されたものと考えれば良いと思います。
フォルダにまとめられたSQLは最初に単体で作成して単体でテストします。希望通り動作するSQLが出来たらPHPで利用します。SQLに限らず、中から外へ向かって開発します。単純なものの組み合わせで複雑な物を作ります。ランタイムでの実行順に開発する必要はありません。
DBエンジニア
DBエンジニアが基本的に全てのDBのテーブルスキーマを定義し、SQLを記述しました。アプリケーションエンジニアはそのSQLを使うのみです。
バリデーション
全てのリソースをJsonSchema
で定義しました。スキーマからはFake JSONが生成する事もできますし、Fake APIサーバーを立ち上げることもできます。
JsonSchema
はreact-jsonschema-form
を使い、フォームを生成することもできます。
PHPのバリデーションライブブラリはほとんど使いませんでした。
(OpenAPIとJSON Schemaを比較した記事があります。参考に)
https://phil.tech/api/2018/03/30/openapi-and-json-schema-divergence/
SSR
Next.jsを使いましたが、余分なラウンドトリップが回避できる分、V8Jsの方が良かったかもと思います。
問題空間、解決空間
PHPは問題を解決するための解決空間の場です。PHPプロジェクトとは別に解決するドメインの問題そのものを定義した"スペックレポジトリ"を作り、それを問題空間として別に用意しました。(OPEN APIの定義書のように考えてもらってOKです)
ドメインをリソースや状態遷移としてJSONで表わし、それはマシンでも人でも読む事が出来ます。そのレポジトリはプロジェクトの依存とするのでvendor/
以下に配置されます。解決空間での行いは、問題空間での定義に基づいた物になります。スペック(問題空間レポジトリ)はバックエンドエンジニアだけでなく、フロントエンジニアやPO、UXデザイナも共有します。
問題空間がリポジトリとして用意されバージョン管理される事で、誰がどういう風にドメインの問題を変更したかという事が明らかになります。(アプリケーション状態遷移図もこのレポジトリに含めます)問題空間が更新されたなら、解決空間のコード(PHP,SQL,JS)も更新される必要があリます。両者のリポジトリは追従する関係になります。
ハイパーメディアAPI
これが一番の特徴です。単なるCRUD(RPC) APIではなく、リンクで遷移するハイパーメディアAPIを採用しました。
メディアタイプにはHALを使いました。HALは基本的には読み込み(safe)専用のハイパーメディアです。GETリクエストの時はURIは全てサーバーから与えられた物を使い、クライアントで組み立てません。これはテストも同様です。
しかし書き込み(unsafe, idempotent)のリクエストの時にはこれを諦めています。URIはサーバーから与えられますが、サブミットする値はクライアント側で組み立てました。REST APIとして、これは妥協ですが(好きな言い方では無いですが)最初の本格的ハイパーメディアAPI実装の妥当な落とし所だと思いました。
リソースはリソースグラフとして提供されます。例えば/news
リソースには/news/weather
と/news/sports
という2つのリソースが@Embed
で埋め込まれています。このグラフのおかげで複雑な構造を物リソースの実装も、テストも容易でした。HALはリソースをグラフとして表せる_embedded
と_links
しかありませんが十分パワフルな事を実感しました。生産性とメンテナンス性に大きな力を発揮しました。
終わりに
使わなくてすむものは使わない。常にシンプルな方法を選ぶ。既存のプラクティスに過度に囚われない。
そうやって出来上がったコードは極端なぐらいシンプルなものになりました。
BEAR.Sundayをプロダクションに使っている方に話を聞くと、その使われ方は様々で、前述したようにそれで良いと思います。CRUD APIでも、ORMでレポジトリパターンを使っても、SQLを直接ResourceObecjetにコーディングしてもOKです。多少クリーンコード的な事から外れてもリソースという"コンテキスト境界"がサービスを密結合から(多少)守ってくれます。
ここではプロジェクトの一例を紹介しました。部分的に取り入れても、全てに疑問を感じてもそれはアプリケーションアーキテクトの自由です。
おまけ:参加した熟練Railsエンジニアによると「今まで自分の参加したRailsのプロジェクトのざっくり倍以上の生産性。さくさくAPIが出来ていく。」という事でした。本当でしょうか? お確かめになってください!
ハイパーメディアAPIの設計、実装の詳細は25日のAdventカレンダーでご紹介します。
お楽しみに!
-
PHPの作者Rasmusさんも "all frameworks that are for general purposes are not optimized for everbody's needs."と言ってます! https://www.phpclasses.org/blog/post/226-4-Reasons-Why-All-PHP-Frameworks-Suck.html ↩