API Platformで特定のエンティティにだけカスタムNormalizerを適用する
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:34
(H: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",
:
:
}
無事、期待どおりの動作を実現できました👌
Discussion