Mathias Arlaud
Co-Founder & COO @Bakslash - Co-Founder & CTO @Synegram
A suitable serialization with
Mathias Arlaud
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
https://symfony.com/doc/current/components/serializer.html
#[ApiResource]
class Robot
{
public int $id;
public string $name;
public string $mission;
public string $unofficialMission;
}
#[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
#[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"
}
Customizing serialization
#[ApiResource]
class Robot
{
public int $id;
public string $name;
public string $mission;
#[Ignore]
public string $unofficialMission;
}
Customizing serialization
#[ApiResource]
class Robot
{
public int $id;
public string $name;
public string $mission;
#[ApiProperty(readable: false, writable: false)]
public string $unofficialMission;
}
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!
Customizing serialization
public function serialize($data, string $format, array $context = []);
array $context = []);
Customizing serialization
class Robot
{
#[Groups(['group-one', 'group-two'])]
public string $name;
#[Groups(['group-one'])]
public string $mission;
public string $unofficialMission;
}
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"}
#[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']],
)]
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;
}
SerializeListener
SerializeListener
ContextBuilder
interface SerializerContextBuilderInterface {
public function createFromRequest(
Request $request,
bool $normalization,
?array $attributes = null,
): array;
}
Serializer
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
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, ...
#[ApiResource(normalizationContext: ['groups' => ['read']])]
class Robot {
#[Groups(['read'])]
public string $name;
#[Groups(['creator'])]
public string $mentalHealth;
public User $creator;
}
SerializeListener
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
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')]
API Platform 2.6
#[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{}
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"]]]
# ...
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", "..."]
}
}
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;
}
Dynamic groups
ResourceMetadataFactory
App\Entity\Robot
SerializeListener
ContextBuilder
Serializer
ResourceMetadata
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
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
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;
}
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;
}
}
ViewEvent
ContextBuilder
By resource type By operation By request
Defaults
By operation Documentation friendly
ResourceMetadataFactory
By resource type By operation Documentation friendly
$context
ViewEvent
Serializer
By context By resource instance
DTOs
$context, $data
By context By resource instance
{"..."}
By Mathias Arlaud