The Serialization Process

Overall Process

API Platform embraces and extends the Symfony Serializer Component to transform PHP entities in (hypermedia) API responses.

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

Serializer screencast
Watch the Serializer screencast

シリアライザーのスクリーンキャストを見る

The main serialization process has two stages:

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

Serializer workflow

As you can see in the picture above, an array is used as a man-in-the-middle. This way, Encoders will only deal with turning specific formats into arrays and vice versa. The same way, Normalizers will deal with turning specific objects into arrays and vice versa. -- The Symfony documentation

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

Unlike Symfony itself, API Platform leverages custom normalizers, its router and the state provider system to perform an advanced transformation. Metadata are added to the generated document including links, type information, pagination data or available filters.

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

The API Platform Serializer is extendable. You can register custom normalizers and encoders in order to support other formats. You can also decorate existing normalizers to customize their behaviors.

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

Available Serializers

  • JSON-LD serializer api_platform.jsonld.normalizer.item
    JSON-LD serializerapi_platform.jsonld.normalizer.item

JSON-LD, or JavaScript Object Notation for Linked Data, is a method of encoding Linked Data using JSON. It is a World Wide Web Consortium Recommendation.

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

  • HAL serializer api_platform.hal.normalizer.item

    HAL serializerapi_platform.hal.normalizer.item

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

    JSON、XML、CSV、YAML シリアライザー (Symfony シリアライザーを使用)api_platform.serializer.normalizer.item

The Serialization Context, Groups and Relations

Serialization Groups screencast
Watch the Serialization Groups screencast

シリアル化グループのスクリーンキャストを見る

API Platform allows you to specify the $context variable used by the Symfony Serializer. This variable is an associative array that has a handy groups key allowing you to choose which attributes of the resource are exposed during the normalization (read) and denormalization (write) processes. It relies on the serialization (and deserialization) groups feature of the Symfony Serializer component.

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

In addition to groups, you can use any option supported by the Symfony Serializer. For example, you can use enable_max_depth to limit the serialization depth.

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

Configuration

Just like other Symfony and API Platform components, the Serializer component can be configured using annotations, XML or YAML. Since annotations are easy to understand, we will use them in the following examples.

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

Note: if you aren't using the API Platform distribution, you will need to enable annotation support in the serializer configuration:

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

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

If you use Symfony Flex, just execute composer req doctrine/annotations and you are all set!

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

If you want to use YAML or XML, please add the mapping path in the serializer configuration:

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

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

Using Serialization Groups

It is simple to specify what groups to use in the API system:

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

  1. Add the normalization context and denormalization context attributes to the resource, and specify which groups to use. Here you see that we add read and write, respectively. You can use any group names you wish.
    正規化コンテキストと非正規化コンテキスト属性をリソースに追加し、使用するグループを指定します。ここでは、読み取りと書き込みをそれぞれ追加していることがわかります。任意のグループ名を使用できます。
  2. Apply the groups to properties in the object.
    グループをオブジェクトのプロパティに適用します。

[codeSelector]

[コードセレクター]

<?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;

    // ...
}
# api/config/api_platform/resources.yaml
resources:
    App\Entity\Book:
        normalizationContext:
            groups: ['read']
        denormalizationContext:
            groups: ['write']

# api/config/serialization/Book.yaml
App\Entity\Book:
    attributes:
        name:
            groups: ['read', 'write']
        author:
            groups: ['write']
<!-- api/config/api_platform/resources.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
                               https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <normalizationContext>
            <values>
                <value name="groups">
                    <values>
                        <value>read</value>
                    </values>
                </value>
            </values>
        </normalizationContext>
        <denormalizationContext>
            <values>
                <value name="groups">
                    <values>
                        <value>write</value>
                    </values>
                </value>
            </values>
        </denormalizationContext>
    </resource>
</resources>

<!-- api/config/serialization/Book.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping
                                http://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd">
    <class name="App\Entity\Book">
        <attribute name="name">
            <group>read</group>
            <group>write</group>
        </attribute>
        <attribute name="author">
            <group>write</group>
        </attribute>
    </class>
</serializer>

[/codeSelector]

[/コードセレクター]

In the previous example, the name property will be visible when reading (GET) the object, and it will also be available to write (PUT / PATCH / POST). The author property will be write-only; it will not be visible when serialized responses are returned by the API.

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

Internally, API Platform passes the value of the normalizationContext as the 3rd argument of the Serializer::serialize() method during the normalization process. denormalizationContext is passed as the 4th argument of the Serializer::deserialize() method during denormalization (writing).

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

To configure the serialization groups of classes's properties, you must use directly the Symfony Serializer's configuration files or annotations.

クラスのプロパティのシリアル化グループを構成するには、Symfony シリアライザーの構成ファイルまたは注釈を直接使用する必要があります。

In addition to the groups key, you can configure any Symfony Serializer option through the $context parameter (e.g. the enable_max_depthkey when using the @MaxDepth annotation).

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

Any serialization and deserialization group that you specify will also be leveraged by the built-in actions and the Hydra documentation generator.

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

Using Serialization Groups per Operation

It is possible to specify normalization and denormalization contexts (as well as any other attribute) on a per-operation basis. API Platform will always use the most specific definition. For instance, if normalization groups are set both at the resource level and at the operation level, the configuration set at the operation level will be used and the resource level ignored.

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

In the following example we use different serialization groups for the GET and PUT operations:

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

[codeSelector]

[コードセレクター]

<?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;

    // ...
}
# api/config/api_platform/resources/Book.yaml
App\Entity\Book:
    normalizationContext:
        groups: ['get']
    operations:
        ApiPlatform\Metadata\Get: ~
        ApiPlatform\Metadata\Put:
            normalizationContext:
                groups: ['put']

# api/config/serializer/Book.yaml
App\Entity\Book:
    attributes:
        name:
            groups: ['get', 'put']
        author:
            groups: ['get']
<!-- api/config/api_platform/resources.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<resources xmlns="https://api-platform.com/schema/metadata/resources-3.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="https://api-platform.com/schema/metadata/resources-3.0
                               https://api-platform.com/schema/metadata/resources-3.0.xsd">
    <resource class="App\Entity\Book">
        <normalizationContext>
            <values>
                <value name="groups">
                    <values>
                        <value>get</value>
                    </values>
                </value>
            </values>
        </normalizationContext>
        <operations>
            <operation class="ApiPlatform\Metadata\Get" />
            <operation class="ApiPlatform\Metadata\Put">
                <normalizationContext>
            <values>
                <value name="groups">
                    <values>
                        <value>put</value>
                    </values>
                </value>
            </values>
        </normalizationContext>
            </operation>
        </operations>
    </resource>
</resources>

<!-- api/config/serialization/Book.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping
                                http://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd">
    <class name="App\Entity\Book">
        <attribute name="name">
            <group>get</group>
            <group>put</group>
        </attribute>
        <attribute name="author">
            <group>get</group>
        </attribute>
    </class>
</serializer>

[/codeSelector]

[/コードセレクター]

The name and author properties will be included in the document generated during a GET operation because the configuration defined at the resource level is inherited. However the document generated when a PUT request will be received will only include the name property because of the specific configuration for this operation.

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

Refer to the operations documentation to learn more.

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

Embedding Relations

Relations screencast
Watch the Relations screencast

関係のスクリーンキャストを見る

By default, the serializer provided with API Platform represents relations between objects using dereferenceable IRIs. They allow you to retrieve details for related objects by issuing extra HTTP requests. However, for performance reasons, it is sometimes preferable to avoid forcing the client to issue extra HTTP requests.

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

Note: We strongly recommend using Vulcain instead of this feature. Vulcain allows creating faster (better hit rate) and better designed APIs than relying on compound documents, and is supported out of the box in the API Platform distribution.

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

Normalization

In the following JSON document, the relation from a book to an author is by default represented by an URI:

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

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

It is possible to embed related objects (in their entirety, or only some of their properties) directly in the parent response through the use of serialization groups. By using the following serialization groups annotations (#[Groups]), a JSON representation of the author is embedded in the book response. As soon as any of the author's attributes is in the book group, the author will be embedded.

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

[codeSelector]

[コードセレクター]

<?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;

    // ...
}
# api/config/api_platform/resources/Book.yaml
App\Entity\Book:
    normalizationContext:
        groups: ['book']

# api/config/serializer/Book.yaml
App\Entity\Book:
    attributes:
        name:
            groups: ['book']
        author:
            groups: ['book']

[/codeSelector]

[/コードセレクター]

[codeSelector]

[コードセレクター]

<?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;

    // ...
}
# api/config/serializer/Person.yaml
App\Entity\Person:
    attributes:
        name:
            groups: ['book']

[/codeSelector]

[/コードセレクター]

The generated JSON using previous settings is below:

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

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

In order to optimize such embedded relations, the default Doctrine state provider will automatically join entities on relations marked as EAGER. This avoids the need for extra queries to be executed when serializing the related objects.

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

Instead of embedding relations in the main HTTP response, you may want to "push" them to the client using HTTP/2 server push.

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

Denormalization

It is also possible to embed a relation in PUT, PATCH and POST requests. To enable that feature, set the serialization groups the same way as normalization. For example:

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

[codeSelector]

[コードセレクター]

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

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(denormalizationContext: ['groups' => ['book']])]
class Book
{
    // ...
}
# api/config/api_platform/resources/Book.yaml
App\Entity\Book:
    denormalizationContext:
        groups: ['book']

[/codeSelector]

[/コードセレクター]

The following rules apply when denormalizing embedded relations:

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

  • If an @id key is present in the embedded resource, then the object corresponding to the given URI will be retrieved through the state provider. Any changes in the embedded relation will also be applied to that object.
    @id キーが埋め込みリソースに存在する場合、指定された URI に対応するオブジェクトが状態プロバイダーを通じて取得されます。埋め込まれたリレーションの変更は、そのオブジェクトにも適用されます。
  • If no @id key exists, a new object will be created containing state provided in the embedded JSON document.
    @id キーが存在しない場合、埋め込まれた JSON ドキュメントで提供された状態を含む新しいオブジェクトが作成されます。

You can specify as many embedded relation levels as you want.

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

Force IRI with relations of the same type (parent/childs relations)

It is a common problem to have entities that reference other entities of the same type:

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

[codeSelector]

[コードセレクター]

<?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.

    // ...
}
# api/config/api_platform/resources/Person.yaml
App\Entity\Person:
    normalizationContext:
        groups: ['person']
    denormalizationContext:
        groups: ['person']

# api/config/serializer/Person.yaml
App\Entity\Person:
    attributes:
        name:
            groups: ['person']
        parent:
            groups: ['person']

[/codeSelector]

[/コードセレクター]

The problem here is that the $parent property become automatically an embedded object. Besides, the property won't be shown on the OpenAPI view.

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

To force the $parent property to be used as an IRI, add an #[ApiProperty(readableLink: false, writableLink: false)] annotation:

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

[codeSelector]

[コードセレクター]

<?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.

    // ...
}

# api/config/api_platform/resources/Person.yaml
resources:
    App\Entity\Person:
        normalizationContext:
          groups: ['person']
        denormalizationContext:
          groups: ['person']
properties:
    App\Entity\Person:
        parent:
            readableLink: false
            writableLink: false

# api/config/serializer/Person.yaml
App\Entity\Person:
    attributes:
        name:
            groups: ['person']
        parent:
            groups: ['person']

[/codeSelector]

[/コードセレクター]

Plain Identifiers

Instead of sending an IRI to set a relation, you may want to send a plain identifier. To do so, you must create your own denormalizer:

関係を設定するために 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__]);
    }
}

Property Normalization Context

If you want to change the (de)normalization context of a property, for instance if you want to change the format of the date time, you can do so by using the #[Context] attribute from the Symfony Serializer component.

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

For instance:

例えば:

<?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;
}

In the above example, you will receive the book's data like this:

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

{
  "@context": "/contexts/Book",
  "@id": "/books/3",
  "@type": "https://schema.org/Book",
  "publicationDate": "1989-06-16"
}

It's also possible to only change the denormalization or normalization 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(normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
    public ?\DateTimeInterface $publicationDate = null;
}

Groups are also supported:

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

<?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

Sometimes you need to expose calculated fields. This can be done by leveraging the groups. This time not on a property, but on a method.

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

[codeSelector]

[コードセレクター]

<?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;
    }
}
# api/config/api_platform/resources/Greeting.yaml
App\Entity\Greeting:
    operations:
        ApiPlatform\Metadata\GetCollection:
            normalizationContext:
                groups: 'greeting:collection:get'

# api/config/serializer/Greeting.yaml
App\Entity\Greeting:
    attributes:
        id:
            groups: 'greeting:collection:get'
        name:
            groups: 'greeting:collection:get'
        sum:
            groups: 'greeting:collection:get'

[/codeSelector]

[/コードセレクター]

Changing the Serialization Context Dynamically

Context Builder & Service Decoration screencast
Watch the Context Builder & Service Decoration screencast

コンテキスト ビルダーとサービス デコレーションのスクリーンキャストを見る

Let's imagine a resource where most fields can be managed by any user, but some can be managed only by admin users:

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

[codeSelector]

[コードセレクター]

<?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/config/api_platform/resources/Book.yaml
App\Entity\Book: 
    normalizationContext:
        groups: ['book:output']
    denormalizationContext:
        groups: ['book:input']

# api/config/serializer/Book.yaml
App\Entity\Book:
    attributes:
        active:
            groups: ['book:output', 'admin:input']
        name:
            groups: ['book:output', 'book:input']

[/codeSelector]

[/コードセレクター]

All entry points are the same for all users, so we should find a way to detect if the authenticated user is an admin, and if so dynamically add the admin:input value to deserialization groups in the $context array.

すべてのエントリ ポイントはすべてのユーザーで同じであるため、認証されたユーザーが管理者であるかどうかを検出する方法を見つける必要があります。その場合、admin:input 値を $context 配列の逆シリアル化グループに動的に追加します。

API Platform implements a ContextBuilder, which prepares the context for serialization & deserialization. Let's decorate this service to override the createFromRequest method:

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;
    }
}

If the user has the ROLE_ADMIN permission and the subject is an instance of Book, admin:input group will be dynamically added to the denormalization context. The $normalization variable lets you check whether the context is for normalization (if TRUE) or denormalization (FALSE).

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

Changing the Serialization Context on a Per-item Basis

The example above demonstrates how you can modify the normalization/denormalization context based on the current user permissions for all books. Sometimes, however, the permissions vary depending on what book is being processed.

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

Think of ACL's: User "A" may retrieve Book "A" but not Book "B". In this case, we need to leverage the power of the Symfony Serializer and register our own normalizer that adds the group on every single item (note: priority 64 is an example; it is always important to make sure your normalizer gets loaded first, so set the priority to whatever value is appropriate for your application; higher values are loaded earlier):

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

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

The Normalizer class is a bit harder to understand, because it must ensure that it is only called once and that there is no recursion. To accomplish this, it needs to be aware of the parent Normalizer instance itself.

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

Here is an example:

次に例を示します。

<?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
    }
}

This will add the serialization group can_retrieve_book only if the currently logged-in user has access to the given book instance.

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

Note: In this example, we use the TokenStorageInterface to verify access to the book instance. However, Symfony provides many useful other services that might be better suited to your use case. For example, the AuthorizationChecker.

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

Name Conversion

The Serializer Component provides a handy way to map PHP field names to serialized names. See the related Symfony documentation.

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

To use this feature, declare a new name converter service. For example, you can convert CamelCase to snake_case with the following configuration:

この機能を使用するには、新しい名前変換サービスを宣言します。たとえば、次の構成で 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'

If symfony's MetadataAwareNameConverter is available it'll be used by default. If you specify one in ApiPlatform configuration, it'll be used. Note that you can use decoration to benefit from this name converter in your own implementation.

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

Decorating a Serializer and Adding Extra Data

In the following example, we will see how we add extra information to the serialized output. Here is how we add the date on each request in GET:

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

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

Note: this normalizer will work only for JSON-LD format, if you want to process JSON data too, you have to decorate another service:

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

    # Need a different name to avoid duplicate YAML key
    'app.serializer.normalizer.item.json':
        class: 'App\Serializer\ApiNormalizer'
        decorates: 'api_platform.serializer.normalizer.item'
<?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 Platform is able to guess the entity identifier using Doctrine metadata (ORM, MongoDB ODM). For ORM, it also supports composite identifiers.

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

If you are not using the Doctrine ORM or MongoDB ODM Provider, you must explicitly mark the identifier using the identifier attribute of the ApiPlatform\Metadata\ApiProperty annotation. For example:

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;

    // ...
}

You can also use the YAML configuration format:

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

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

In some cases, you will want to set the identifier of a resource from the client (e.g. a client-side generated UUID, or a slug). In such cases, you must make the identifier property a writable class property. Specifically, to use client-generated IDs, you must do the following:

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

  1. create a setter for the identifier of the entity (e.g. public function setId(string $id)) or make it a public property ,
    エンティティの識別子のセッターを作成する (例: public function setId(string $id)) か、パブリック プロパティにします。
  2. add the denormalization group to the property (only if you use a specific denormalization group), and,
    プロパティに非正規化グループを追加します (特定の非正規化グループを使用する場合のみ)。
  3. if you use Doctrine ORM, be sure to not mark this property with the @GeneratedValue annotation or use the NONE value
    Doctrine ORM を使用する場合は、このプロパティを @GeneratedValue アノテーションでマークしないか、NONE 値を使用してください。

Embedding the JSON-LD Context

By default, the generated JSON-LD context (@context) is only referenced by an IRI. A client that uses JSON-LD must send a second HTTP request to retrieve it:

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

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

You can configure API Platform to embed the JSON-LD context in the root document by adding the jsonld_embed_context attribute to the #[ApiResource] annotation:

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

[codeSelector]

[コードセレクター]

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

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(normalizationContext: ['jsonld_embed_context' => true])]
class Book
{
    // ...
}
# api/config/api_platform/resources/Book.yaml
App\Entity\Book:
    normalizationContext:
        jsonldEmbedContext: true

[/codeSelector]

[/コードセレクター]

The JSON output will now include the embedded context:

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

This is a special case where, in an entity, you have a toMany relation. By default, Doctrine will use an ArrayCollection to store your values. This is fine when you have a read operation, but when you try to write you can observe an issue where the response is not reflecting the changes correctly. It can lead to client errors even though the update was correct. Indeed, after an update on this relation, the collection looks wrong because ArrayCollection's indexes are not sequential. To change this, we recommend to use a getter that returns $collectionRelation->getValues(). Thanks to this, the relation is now a real array which is sequentially indexed.

これは、エンティティに 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;
    }
}

For reference please check #1534.

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