CHANSHIGELOG

いろんなこと

HalJsonResponseBundleを作ってみている話

Symfony Advent Calendar 2021 - 22日目の記事です。
昨日は @ttskch さんの symfony/consoleを使えばCLIツールが超簡単に作れる! | blog.ttskch でした !

はじめまして!主に九州・福岡/宮崎で、食品宅配や飲食関連のEC/システム開発を行いつつ、現職の新拠点開設に向けて色々やっているちゃんしげです。アドベントカレンダー初参戦となります。

はじめに

普段の業務では、Laminas(ZendFramework)やLaravel、社内の一部システムでBEAR.Sundayをつかって開発をしていますが、さまざまなところで恩恵を受けているSymfonyへの愛が芽生えてきたことと、中・大規模の新規プロジェクトでも利用したく理解を深めていく一つの方法としてResponseに関わるBundleを作ってみることにしました。

なぜ application/hal+json なのか

最近では、プロジェクトの大小問わずAPI開発をすることが多くなってきていて、REST APIを素直に取り組むには、BEAR.Sundayでも採用されている「ハイパーメディアAPI」の考え方で開発をしたいと思ったことから HALを選択しました。ハイパーテキストが、複数のテキストを相互に関連づけ、結びつける仕組みであるように、HALはリソース同士を関連づけ、リンクを辿って情報を取得・操作する一連の制約を持たせることができるため、クライアント(利用者)側がAPIの使い方を事前にしらなくても、API自身で使い方(情報の関連性などetc...)を表現できることが利点だと思います。

作りたいもの

  • レスポンスのContent-typeをapplication/hal+jsonとしたい
  • リソースに関連するLinkをAnnotationで指定したい
  • リソースのステータスコードもAnnotationで指定したい

※関連するリソースを埋め込むEmbedについては今後考えます

Annotation周りは、PHP8から使えるAttributeで実装しようと考えました。 例として、リソースURI/greeting、パラメーターは ?username=example_name のみ受け付けるGreetingControllerを、以下のように実装した場合...

declare(strict_types=1);

namespace App\Controller;

use App\Domain\Greeting\Input;
use Chanshige\HalJsonResponseBundle\Annotation\Link;
use Chanshige\HalJsonResponseBundle\Annotation\Response;
use Symfony\Component\Routing\Annotation\Route;

use function sprintf;

class GreetingController
{
    #[Route(path: '/greeting', methods: ['HEAD', 'GET'])]
    #[Response(statusCode: 200)]
    #[Link(rel: 'user', href: '/user/{username}')]
    public function index(Input $input): array
    {
        return [
            'greeting' => sprintf('Hello %s', $input->name()),
            'username' => $input->name()
        ];
    }
}

※上記の Inputみたいな実装は1つ前の記事に書いています。ぜひこちらもご覧ください!

こんなレスポンスが帰ってくるようにしたい。

< Content-Type: application/hal+json
{
    "greeting": "Hello chanshige",
    "_links": {
        "self": {
            "href": "/greeting?username=chanshige"
        },
        "user": {
            "href": "/user/chanshige"
       }
}

では、作ってみる!

どう作っていくのが良いのだろう?とGoogle先生をつかって調べてたんですがよくわからなかったので、困ったときの公式The Bundle Systemをみつつ、symfony/skeletonでSymfony6.0を立ち上げて、bundle開発用のディレクトリをきって、namespaceを設定しました。

※ namespaceについてもなんでも良いわけではなく、命名規則があります。

composer.json

    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "Chanshige\\": "bundles/"
        }
    },

次にBundleのディレクトリ構造は、バンドル間でコードの一貫性を保つために規則があるようなので、すでに公開されているBundleライブラリをみながら、このような感じですすめています。(めっちゃ途中です...)

bundles
└── HalJsonResponseBundle # bundle本体
    ├── Annotation
    │   ├── Link.php
    │   └── Response.php
    ├── DependencyInjection
    │   └── HalJsonResponseExtension.php
    ├── EventListener
    │   └── AnnotationSubscriber.php
    ├── Extend
    │   └── UriTemplate.php
    ├── HalJsonResponseBundle.php
    ├── Handler
    │   └── JsonResponseHandler.php
    └── Resources
        └── config
            └── services.yml

流れとして、Annotation(Attribute)とコンテンツ(Body)は、kernel.controllerイベントで拾いまとめて、kernel.viewイベントでそれらを合成したresponseをセットしにいく形で考えています。

※EmbedはsubRequestとかを使えばいけそうかな?と想像...

進めてはみたものの...

今日までに完成させるつもりでしたが、色々とありで間に合いませんでした...!!!!! 🙇‍♂️ が、実際にBundleライブラリとして公開できる形まで持っていこうと思いますので、実装過程はchanshige/symfony_appリポジトリにて随時コミットしていきつつ、次回のブログで実装について書いていく予定にしました!(秒で気持ち切り替えました)

github.com

初回からとても中途半端になってしまいましたが、Symfony本体のコードリーディングを行っていくうち、”ほぉ〜こうなってるのか”みたいな納得感や、仕組みとして上手く出来てるなあ〜とめちゃくちゃ勉強になることが多く感じました。色々理解を深めていこうと思います。

最後まで読んでいただき、ありがとうございました! 明日は@ippey_s さんです!お楽しみにー!