API Platform and Symfony

A suitable serialization with

Mathias Arlaud

Serialization

Representing data structures in a format that can be sent or persisted in order to be reconstructed later

Binary, textual

Construction pattern

Databases, flat files, APIs

Anywhere, interoperable

Serialization

https://symfony.com/doc/current/components/serializer.html

Beep-boop!

#[ApiResource]
class Robot
{
    public int $id;

    public string $name;

    public string $mission;

    public string $unofficialMission;
}

Beep-boop!

#[ApiResource]
class Robot
{
    public int $id;

    public string $name;

    public string $mission;

    public string $unofficialMission;
}
{
  "id": 1,
  "name": "Persévérance",
  "mission": "Trouver la vie sur Mars",
  "unofficialMission": "Anéantir toute vie sur Mars"
}
> curl /api/robots/1.json

Customizing serialization

#[ApiResource]
class Robot
{
    public int $id;

    public string $name;

    public string $mission;

    public string $unofficialMission;
}
{
  "id": 1,
  "name": "Persévérance",
  "mission": "Trouver la vie sur Mars",
  "unofficialMission": "Anéantir toute vie sur Mars"
}

Ignore / ApiProperty

Customizing serialization

#[ApiResource]
class Robot
{
  public int $id;

  public string $name;

  public string $mission;

  #[Ignore]
  public string $unofficialMission;
}

Ignore / ApiProperty

Customizing serialization

#[ApiResource]
class Robot
{
  public int $id;

  public string $name;

  public string $mission;

  #[ApiProperty(readable: false, writable: false)]
  public string $unofficialMission;
}

Ignore / ApiProperty

Customizing serialization

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

  #[ApiProperty(readable: false, writable: false)]
  public string $unofficialMission;
}
#[ApiResource]
class Robot
{
  // ...

  #[Ignore]
  public string $unofficialMission;
}
> curl /api/robots/1.json
> curl /api/robots.json
...

Groups!

Groups

Customizing serialization

public function serialize($data, string $format, array $context = []);
array $context = []);

Groups

Customizing serialization

class Robot
{
  #[Groups(['group-one', 'group-two'])]
  public string $name;

  #[Groups(['group-one'])]
  public string $mission;

  public string $unofficialMission;
}

Groups

Customizing serialization

class Robot
{
  #[Groups(['group-one', 'group-two'])]
  public string $name;

  #[Groups(['group-one'])]
  public string $mission;

  public string $unofficialMission;
}
serialize(..., ['groups' => ['group-one']]);
serialize(..., ['groups' => ['group-two']]);
{"name": "foo", "mission": "bar"}
{"name": "foo"}

Groups and API Platform

#[ApiResource(
  itemOperations: [
    'get' => [
      'normalization_context' => ['groups' => ['item']],
    ],
  ],
  collectionOperations: [
    'get' => [
      'normalization_context' => ['groups' => ['list']],
    ],
  ],
)]

Customizing serialization

> curl /api/robots/1.json
['item']
> curl /api/robots.json
['list']
#[ApiResource]
#[Get(
  normalizationContext: ['groups' => ['item']],
)]
#[GetCollection(
  normalizationContext: ['groups' => ['list']],
)]

API Platform and Symfony

Dynamic groups

> curl /api/robots/1.json

SerializeListener

ContextBuilder, Serializer

(RespondListener)

Yep, I know it!

ViewEvent

Does anyone know how to convert a        to a       ?

Controller
Response

SerializeListener

#[ApiResource(normalizationContext: ['groups' => ['read']])]
class Robot
{
  #[Groups(['read'])]
  public string $name;

  #[Groups(['read'])]
  public string $mission;
  
  #[Groups(['secret_service'])]
  public string $unofficialMission;
}

ContextBuilders

SerializeListener

SerializeListener

ContextBuilder
interface SerializerContextBuilderInterface {

  public function createFromRequest(
    Request $request,
    bool $normalization,
    ?array $attributes = null,
  ): array;
  
}
Serializer

ContextBuilders

ContextBuilders

class SecretServiceContextBuilder implements SerializerContextBuilderInterface
{
  // ...
  
  public function createFromRequest(...): array
  {
    $context = $this->decorated->createFromRequest(...);
    
    if (Robot::class === ($context['resource_class'] ?? null)
      && $this->authorizationChecker->isGranted('ROLE_SECRET_SERVICE')
    ) {
      $context['groups'][] = 'secret_service';
    }
    
    return $context;
  }
}
services:
  App\Serializer\SecretServiceContextBuilder:
    decorates: 'api_platform.serializer.context_builder'

Context used by the serializer

Custom logic

API Platform generated context

SerializeListener

ApiProperty::security

API Platform 2.6

#[ApiResource(normalizationContext: ['groups' => ['read']])]
class Robot
{ 


  #[Groups(['read'])]
  public string $unofficialMission;
}
#[ApiProperty(security: 'is_granted("ROLE_SECRET_SERVICE")')]

ExpressionLanguage

user, object, is_granted, ...

Serializers

#[ApiResource(normalizationContext: ['groups' => ['read']])]
class Robot {
  #[Groups(['read'])]
  public string $name;
  
  #[Groups(['creator'])]
  public string $mentalHealth;
  
  public User $creator;
}

SerializeListener

Serializers

SerializeListener

SerializeListener

ContextBuilder
interface NormalizerInterface
{
  public function normalize(
    $data,
    string $format = null,
    array $context = []
  );

  public function supportsNormalization(
    $data,
    string $format = null,
    array $context = []
  ): bool;
}
Serializer

Serializers

SerializeListener

class RobotNormalizer implements NormalizerInterface
{
  // ...

  public function normalize(...)
  {
    if ($this->security->getUser() === $data->creator) {
      $context['groups'][] = 'creator';
    }

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

  public function supportsNormalization(...): bool
  {
    return ... && $data instanceof Robot;
  }
}

Scope the normalizer

Custom logic

Regular normalization

#[ApiResource(normalizationContext: ['groups' => ['read']])]
class Robot
{ 


  #[Groups(['read'])]
  public string $unofficialMission;
}
#[ApiProperty(security: 'object.creator == user')]

ApiProperty::security

API Platform 2.6

Defaults

#[ApiResource(
  normalizationContext: ['groups' => ['read']],
  itemOperations: [
    'get' => ['normalization_context' => 
      ['groups' => ['read', 'item']]],
  ],
  collectionOperations: [
    'get' => ['normalization_context' =>
       ['groups' => ['read', 'list']]],
  ],
)]
class Robot {}
#[ApiResource(
  normalizationContext: ['groups' => ['read']],
  itemOperations: [
    'get' => ['normalization_context' => 
      ['groups' => ['read', 'item']]],
  ],
  collectionOperations: [
    'get' => ['normalization_context' =>
       ['groups' => ['read', 'list']]],
  ],
)]
class Datasheet {}

API Platform 2.6

#[ApiResource]
#[Get(
  normalizationContext: ['groups' => ['read', 'item']],
)]
#[GetCollection(
  normalizationContext: ['groups' => ['read', 'list']],
)]
class Robot {}
#[ApiResource]
#[Get(
  normalizationContext: ['groups' => ['read', 'item']],
)]
#[GetCollection(
  normalizationContext: ['groups' => ['read', 'list']],
)]
class Datasheet{}

Defaults

API Platform 2.6

#[ApiResource]
class Robot {}

#[ApiResource]
class Datasheet {}
# config/packages/api_platform.yaml

api_platform:
  defaults:
    normalizationContext:
      groups: ["read"]
    itemOperations:
      get: ["normalization_context": ["groups" => ["read", "item"]]]
    collectionOperations:
      get: ["normalization_context": ["groups" => ["read", "list"]]]
      
    # ...

ResourceMetadataFactory

Dynamic groups

#[ApiResource]
class Robot {
  #[Groups(['read'])]
  public Datasheet $datasheet;
}

#[ApiResource]
class Datasheet {
    #[Groups(['read'])]
    public string $reference;

    #[Groups(['read'])]
    public array $specs;
}
{
  "datasheet": {
    "reference": "PE-01",
    "specs": ["lot of data", "..."]
  }
}

ResourceMetadataFactory

Dynamic groups

{
  "datasheet": {
    "reference": "PE-01"
  }
}
#[ApiResource(normalizationContext: ['groups' => ['robot:read']])]
class Robot {
  #[Groups(['robot:read'])]
  public Datasheet $datasheet;
}

#[ApiResource(normalizationContext: ['groups' => ['datasheet:read']])]
class Datasheet {
    #[Groups(['datasheet:read', 'robot:read'])]
    public string $reference;

    #[Groups(['datasheet:read'])]
    public array $specs;
}

ResourceMetadataFactory

Dynamic groups

ResourceMetadataFactory
App\Entity\Robot

SerializeListener

ContextBuilder
Serializer
ResourceMetadata

ResourceMetadataFactory

Dynamic groups

class GroupResourceMetadataFactory implements ResourceMetadataFactoryInterface
{
  // ...

  public function create(string $resourceClass): ResourceMetadata
  {
    $resourceMetadata = $this->decorated->create($resourceClass);

    return $resourceMetadata
      ->withItemOperations($this->addGroupsToOperations($resourceMetadata, true))
      ->withCollectionOperations($this->addGroupsToOperations($resourceMetadata, false))
    ;
  }
    
  // Return operations with dynamic groups (eg: robot:read, robot:list, or robot:item).
  private function addGroupsToOperations(ResourceMetadata $metadata, bool $isItem) {}
}
services:
  App\ApiPlatform\AutoGroupResourceMetadataFactory:
    decorates: 'api_platform.metadata.resource.metadata_factory'

Custom logic

API Platform generated metadata

ResourceMetadataFactory

Dynamic groups

#[ApiResource]
class Robot {
  #[Groups(['robot:read'])]
  public Datasheet $datasheet;
}

#[ApiResource]
class Datasheet {
    #[Groups(['datasheet:read', 'robot:read'])]
    public string $reference;

    #[Groups(['datasheet:read'])]
    public array $specs;
}

SerializeListener

ContextBuilder
Serializer
ResourceMetadata

Data Transfer Objects

DTOs

#[ApiResource(
  shortName: 'Astronaut',
  output: Astronaut::class,
)]
class Robot {
  public string $name;
  
  
  public string $mission;
  
  
  public int $battery;


  public Datasheet $datasheet;
}
class Astronaut {
  #[Groups(['astronaut:read'])]
  public string $name;
  
  #[Groups(['astronaut:read'])]
  public string $task;
  
  #[Groups(['astronaut:read'])]
  public bool $hasOxygen;
}

DataTransformers

DTOs

class AstronautDataTransformer implements DataTransformerInterface
{
    public function transform(...): Astronaut
    {
        $astronaut = new Astronaut();
        $astronaut->name = $robot->name;
        $astronaut->task = $robot->mission;
        $astronaut->hasOxygen = $robot->battery > 0;

        return $astronaut;
    }

    public function supportsTransformation(...): bool
    {
        return $data instanceof Robot && Astronaut::class === $to;
    }
}

Wrap it up

ViewEvent
ContextBuilder
By resource type
By operation
By request
Defaults
By operation
Documentation friendly
ResourceMetadataFactory
By resource type
By operation
Documentation friendly
$context

Wrap it up

ViewEvent
Serializer
By context
By resource instance
DTOs
$context, $data
By context
By resource instance
{"..."}

Thanks!

[APIP Con] A suitable serialization with API Platform and Symfony

By Mathias Arlaud

[APIP Con] A suitable serialization with API Platform and Symfony

Meetup 01/07/2021

  • 1,059