API Platformのシリアライゼーションプロセス

全体的なプロセス

API プラットフォームは Symfony シリアライザー コンポーネントを取り入れて拡張し、(ハイパーメディア) API 応答で PHP エンティティを変換します。

主なシリアライゼーション プロセスには、次の 2 つの段階があります。

上の図からわかるように、配列は中間者として使用されます。このように、エンコーダーは特定のフォーマットを配列に、またはその逆に変換することのみを処理します。同様に、ノーマライザーは特定のオブジェクトを配列に変換したり、その逆を処理したりします。 The Symfony documentation

Symfony 自体とは異なり、API プラットフォームは、カスタム ノーマライザー、そのルーター、および状態プロバイダー システムを活用して、高度な変換を実行します。生成されたドキュメントには、リンク、タイプ情報、ページネーション データ、または利用可能なフィルターなどのメタデータが追加されます。

API Platform Serializer は拡張可能です。他の形式をサポートするために、カスタム ノーマライザーとエンコーダーを登録できます。既存のノーマライザーを装飾して、その動作をカスタマイズすることもできます。

利用可能なシリアライザ

  • JSON-LD serializer [api_platform.jsonld.normalizer.item]

JSON-LD (JavaScript Object Notation for Linked Data) は、JSON を使用して Linked Data をエンコードする方法です。これは World Wide Web Consortium の推奨事項です。

  • HAL serializer [api_platform.hal.normalizer.item]
  • JSON, XML, CSV, YAML serializer (using the Symfony serializer) [api_platform.serializer.normalizer.item]

The Serialization Context, Groups and Relations

API プラットフォームでは、Symfony シリアライザーが使用する [$context] 変数を指定できます。この変数は、正規化 (読み取り) および非正規化 (書き込み) プロセス中に公開されるリソースの属性を選択できる便利な [groups] キーを持つ連想配列です。これは、Symfony シリアライザー コンポーネントのシリアライゼーション (およびデシリアライゼーション) グループ機能に依存しています。

グループに加えて、Symfony シリアライザーでサポートされている任意のオプションを使用できます。たとえば、[enable_max_depth] を使用してシリアル化の深さを制限できます。

Configuration

他の Symfony および API プラットフォーム コンポーネントと同様に、Serializer コンポーネントは、注釈、XML または YAML を使用して構成できます。注釈はわかりやすいので、以下の例で使用します。

注: API プラットフォーム ディストリビューションを使用していない場合は、シリアライザー構成で注釈サポートを有効にする必要があります。

# api/config/packages/framework.yaml
framework:
    serializer: { enable_annotations: true }

Symfony Flexを使用している場合は、[composer req doctrine/annotations] を実行するだけで準備完了です!

YAML または XML を使用する場合は、シリアライザー構成にマッピング パスを追加してください。

# api/config/packages/framework.yaml
framework:
    serializer:
        mapping:
            paths: ['%kernel.project_dir%/config/serialization']

API システムで使用するグループを指定するのは簡単です。

正規化コンテキストと非正規化コンテキスト属性をリソースに追加し、使用するグループを指定します。ここで、それぞれ [read] と [write] を追加していることがわかります。任意のグループ名を使用できます。

グループをオブジェクトのプロパティに適用します。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    normalizationContext: ['groups' => ['read']],
    denormalizationContext: ['groups' => ['write']],
)]
class Book
{
    #[Groups(['read', 'write'])]
    public $name;

    #[Groups('write')]
    public $author;

    // ...
}

前の例では、[name] プロパティはオブジェクトの読み取り ([GET]) 時に表示され、書き込み ([PUT] / [PATCH] / [POST]) にも使用できます。 [author] プロパティは書き込み専用になります。シリアル化された応答が API によって返された場合は表示されません。

内部的には、API プラットフォームは正規化プロセス中に [normalizationContext] の値をSerializer::serialize() メソッドの 3 番目の引数として渡します。 [denormalizationContext] は、非正規化 (書き込み) 時に Serializer::deserialize() メソッドの第 4 引数として渡されます。


クラスのプロパティのシリアル化グループを構成するには、the Symfony Serializer’s configuration files or annotationsを直接使用する必要があります。

[groups] キーに加えて、[$context] パラメーター (@MaxDepth アノテーションを使用する場合の [enable_max_depth] キーなど) を使用して、任意の Symfony シリアライザー オプションを構成できます。

指定したシリアライゼーション グループとデシリアライゼーション グループは、組み込みアクションと Hydra ドキュメント ジェネレーターによっても活用されます。

Using Serialization Groups per Operation

操作ごとに、正規化および非正規化のコンテキスト (およびその他の属性) を指定できます。 API プラットフォームは、常に最も具体的な定義を使用します。たとえば、正規化グループがリソース レベルと操作レベルの両方で設定されている場合、操作レベルで設定された構成が使用され、リソース レベルは無視されます。

次の例では、[GET] 操作と [PUT] 操作に異なるシリアル化グループを使用しています。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Put;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(normalizationContext: ['groups' => ['get']])]
#[Get]
#[Put(normalizationContext: ['groups' => ['put']])]
class Book
{
    #[Groups(['get', 'put'])
    public $name;

    #[Groups('get')]
    public $author;

    // ...
}

リソース レベルで定義された構成が継承されるため、[GET] 操作中に生成されるドキュメントには [name] プロパティと [author] プロパティが含まれます。ただし、[PUT] リクエストを受信したときに生成されるドキュメントには、この操作の特定の構成のため、[name] プロパティのみが含まれます。

詳細については、Operationsドキュメントを参照してください。

Embedding Relations

デフォルトでは、API プラットフォームで提供されるシリアライザは、参照解除可能な IRI を使用してオブジェクト間の関係を表します。追加の HTTP リクエストを発行することで、関連するオブジェクトの詳細を取得できます。ただし、パフォーマンス上の理由から、クライアントに余分な HTTP 要求を強制的に発行させない方がよい場合があります。

注: この機能の代わりにVulcain を使用することを強くお勧めします。 Vulcain を使用すると、複合ドキュメントに依存するよりも高速 (より優れたヒット率) で優れた設計の API を作成でき、API プラットフォーム ディストリビューションですぐにサポートされます。

次の JSON ドキュメントでは、bookからauthorへの関係はデフォルトで URI で表されます。

{
  "@context": "/contexts/Book",
  "@id": "/books/62",
  "@type": "Book",
  "name": "My awesome book",
  "author": "/people/59"
}

シリアライゼーション グループを使用して、関連するオブジェクト (全体、または一部のプロパティのみ) を親の応答に直接埋め込むことができます。次のシリアル化グループ アノテーション ([#[Groups]]) を使用することで、著者の JSON 表現が書籍の応答に埋め込まれます。著者の属性のいずれかが [book] グループにあるとすぐに、著者が埋め込まれます。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(normalizationContext: ['groups' => ['book']])]
class Book
{
    #[Groups('book')]
    public $name;

    #[Groups('book')]
    public $author;

    // ...
}
<?php
// api/src/Entity/Person.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource]
class Person
{
    #[Groups('book')]
    public $name;

    // ...
}

以前の設定を使用して生成された JSON は以下のとおりです。

{
  "@context": "/contexts/Book",
  "@id": "/books/62",
  "@type": "Book",
  "name": "My awesome book",
  "author": {
    "@id": "/people/59",
    "@type": "Person",
    "name": "Kévin Dunglas"
  }
}

このような埋め込み関係を最適化するために、デフォルトの Doctrine 状態プロバイダーは [EAGER] とマークされた関係でエンティティを自動的に結合します。これにより、関連するオブジェクトをシリアル化するときに余分なクエリを実行する必要がなくなります。

リレーションをメインの HTTP レスポンスに埋め込む代わりに、HTTP/2 サーバー プッシュを使用してクライアントにリレーションを「プッシュ」することができます。

Denormalization

[PUT]、[PATCH]、[POST] リクエストにリレーションを埋め込むこともできます。この機能を有効にするには、シリアル化グループを正規化と同じ方法で設定します。例えば:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(denormalizationContext: ['groups' => ['book']])]
class Book
{
    // ...
}

埋め込まれたリレーションを非正規化する場合、次のルールが適用されます。

[@id] キーが埋め込みリソースに存在する場合、指定された URI に対応するオブジェクトが状態プロバイダーを通じて取得されます。埋め込まれたリレーションの変更は、そのオブジェクトにも適用されます。

[@id] キーが存在しない場合、埋め込まれた JSON ドキュメントで提供される状態を含む新しいオブジェクトが作成されます。

埋め込まれた関係レベルはいくつでも指定できます。

同じタイプの関係 (親子関係) で IRI を強制する

エンティティが同じタイプの他のエンティティを参照することはよくある問題です。

<?php
// api/src/Entity/Person.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    normalizationContext: ['groups' => ['person']],
    denormalizationContext: ['groups' => ['person']]
)]
class Person
{
    #[Groups('person')]
    public $name;

   /**
    * @var Person
    */
    #[Groups('person')]
   public $parent;  // Note that a Person instance has a relation with another Person.
 
    // ...
}

ここでの問題は、$parent プロパティが自動的に埋め込みオブジェクトになることです。また、プロパティは OpenAPI ビューには表示されません。

$parent プロパティを強制的に IRI として使用するには、[#[ApiProperty(readableLink: false, writableLink: false)]] 注釈を追加します。

<?php
// api/src/Entity/Person.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    normalizationContext: ['groups' => ['person']],
    denormalizationContext: ['groups' => ['person']]
)]
class Person
{
    #[Groups('person')]
    public string $name;

   #[Groups('person')]
   #[ApiProperty(readableLink: false, writableLink: false)]
   public Person $parent;  // This property is now serialized/deserialized as an IRI.
 
    // ...
}
Plain Identifiers

関係を設定するために IRI を送信する代わりに、プレーンな識別子を送信したい場合があります。そのためには、独自の非正規化子を作成する必要があります。

<?php
// api/src/Serializer/PlainIdentifierDenormalizer

namespace App\Serializer;

use ApiPlatform\Api\IriConverterInterface;
use App\Entity\Dummy;
use App\Entity\RelatedDummy;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;

class PlainIdentifierDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
    use DenormalizerAwareTrait;

    private $iriConverter;

    public function __construct(IriConverterInterface $iriConverter)
    {
        $this->iriConverter = $iriConverter;
    }

    /**
     * {@inheritdoc}
     */
    public function denormalize($data, $class, $format = null, array $context = [])
    {
        $data['relatedDummy'] = $this->iriConverter->getIriFromResource(resource: RelatedDummy::class, context: ['uri_variables' => ['id' => $data['relatedDummy']]]);

        return $this->denormalizer->denormalize($data, $class, $format, $context + [__CLASS__ => true]);
    }

    /**
     * {@inheritdoc}
     */
    public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
    {
        return \in_array($format, ['json', 'jsonld'], true) && is_a($type, Dummy::class, true) && !empty($data['relatedDummy']) && !isset($context[__CLASS__]);
    }
}
プロパティの正規化コンテキスト

プロパティの (非) 正規化コンテキストを変更する場合、たとえば日時の形式を変更する場合は、Symfony シリアライザー コンポーネントの [#[Context]] 属性を使用して行うことができます。

例えば

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

#[ORM\Entity]
#[ApiResource]
class Book
{
    #[ORM\Column] 
    #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
    public ?\DateTimeInterface $publicationDate = null;
}

上記の例では、本のデータを次のように受け取ります。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

#[ORM\Entity]
#[ApiResource]
class Book
{
    #[ORM\Column]
    #[Context(normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
    public ?\DateTimeInterface $publicationDate = null;
}

グループもサポートされています。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

#[ORM\Entity]
#[ApiResource]
class Book
{
    #[ORM\Column]
    #[Groups(["extended"])]
    #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
    #[Context(
        context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED],
        groups: ['extended'],
    )]
    public ?\DateTimeInterface $publicationDate = null;
}
Calculated Field

場合によっては、計算フィールドを公開する必要があります。これは、グループを活用することで実行できます。今回はプロパティではなくメソッドです。

<?php
// api/src/Entity/Greeting.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource]
#[GetCollection(normalizationContext: ['groups' => 'greeting:collection:get'])]
class Greeting
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    #[Groups("greeting:collection:get")]
    private ?int $id = null;
    
    private $a = 1;
    
    private $b = 2;

    #[ORM\Column]
    #[Groups("greeting:collection:get")]
    public string $name = '';

    public function getId(): int
    {
        return $this->id;
    }

    #[Groups('greeting:collection:get')] // <- MAGIC IS HERE, you can set a group on a method.
    public function getSum(): int
    {
        return $this->a + $this->b;
    }
}

シリアル化コンテキストを動的に変更する

ほとんどのフィールドを任意のユーザーが管理できるが、一部のフィールドは管理ユーザーのみが管理できるリソースを想像してみましょう。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    normalizationContext: ['groups' => ['book:output']],
    denormalizationContext: ['groups' => ['book:input']],
)]
class Book
{
    // ...

    /**
     * This field can be managed only by an admin
     */
    #[Groups(['book:output', 'admin:input'])]
    public bool $active = false;

    /**
     * This field can be managed by any user
     */
    #[Groups(['book:output', 'book:input'])]
    public string $name;

    // ...
}

すべてのユーザーのエントリ ポイントはすべて同じであるため、認証されたユーザーが管理者であるかどうかを検出する方法を見つける必要があります。

API プラットフォームは、シリアル化と逆シリアル化のコンテキストを準備する [ContextBuilder] を実装します。 [createFromRequest] メソッドをオーバーライドするようにこのサービスを装飾しましょう。

# api/config/services.yaml
services:
    # ...
    'App\Serializer\BookContextBuilder':
        decorates: 'api_platform.serializer.context_builder'
        arguments: [ '@App\Serializer\BookContextBuilder.inner' ]
        autoconfigure: false
<?php
// api/src/Serializer/BookContextBuilder.php
namespace App\Serializer;

use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use App\Entity\Book;

final class BookContextBuilder implements SerializerContextBuilderInterface
{
    private $decorated;
    private $authorizationChecker;

    public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->decorated = $decorated;
        $this->authorizationChecker = $authorizationChecker;
    }

    public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
    {
        $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
        $resourceClass = $context['resource_class'] ?? null;

        if ($resourceClass === Book::class && isset($context['groups']) && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) {
            $context['groups'][] = 'admin:input';
        }

        return $context;
    }
}

ユーザーに [ROLE_ADMIN] 権限があり、サブジェクトが Book のインスタンスである場合、[admin:input] グループが非正規化コンテキストに動的に追加されます。 [$normalization] 変数を使用すると、コンテキストが正規化用 ([TRUE] の場合) か非正規化用 ([FALSE]) かを確認できます。

アイテムごとにシリアル化コンテキストを変更する

上記の例は、すべての本に対する現在のユーザー権限に基づいて、正規化/非正規化コンテキストを変更する方法を示しています。ただし、処理する書籍によって権限が異なる場合があります。

ACL について考えてみましょう。ユーザー「A」はブック「A」を取得できますが、ブック「B」は取得できません。この場合、Symfony シリアライザーの機能を活用し、すべてのアイテムにグループを追加する独自のノーマライザーを登録する必要があります (注: 優先度 [64] は例です。ノーマライザーが最初に読み込まれるようにすることが常に重要です。であるため、優先度をアプリケーションに適した値に設定します。値が大きいほど早くロードされます):

# api/config/services.yaml
services:
    'App\Serializer\BookAttributeNormalizer':
        arguments: [ '@security.token_storage' ]
        tags:
            - { name: 'serializer.normalizer', priority: 64 }

Normalizer クラスは、一度だけ呼び出され、再帰がないことを確認する必要があるため、理解するのが少し難しくなります。これを実現するには、親の Normalizer インスタンス自体を認識している必要があります。

次に例を示します。

<?php
// api/src/Serializer/BookAttributeNormalizer.php
namespace App\Serializer;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;

class BookAttributeNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
    use NormalizerAwareTrait;

    private const ALREADY_CALLED = 'BOOK_ATTRIBUTE_NORMALIZER_ALREADY_CALLED';

    private $tokenStorage;

    public function __construct(TokenStorageInterface $tokenStorage)
    {
        $this->tokenStorage = $tokenStorage;
    }

    public function normalize($object, $format = null, array $context = [])
    {
        if ($this->userHasPermissionsForBook($object)) {
            $context['groups'][] = 'can_retrieve_book';
        }

        $context[self::ALREADY_CALLED] = true;

        return $this->normalizer->normalize($object, $format, $context);
    }

    public function supportsNormalization($data, $format = null, array $context = [])
    {
        // Make sure we're not called twice
        if (isset($context[self::ALREADY_CALLED])) {
            return false;
        }

        return $data instanceof Book;
    }

    private function userHasPermissionsForBook($object): bool
    {
        // Get permissions from user in $this->tokenStorage
        // for the current $object (book) and
        // return true or false
    }
}

これにより、現在ログインしているユーザーが特定のBookインスタンスにアクセスできる場合にのみ、シリアライゼーション グループ [can_retrieve_book] が追加されます。

注: この例では、[TokenStorageInterface] を使用して book インスタンスへのアクセスを確認します。ただし、Symfony は、ユースケースにより適した他の多くの便利なサービスを提供します。たとえば、AuthorizationChecker です。

Name Conversion

Serializer コンポーネントは、PHP フィールド名をシリアル化された名前にマップする便利な方法を提供します。関連する Symfony documentationを参照してください。

この機能を使用するには、新しい名前変換サービスを宣言します。たとえば、次の構成で [CamelCase] を [snake_case] に変換できます。

# api/config/services.yaml
services:
    'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter': ~
# api/config/packages/api_platform.yaml
api_platform:
    name_converter: 'Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter'

symfony の [MetadataAwareNameConverter] が利用可能な場合、デフォルトで使用されます。 ApiPlatform 構成で指定すると、それが使用されます。装飾を使用して、独自の実装でこの名前コンバーターの恩恵を受けることができることに注意してください。

シリアライザーの装飾と余分なデータの追加

次の例では、シリアル化された出力に追加情報を追加する方法を示します。 [GET] で各リクエストに日付を追加する方法は次のとおりです。

# api/config/services.yaml
services:
    'App\Serializer\ApiNormalizer':
        # By default .inner is passed as argument
        decorates: 'api_platform.jsonld.normalizer.item'

注: このノーマライザーは JSON-LD 形式でのみ機能します。JSON データも処理する場合は、別のサービスをデコレートする必要があります。

<?php
// api/src/Serializer/ApiNormalizer
namespace App\Serializer;

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;

final class ApiNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
    private $decorated;

    public function __construct(NormalizerInterface $decorated)
    {
        if (!$decorated instanceof DenormalizerInterface) {
            throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
        }

        $this->decorated = $decorated;
    }

    public function supportsNormalization($data, $format = null)
    {
        return $this->decorated->supportsNormalization($data, $format);
    }

    public function normalize($object, $format = null, array $context = [])
    {
        $data = $this->decorated->normalize($object, $format, $context);
        if (is_array($data)) {
            $data['date'] = date(\DateTime::RFC3339);
        }

        return $data;
    }

    public function supportsDenormalization($data, $type, $format = null)
    {
        return $this->decorated->supportsDenormalization($data, $type, $format);
    }

    public function denormalize($data, string $type, string $format = null, array $context = [])
    {
        return $this->decorated->denormalize($data, $type, $format, $context);
    }

    public function setSerializer(SerializerInterface $serializer)
    {
        if($this->decorated instanceof SerializerAwareInterface) {
            $this->decorated->setSerializer($serializer);
        }
    }
}

Entity Identifier Case

API プラットフォームは、Doctrine メタデータ (ORM、MongoDB ODM) を使用してエンティティ識別子を推測できます。 ORM の場合、複合識別子もサポートします。

Doctrine ORM または MongoDB ODM Provider を使用していない場合は、[ApiPlatform\Metadata\ApiProperty] アノテーションの [identifier] 属性を使用して識別子を明示的にマークする必要があります。例えば:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiProperty;

#[ApiResource]
class Book
{
    // ...

    #[ApiProperty(identifier: true)]
    private $id;

    /**
     * This field can be managed only by an admin
     */
    public bool $active = false;

    /**
     * This field can be managed by any user
     */
    public string $name;

    // ...
}

YAML 構成形式を使用することもできます。

# api/config/api_platform/resources.yaml
properties:
    App\Entity\Book:
        id:
            identifier: true


場合によっては、クライアントからリソースの識別子を設定する必要があります (クライアント側で生成された UUID やスラッグなど)。このような場合、identifier プロパティを書き込み可能なクラス プロパティにする必要があります。具体的には、クライアント生成 ID を使用するには、次のことを行う必要があります。

エンティティの識別子 (例: [public function setId(string $id)]) のセッターを作成するか、それを [public] プロパティにします。

プロパティに非正規化グループを追加します (特定の非正規化グループを使用する場合のみ)。

Doctrine ORM を使用する場合は、このプロパティに [@GeneratedValue] アノテーションを付けたり、[NONE] 値を使用したりしないでください。

JSON-LD コンテキストの埋め込み

デフォルトでは、生成された JSON-LD コンテキスト ([@context]) は IRI によってのみ参照されます。 JSON-LD を使用するクライアントは、それを取得するために 2 番目の HTTP 要求を送信する必要があります。

{
  "@context": "/contexts/Book",
  "@id": "/books/62",
  "@type": "Book",
  "name": "My awesome book",
  "author": "/people/59"
}

[jsonld_embed_context] 属性を [#[ApiResource]] アノテーションに追加することで、JSON-LD コンテキストをルート ドキュメントに埋め込むように API プラットフォームを構成できます。

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(normalizationContext: ['jsonld_embed_context' => true])]
class Book
{
    // ...
}

JSON 出力には、埋め込みコンテキストが含まれるようになりました。

{
  "@context": {
    "@vocab": "http://localhost:8000/apidoc#",
    "hydra": "http://www.w3.org/ns/hydra/core#",
    "name": "https://schema.org/name",
    "author": "https://schema.org/author"
  },
  "@id": "/books/62",
  "@type": "Book",
  "name": "My awesome book",
  "author": "/people/59"
}

Collection Relation

これは、エンティティで [toMany] 関係を持つ特殊なケースです。デフォルトでは、Doctrine は [ArrayCollection] を使用して値を保存します。読み取り操作を行う場合はこれで問題ありませんが、書き込みを試みると、応答が変更を正しく反映していないという問題が発生することがあります。更新が正しく行われた場合でも、クライアント エラーが発生する可能性があります。実際、この関係を更新すると、[ArrayCollection] のインデックスが連続していないため、コレクションが正しく表示されなくなります。これを変更するには、[$collectionRelation->getValues()] を返す getter を使用することをお勧めします。このおかげで、リレーションは現在、連続してインデックス付けされた実数配列です。

<?php
// api/src/Entity/Brand.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource]
final class Brand
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\ManyToMany(targetEntity: Car::class, inversedBy: 'brands')]
    #[ORM\JoinTable(name: 'CarToBrand')]
    #[ORM\JoinColumn(name: 'brand_id', referencedColumnName: 'id', nullable: false)]
    #[ORM\InverseJoinColumn(name: 'car_id', referencedColumnName: 'id', nullable: false)]
    private $cars;

    public function __construct()
    {
        $this->cars = new ArrayCollection();
    }

    public function addCar(DummyCar $car)
    {
        $this->cars[] = $car;
    }

    public function removeCar(DummyCar $car)
    {
        $this->cars->removeElement($car);
    }

    public function getCars()
    {
        return $this->cars->getValues();
    }

    public function getId()
    {
        return $this->id;
    }
}

参考までに #1534 を参照してください。