A suitable serialization with

API Platform and Symfony

@matarld

Mathias Arlaud

@matarld
mtarld
les-tilleuls.coop
@matarld

Serialization 101

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

@matarld

Serialization

@matarld
18
true
['foo']
Bar {+baz: true}
// data

serialize/unserialize

@matarld
18
true
['foo']
$bar
// data
serialize(18);
serialize(true);
serialize(['foo']);
serialize($bar);
// serialization

serialize/unserialize

@matarld
18
true
['foo']
$bar
// data
serialize(18);
serialize(true);
serialize(['foo']);
serialize($bar);
// serialization
i:18;
b:1;
a:1:{i:0;s:3:"foo";};
O:3:"Bar":1:{s:3:"baz";b:1;};
// serialized

serialize/unserialize

@matarld
i:18;
b:1;
a:1:{i:0;s:3:"foo";};
O:3:"Bar":1:{s:3:"baz";b:1;};
// serialized

serialize/unserialize

@matarld
unserialize('i:18;');
unserialize('b:1;');
unserialize('...');
unserialize('...');
// deserialization
i:18;
b:1;
a:1:{i:0;s:3:"foo";};
O:3:"Bar":1:{s:3:"baz";b:1;};
// serialized

serialize/unserialize

@matarld
i:18;
b:1;
a:1:{i:0;s:3:"foo";};
O:3:"Bar":1:{s:3:"baz";b:1;};
// serialized
unserialize('i:18;');
unserialize('b:1;');
unserialize('...');
unserialize('...');
// deserialization
18
true
['foo']
Bar {+baz: true}
// data

serialize/unserialize

@matarld

json_encode/json_decode

18
true
['foo']
Bar {+baz: true}
// data
@matarld
18
true
['foo']
$bar
// data
json_encode(18);
json_encode(true);
json_encode(['foo']);
json_encode($bar);
// serialization

json_encode/json_decode

@matarld
// data
// serialization
// serialized
18
true
['foo']
$bar
json_encode(18);
json_encode(true);
json_encode(['foo']);
json_encode($bar);
18
true
["foo"]
{"baz": true}

json_encode/json_decode

@matarld
18
true
["foo"]
{"baz": true}
// serialized

json_encode/json_decode

@matarld
json_decode('18');
json_decode('true');
json_decode('["foo"]');
json_decode('{"baz": true}');
// deserialization
18
true
["foo"]
{"baz": true}
// serialized

json_encode/json_decode

@matarld
18
true
["foo"]
{"baz": true}
// serialized
json_decode('18');
json_decode('true');
json_decode('["foo"]');
json_decode('{"baz": true}');
// deserialization
18
true
['foo']
['baz' => true]
// data

json_encode/json_decode

Symfony Serializer

@matarld
@matarld

Symfony Serializer

@matarld
$serializer = new Serializer(
  normalizers: [new PropertyNormalizer()],
  encoders: [new JsonEncoder()],
);

$serialized = $serializer->serialize($user, 'json');
$deserialized = $serializer->deserialize($string, User::class, 'json');

Symfony Serializer

API Platform

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

    public string $name;

    public string $mission;

    public string $unofficialMission;
}

Beep boop!

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

    public string $name;

    public string $mission;

    public string $unofficialMission;
}
{
  "id": 1,
  "name": "Persévérance",
  "mission": "Find life on Mars",
  "unofficialMission": "Wipe out all life on Mars"
}
> curl /api/robots/1.json

Beep boop!

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

    public string $name;

    public string $mission;

    public string $unofficialMission;
}
{
  "id": 1,
  "name": "Persévérance",
  "mission": "Find life on Mars",
  "unofficialMission": "Wipe out all life on Mars"
}
> curl /api/robots/1.json

Customizing serialization

@matarld

Ignore

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

  public string $name;

  public string $mission;

  #[Ignore]
  public string $unofficialMission;
}
@matarld

Ignore

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

  public string $name;

  public string $mission;

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

Groups!

@matarld

Groups

interface SerializerInterface
{
  public function serialize($data, $format, $context = []);
  
  public function deserialize($data, $type, $format, $context = []);
}
@matarld
#[ApiResource]
final class Robot
{
  #[Groups(['group-one', 'group-two'])]
  public string $name;

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

  public string $unofficialMission;
}

Groups

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

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

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

Groups

@matarld

Groups and API Platform

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

API Platform and Symfony

> 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
@matarld

Context Builders

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

  #[Groups(['read'])]
  public string $mission;
  
  #[Groups(['secret_service'])]
  public string $unofficialMission;
}
@matarld
SerializeListener
ContextBuilder
interface SerializerContextBuilderInterface
{
  public function createFromRequest(
    Request $request,
    bool $normalization,
    ?array $attributes = null,
  ): array; 
}
Serializer

Context Builders

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

Context used by the serializer

Custom logic

API Platform generated context

Context Builders

@matarld

ApiProperty::$security

ExpressionLanguage

user, object, is_granted, ...
#[ApiResource(normalizationContext: ['groups' => ['read']])]
final class Robot
{ 




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

Serializers

#[ApiResource(
  normalizationContext: ['groups' => ['read']],
)]
final class Robot {
  #[Groups(['read'])]
  public string $name;
  
  #[Groups(['creator'])]
  public string $mentalHealth;
  
  public User $creator;
}
@matarld
SerializeListener
interface NormalizerInterface
{
  public function normalize(
    $data,
    string $format = null,
    array $context = [],
  );

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

Serializers

@matarld
final 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

Serializers

@matarld

ApiProperty::$security

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




  #[Groups(['read'])]
  public string $mentalHealth;
}
#[ApiProperty(
  security: 'object.creator == user',
)]
@matarld
#[ApiResource(
  normalizationContext: ['groups' => ['read']],
)]
final class Robot {}
#[ApiResource(
  normalizationContext: ['groups' => ['read']],
)]
final class Datasheet {}

Defaults

@matarld
#[ApiResource]
final class Datasheet {}
#[ApiResource]
final class Robot {}
api_platform:
  defaults:
    normalizationContext:
      groups: ["read"]

Defaults

@matarld

Resource Metadata

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

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

    #[Groups(['read'])]
    public array $specs;
}
{
  "datasheet": {
    "reference": "PE-01",
    "specs": ["lot of data", "..."]
  }
}
@matarld
#[ApiResource(
  normalizationContext: ['groups' => ['robot:read']],
)]
final class Robot {
  #[Groups(['robot:read'])]
  public Datasheet $datasheet;
}

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

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

Resource Metadata

@matarld
ContextBuilder
ResourceMetadataCollectionFactory
App\ApiResource\Robot
SerializeListener
Serializer
ResourceMetadataCollection
Operation

Resource Metadata

@matarld
final class GroupResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
{
  public function create(string $resourceClass): ResourceMetadataCollection
  {
    $metadata = $this->decorated->create($resourceClass);
    
    foreach ($resources as $i => $resource) {
        $metadata[$i] = $resource->withOperations($this->addGroupsToOperations($resource));
    }
    
    return $metadata;
  }
    
  // Return operations with dynamic groups (eg: robot:read, robot:list, or robot:item).
  private function addGroupsToOperations(ApiResource $metadata): ApiResource {}
}

Resource Metadata

@matarld
SerializeListener
Serializer
ContextBuilder
#[ApiResource]
final class Robot {
  #[Groups(['robot:read'])]
  public Datasheet $datasheet;
}

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

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

Resource Metadata

@matarld

Wrap it up!

ViewEvent
ContextBuilder
By resource type
By operation
By request
Defaults
By operation
Documentation friendly
ResourceMetadata
By resource type
By operation
Documentation friendly
$context
@matarld

Wrap it up!

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

Thank you!

@matarld
@matarld
final class DatabaseConnection
{
  public function __construct(
    private readonly string $dsn,
  ) {
  }
}
O:18:"DatabaseConnection":1:{s:23:"DatabaseConnection\x00dsn";s:7:"the_dsn";};

__sleep/__wakeup

@matarld
final class DatabaseConnection
{
  private readonly \PDO $pdo;

  public function __construct(
    private readonly string $dsn,
  ) {
    $this->pdo = new \PDO($this->dsn);
  }
}

__sleep/__wakeup

@matarld
final class DatabaseConnection
{
  // ...
  
  public function __sleep(): array
  {
    return ['dsn'];
  }
  
  public function __wakeup(): void
  {
    $this->pdo = new \PDO($this->dsn);
  }
}

__sleep/__wakeup

@matarld
final class DatabaseConnection implements \Serializable
{
  // ...
  
  public function serialize(): string
  {
    return $this->dsn;
  }
}
the_dsn

Serializable

@matarld
final class DatabaseConnection implements \Serializable
{
  // ...
  
  public function unserialize(string $data): void
  {
    $this->dsn = $data;
    $this->pdo = new \PDO($data);
  }
}

Serializable