Mathias Arlaud
Co-Founder & COO @Bakslash - Co-Founder & CTO @Synegram
@matarld
mtarld
les-tilleuls.coop
@matarld
@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
https://symfony.com/doc/current/components/serializer.html
@matarld
#[ApiResource]
class Robot
{
    public int $id;
    public string $name;
    public string $mission;
    public string $unofficialMission;
}
@matarld
#[ApiResource]
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]
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]
class Robot
{
  public int $id;
  public string $name;
  public string $mission;
  #[Ignore]
  public string $unofficialMission;
}
@matarld
#[ApiResource]
class Robot
{
  public int $id;
  public string $name;
  public string $mission;
  #[ApiProperty(readable: false, writable: false)]
  public string $unofficialMission;
}@matarld
#[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!
@matarld
public function serialize($data, string $format, array $context = []);@matarld
class Robot
{
  #[Groups(['group-one', 'group-two'])]
  public string $name;
  #[Groups(['group-one'])]
  public string $mission;
  public string $unofficialMission;
}
@matarld
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"}@matarld
> curl /api/robots/1.json
['item']
> curl /api/robots.json
['list']
#[ApiResource(
  new Get(
    normalizationContext: ['groups' => ['item']],
  ),
  new GetCollection(
    normalizationContext: ['groups' => ['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']],
)]
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']])]
class Robot
{ 
  #[Groups(['read'])]
  public string $unofficialMission;
}
#[ApiProperty(
  security: 'is_granted("ROLE_SECRET_SERVICE")',
)]@matarld
#[ApiResource(normalizationContext: ['groups' => ['read']])]
class Robot {
  #[Groups(['read'])]
  public string $name;
  
  #[Groups(['creator'])]
  public string $mentalHealth;
  
  public User $creator;
}@matarld
SerializeListenerContextBuilder
interface NormalizerInterface
{
  public function normalize(
    $data,
    string $format = null,
    array $context = []
  );
  public function supportsNormalization(
    $data,
    string $format = null,
    array $context = []
  ): bool;
}Serializer
@matarld
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']])]
class Robot
{ 
  #[Groups(['read'])]
  public string $unofficialMission;
}
#[ApiProperty(
  security: 'object.creator == user',
)]@matarld
#[ApiResource(
  new Get(
    normalizationContext: ['groups' => ['read']],
  ),
  new GetCollection(
    normalizationContext: ['groups' => ['read']],
  ),
)]
class Robot {}#[ApiResource(
  new Get(
    normalizationContext: ['groups' => ['read']],
  ),
  new GetCollection(
    normalizationContext: ['groups' => ['read']],
  ),
)]
class Datasheet {}@matarld
#[ApiResource(
  normalizationContext: ['groups' => ['read']],
)]
class Datasheet {}#[ApiResource(
  normalizationContext: ['groups' => ['read']],
)]
class Robot {}@matarld
#[ApiResource]
class Datasheet {}#[ApiResource]
class Robot {}api_platform:
  defaults:
    normalizationContext:
      groups: ["read"]
      
    # ...
    @matarld
#[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", "..."]
  }
}@matarld
#[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;
}{
  "datasheet": {
    "reference": "PE-01"
  }
}@matarld
ResourceMetadataCollectionFactory
App\Entity\Robot
SerializeListener
Serializer
ResourceMetadataCollection
ContextBuilder
Operation
@matarld
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]
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;
}@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
class Robot
{
  public string $name;
  
  public string $mission;
  
  public int $battery;
  public Datasheet $datasheet;
}class Astronaut
{
  public string $name;
  
  public string $task;
  
  public bool $hasOxygen;
}@matarld
#[ApiResource(
  operations: [
    new Get(),
  ]
)]
class Robot {}#[ApiResource(
  operations: [
    new GetCollection(
      uriTemplate: '/robots',
      provider: AstronautsProvider::class,
    ),
  ]
)]
class Astronaut {}@matarld
final class AstronautsProvider implements ProviderInterface
{
  public function provide($operation, $uriVariables, $context): array
  {
    $robots = $this->robotRepository->findAll();
    
    return array_map(static function (Robot $r): Astronaut {
      $a = new Astronaut();
      $a->name = $r->name;
      $a->task = $r->mission;
      $a->hasOxygen = $r->battery > 0;
      return $a;
    }, $robots);
}
@matarld
New resource
By resource type By operation Documentation friendly
By Mathias Arlaud