🎻

API Platformで特定のエンティティにだけカスタムNormalizerを適用する

2022/08/23に公開約5,100字

2022/09/01 追記

  • 今回やりたかった日付プロパティのフォーマット変更だけなら、カスタムNormalizerを書くまでもなく、プロパティに #[Context([DateTimeNormalizer::FORMAT_KEY => 'H:i'])] をつける だけで対応できることに気づきました🙏
  • アトリビュートではなくYAMLファイルで設定したい場合は以下のような書き方で対応できます
    • ドキュメントのどこにも書いておらずググっても誰も言及していなかったので この辺のコード を判読しました🙄
    • コンテキストの定義の側にも対象のgroupsを書かないといけない仕様で、記述が重複してしまうので、下記の例ではYAMLのanchor/aliasを使ってDRYにしています
App\Entity\Foo:
  attributes:
    timeProperty:
      groups: &groups
        - foo:read
        - foo:write
      # - etc...
      contexts:
        - groups: *groups
          context:
            datetime_format: H:i
  • また、そもそもすべての日付系プロパティのデフォルトのフォーマットが初期設定だと Y-m-d\TH:i:sP なので、これを Y-m-d H:i:s に変更したい場合は、ここに書かれている要領で 以下のような設定をすればOKです
# config/packages/framework.yaml
framework:
  serializer:
    default_context:
      datetime_format: Y-m-d H:i:s

追記ここまで


API Platform で以下のシチュエーションに遭遇してカスタムNormalizerを書いたのでメモです。

やりたかったこと

  • DBAL Typeが time で型が \DateTimeInterface なプロパティを持つエンティティがあった
  • このエンティティをシリアライズすると、標準の動作では 1970-01-01T12:34:56+09:00 のような文字列になった
  • これを 12:34H:i)形式の文字列になるようにしたかった

やったこと

API Platform標準のNormalizerをデコレートしたカスタムNormalizerを書きました。

API Platformでは、公式ドキュメント に詳細が書かれているとおり、Symfony Serializer を使ってオブジェクトのシリアライズが行われています。

なので、Symfony Serializerの流儀に従って カスタムNormalizerを書けば OKです。

まず、src/Serializer/Normalizer/FooNormalizer.php を以下のような内容で作成します。

<?php

declare(strict_types=1);

namespace App\Serializer\Normalizer;

use ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer as JsonLdItemNormalizer;
use ApiPlatform\Core\Serializer\ItemNormalizer;
use App\Entity\Foo;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

class FooNormalizer implements NormalizerInterface
{
    // API Platform標準のNormalizerを、application/json用とapplication/ld+json用の2つインジェクト
    public function __construct(private ItemNormalizer $itemNormalizer, private JsonLdItemNormalizer $jsonLdItemNormalizer)
    {
    }

    public function normalize(mixed $foo, string $format = null, array $context = []): array
    {
        // フォーマットに応じた標準Normalizerで一旦ノーマライズする
        $data = (array) match ($format) {
            JsonLdItemNormalizer::FORMAT => $this->jsonLdItemNormalizer->normalize($foo, $format, $context),
            default => $this->itemNormalizer->normalize($foo, $format, $context),
        };

        // 対象のプロパティの値だけ書き換える
        $data['timeProperty'] = (new \DateTime($data['timeProperty']))->format('H:i');

        return $data;
    }

    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
    {
        // このカスタムNormalizerはFooエンティティについてのみ実行される
        return $data instanceof Foo;
    }
}

API Platform標準のNoarmalizerは以下の6種類が用意されています。これらのうち必要なものをインジェクトして、normalize() メソッド内でフォーマットに応じて使い分けるようにします。

フォーマット クラス
デフォルト ApiPlatform\Core\Serializer\ItemNormalizer
jsonld ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer
jsonapi ApiPlatform\Core\JsonApi\Serializer\ItemNormalizer
hal ApiPlatform\Core\Hal\Serializer\ItemNormalizer
graphql ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer
elasticsearch ApiPlatform\Core\Bridge\Elasticsearch\Serializer\ItemNormalizer

あとは config/services.yaml で以下のように登録してあげればOKです。

services:
  App\Serializer\Normalizer\FooNormalizer:
    arguments:
      - '@api_platform.serializer.normalizer.item'
      - '@api_platform.jsonld.normalizer.item'
      # - '@api_platform.jsonapi.normalizer.item'
      # - '@api_platform.hal.normalizer.item'
      # - '@api_platform.graphql.normalizer.item'
      # - '@api_platform.elasticsearch.normalizer.item'
    tags: [{name: serializer.normalizer, priority: 1}]

Symfony Serializerの公式ドキュメント で言及されているとおり、Symfony\Component\Serializer\Normalizer\NormalizerInterface を実装しているクラスは自動で serializer.normalizer でタグ付けされるので、最後の行はなくても動きそうですが、API Platform標準のNormalizerではなく必ずカスタムNormalizerが適用されてほしい ので、priority を明示するためにあえて書いています。

結果

Accept: application/ld+json でリクエストしたとき:

{
    "@context": "\/api\/contexts\/Foo",
    "@id": "\/api\/v1\/foos\/1",
    "@type": "Foo",
    "timeProperty": "12:34",
    :
    :
}

Accept: application/json でリクエストしたとき:

{
    "timeProperty": "12:34",
    :
    :
}

無事、期待どおりの動作を実現できました👌

GitHubで編集を提案

Discussion

ログインするとコメントできます