Mathias Arlaud
Co-Founder & COO @Bakslash - Co-Founder & CTO @Synegram
A suitable serialization with
@matarld
@matarld
mtarld
les-tilleuls.coop
@matarld
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
@matarld
18
true
['foo']
Bar {+baz: true}
// data
@matarld
18
true
['foo']
$bar
// data
serialize(18);
serialize(true);
serialize(['foo']);
serialize($bar);
// serialization
@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
@matarld
i:18;
b:1;
a:1:{i:0;s:3:"foo";};
O:3:"Bar":1:{s:3:"baz";b:1;};
// serialized
@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
@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
@matarld
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
@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}
@matarld
18
true
["foo"]
{"baz": true}
// serialized
@matarld
json_decode('18');
json_decode('true');
json_decode('["foo"]');
json_decode('{"baz": true}');
// deserialization
18
true
["foo"]
{"baz": true}
// serialized
@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
@matarld
@matarld
@matarld
$serializer = new Serializer(
normalizers: [new PropertyNormalizer()],
encoders: [new JsonEncoder()],
);
$serialized = $serializer->serialize($user, 'json');
$deserialized = $serializer->deserialize($string, User::class, 'json');
@matarld
@matarld
#[ApiResource]
final class Robot
{
public int $id;
public string $name;
public string $mission;
public string $unofficialMission;
}
@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
@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
@matarld
#[ApiResource]
final class Robot
{
public int $id;
public string $name;
public string $mission;
#[Ignore]
public string $unofficialMission;
}
@matarld
#[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
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;
}
@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"}
@matarld
#[ApiResource(
new Get(
normalizationContext: ['groups' => ['item']],
),
new GetCollection(
normalizationContext: ['groups' => ['list']],
),
)]
> curl /api/robots/1.json
['item']
> curl /api/robots.json
['list']
@matarld
> 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
#[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
@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
@matarld
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
#[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
@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
@matarld
#[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 {}
@matarld
#[ApiResource]
final class Datasheet {}
#[ApiResource]
final class Robot {}
api_platform:
defaults:
normalizationContext:
groups: ["read"]
@matarld
#[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"
}
}
@matarld
ContextBuilder
ResourceMetadataCollectionFactory
App\ApiResource\Robot
SerializeListener
Serializer
ResourceMetadataCollection
Operation
@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 {}
}
@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;
}
@matarld
ViewEvent
ContextBuilder
By resource type
By operation
By request
Defaults
By operation Documentation friendly
ResourceMetadata
By resource type By operation Documentation friendly
$context
@matarld
ViewEvent
Serializer
By context
By resource instance
$context, $data
{"..."}
@matarld
@matarld
final class DatabaseConnection
{
public function __construct(
private readonly string $dsn,
) {
}
}
O:18:"DatabaseConnection":1:{s:23:"DatabaseConnection\x00dsn";s:7:"the_dsn";};
@matarld
final class DatabaseConnection
{
private readonly \PDO $pdo;
public function __construct(
private readonly string $dsn,
) {
$this->pdo = new \PDO($this->dsn);
}
}
@matarld
final class DatabaseConnection
{
// ...
public function __sleep(): array
{
return ['dsn'];
}
public function __wakeup(): void
{
$this->pdo = new \PDO($this->dsn);
}
}
@matarld
final class DatabaseConnection implements \Serializable
{
// ...
public function serialize(): string
{
return $this->dsn;
}
}
the_dsn
@matarld
final class DatabaseConnection implements \Serializable
{
// ...
public function unserialize(string $data): void
{
$this->dsn = $data;
$this->pdo = new \PDO($data);
}
}
By Mathias Arlaud