@matarld

Trying to make

API Platform's serialization

superfast

Mathias Arlaud

@matarld
mtarld
@bakslahHQ
@matarld

what's

serialization?

@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
GET /api/cats/1
Accept: application/json 
{
  "name": "Undercover",
  "aggressivity": "high"
}

serialization

@matarld
POST /api/cats
Content-Type: application/json 
{
  "name": "Black mustache",
  "aggressivity": "low"
}

deserialization

@matarld
SomeProvider
AnotherProvider
...
RespondProcessor
...
GET /api/cats/1
Accept: application/json 
Response
@matarld
SomeProvider
DeserializeProvider
...
POST /api/cats
Content-Type: application/json 
{
  "name": "Black mustache",
  "aggressivity": "low"
}
@matarld
@matarld
Just count mustaches
Capitalize the name
Evaluate aggressivity
Don't keep the id
@matarld
Create "n" default mustaches
Set claws size by aggressivity
Instantiate a new cat
@matarld
class Cat
{
  #[Ignore]
  public int $id;

  #[SerializedName('name')]
  private string $nickname;
  
  /** @var Mustache[] */
  public array $mustaches;
  
  public function getNickname(): string
  {
    return $this->name;
  }
}
@matarld
class Cat
{
  #[Ignore]
  public int $id;

  #[SerializedName('name')]
  private string $nickname;
  
  /** @var Mustache[] */
  public array $mustaches;
  
  public function getNickname(): string
  {
    return $this->nickname;
  }
}

Can I read $id?

No, you can ignore it.

@matarld
class Cat
{
  #[Ignore]
  public int $id;

  #[SerializedName('name')]
  private string $nickname;
  
  /** @var Mustache[] */
  public array $mustaches;
  
  public function getNickname(): string
  {
    return $this->nickname;
  }
}

Can I read $nickname?

Yep, but use the getter to read it.

Change "nickname" to "name".

Do anything with the key?

Nothing in particular.

Do anything with the value?

@matarld
class Cat
{
  #[Ignore]
  public int $id;

  #[SerializedName('name')]
  private string $nickname;
  
  /** @var Mustache[] */
  public array $mustaches;
  
  public function getNickname(): string
  {
    return $this->nickname;
  }
}

Can I read $mustaches?

Yes, you can.

Nothing in particular.

Do anything with the key?

Check if it's an object collection. If so, serialize objects of that collection as well.

Do anything with the value?

@matarld
class CatMustacheNormalizer implements NormalizerInterface
{
  public function normalize($object, $format, $context): array
  {
    $normalized = $this->normalizer->normalize($object, $format, $context);
    $normalized['mustaches'] = count($object->mustaches);
    
    return $normalized;
  }

  // ...
}
@matarld

🐌

Cache

@matarld

let's talk about

metadata

(                                             )

@matarld

 Metadata is data

data, but about data

@matarld

this is a

thing

@matarld

this is an

animal

@matarld

this is a

cat

@matarld

this is a

flying cat

@matarld

data

/** @temlate T of string */
final class Rain {
  /** @return list<T> */
  public function content(): array {...}
}

/** @var Rain<'cat'|'dog'> $*/
$englishRain = new Rain();

$fallingStuff = $englishRain->content();

type

no idea
array of something
list of something
list of cats and dogs

Possible JSON

[
  [0.7]
]
{
  "c": 1
}

true

{
  "a": 1
}
[
  3.14
]
[
  "pi"
]
[
  true
]
[
  3.14
]
[
  "dog"
]
[
  "cat"
]
[
  "dog"
]
[
  "dog"
]
@matarld
interface PropertyTypeExtractorInterface
{
    /**
     * @return Type[]|null
     */
    public function getTypes(...): ?array;
}
@matarld
interface PropertyTypeExtractorInterface
{
    /**
     * @return Type[]|null
     */
    public function getTypes(...): ?array;
}

only properties

no union/intersection difference

no list/dict difference

no generics

...

@matarld
/**
 * @template T of Animal
 */
final class FlyingAnimal
{
  public function __construct(
    public object $type,
    private int $speed,
  ) {
  }
  
  /**
   * @return class-string<T>
   */
  public function getTypeClass(): string
  {
    return $this->type::class;
  }
  
  public function setSpeed(int $speed): void
  {
  	$this->speed = $speed;
  }
}

PropertyInfo

@matarld

ft. @Korbeil_

@matarld
/**
 * @template T of Animal
 */
final class FlyingAnimal
{
  public function __construct(
    public object $type,
    private int $speed,
  ) {
  }
  
  /**
   * @return class-string<T>
   */
  public function getTypeClass(): string
  {
    return $this->type::class;
  }
  
  public function setSpeed(int $speed): void
  {
  	$this->speed = $speed;
  }
}

TypeInfo

@matarld

PropertyInfo

TypeInfo

@matarld
object
array
format
huge collection of objects
super big array
@matarld
format
huge collection of objects

Streaming!

@matarld
@matarld
class Cat
{
  public string $name;
  public bool $flying;
}

PHP

Possible JSON

{
  "name": "any_string",
  "flying": true|false
}
any string
true/false
@matarld
[
  {
    "name": "
[
  {
    "name": "Undercover
[
  {
    "name": "Undercover",
    "flying": 
[
  {
    "name": "Undercover",
    "flying": true
[
  {
    "name": "Undercover",
    "flying": true
  },
  {
    "name": "
[
  {
    "name": "Undercover",
    "flying": true
  },
  {
    "name": "Black mustache
[
  {
    "name": "Undercover",
    "flying": true
  },
  {
    "name": "Black mustache",
    "flying": 
[
  {
    "name": "Undercover",
    "flying": true
  },
  {
    "name": "Black mustache",
    "flying": false
[
  {
    "name": "Undercover",
    "flying": true
  },
  {
    "name": "Black mustache",
    "flying": false
​  }
]

in memory

in memory

@matarld

Encoder

return static function ($cats, $stream, $config) {
  $stream->write('[');
  $prefix_0 = '';
  foreach ($cats as $cat) {
    $stream->write($prefix_0);
    $stream->write('{"name":');
    $stream->write(json_encode($cat->name));
    $stream->write(',"flying":');
    $stream->write($cat->flying ? 'true' : 'false');
    $stream->write('}');
    $prefix_0 = ',';
  }
  $stream->write(']');
};
@matarld
Does an encoder exist?

I want to

serialize a cat

Yep 👌

Encode the cat
    /\_____/\
   /  o   o  \
  ( ==  ^  == )
   )         (
  (           )
 ( (  )   (  ) )
(__(__)___(__)__)
Compute the cat JSON shape

Not yet 😬

Generate and store the encoder
@matarld
RespondProcessor
...
GET /api/cats/1
Accept: application/json 
Response
@matarld
GET /api/cats/1
Accept: application/json 
StreamedResponse
SerializeAndRespondProcessor
...
@matarld

Read only needed

JSON part lazily

Blazing fast!

Flat memory usage

Stream ready

Generics ready

Simple API

Edit data on-the-fly

@matarld
   __
o-''|\_____/)
 \_/|_)     )
    \  __  /
    (_/ (_/  
@matarld
class Cat
{
  #[Groups('read')]
  public int $id;

  #[Groups(['read', 'write'])]
  public string $nickname;
  
  #[Groups(['admin'])]
  public bool $isAlive;
}
-
read
write
admin
read + write
read + admin
write + admin
read + write + admin
C = 2^n
@matarld
#[ORM\Entity]
#[ApiResource(
  normalizationContext: ['groups' => 'read'],
  denormalizationContext: ['groups' => 'write'],
)]
class Cat
{
  #[ORM\Id]
  #[ORM\GeneratedValue]
  #[ORM\Column]
  #[Groups('read')]
  public int $id;

  #[ORM\Column]
  #[SerializedName('name')]
  #[Groups(['read', 'write'])]
  public string $nickname;
}
@matarld
#[ORM\Entity]
#[ApiResource(
  normalizationContext: ['groups' => 'read'],
  denormalizationContext: ['groups' => 'write'],
)]
class Cat
{
  #[ORM\Id]
  #[ORM\GeneratedValue]
  #[ORM\Column]
  #[Groups('read')]
  public int $id;

  #[ORM\Column]
  #[SerializedName('name')]
  #[Groups(['read', 'write'])]
  public string $nickname;
}
@matarld

the takeaway

one

resource

one

representation

@matarld
#[ORM\Entity]
class CatEntity
{
  #[ORM\Id]
  #[ORM\GeneratedValue]
  #[ORM\Column]
  public int $id;

  #[ORM\Column]
  public string $nickname;
}
#[Get]
class CatGetResource
{
  public int $id;
  public string $name;
}
#[Post]
class CatPostResource
{
  public string $name;
}
@matarld
class CatGetResourceProvider implements ProviderInterface
{
    public function __construct(
        #[Autowire(service: ItemProvider::class)]
        private ProviderInterface $provider
    ) {
    }

    public function provide(Operation $operation, array $uriVariables, array $context)
    {
        $catEntity = $this->provider->provide($operation, $uriVariables, $context);
        
        $catGetResource = new CatGetResource();
        $catGetResource->id = $catEntity->id;
        $catGetResource->name = ucfirst($catEntity->nickname);
        
        return $catGetResource;
    }
}
@matarld
@matarld
#[Map(CatGetResource::class)]
class CatEntity
{
  #[Map(target: 'id')]
  public int $id;

  #[Map(
    target: 'name',
    transform: 'ucfirst',
  )]
  public string $nickname;
}
#[Get]
class CatGetResource
{
  public int $id;
  public string $name;
}
@matarld
class CatGetResourceProvider implements ProviderInterface
{
    public function __construct(
        #[Autowire(service: ItemProvider::class)]
        private ProviderInterface $provider,
        private ObjectMapperInterface $mapper,
    ) {
    }

    public function provide(Operation $operation, array $uriVariables, array $context)
    {
        return $this->mapper->map(
          source: $this->provider->provide($operation, $uriVariables, $context),
          target: CatGetResource::class,
        );
    }
}
@matarld
JsonEncoder
+ Mapper
Serializer

x40

serialize

@matarld

x10

deserialize

JsonEncoder
+ Mapper
Serializer
@matarld

what's

next?

@matarld

thanks!

@matarld
{
  "name": "Jose",
  "aggressivity": "none",
  "mustaches": 4
}

normalization

encoding

@matarld
{
  "name": "Jose",
  "aggressivity": "none",
  "mustaches": 4
}

decoding

denormalization

@matarld
@matarld

BC policy

AbstractNormalizer
AbstractObjectNormalizer
AbstractItemNormalizer
ItemNormalizer

[APIP Con] Faster API Platform serialization

By Mathias Arlaud

[APIP Con] Faster API Platform serialization

  • 18