Generics in symfony/serializer

Viewed 295

I use the symfony serializer to deserialize REST server answers to objects.

The data returned by the server is someting like this (pseudocode, the answer itself is JSON):

// Endpoint 1
class Paginated {
  public items:Object1[]
  public page:int
}

// Endpoint 2
class Paginated {
 public: items:Object2[]
 public page:int
}

So, every answer is wrapped in the same "Paginated" object.

Sice I don't want to repeat the common members in every object I want to implement the objects in my Symfony app the same way as described in this pseudo code.

The problem is, that PHP isn't supporting generics to typehint the "items" member and the symfony serializer doesn't seem to offer something similar.

So, whats the best way to tackle this problem?

1 Answers

An easy way to solve the issue is by using the callbacks context option. In that callback you can just deserialize the objects you are going to pass into Paginated yourself. You would then have different callbacks for each object-type you want to support in Paginated and register it. It doesn't have to be a closure like shown in the docs, you can also use a class with __invoke() to make it easier to reuse in different places.

Another way to solve this more generically is, by writing a custom Denormalizer that implements the DenormalizerAwareInterface (so it can delegate denormalization of the nested items back to the serializer.

Much like Symfony can recognize something like Object[] as a list of Objects, you can created your own custom type convention to simulate a Generic.

Assuming you want this to be something like Paginated<Object1>, then your serializer call would probably look something like this:

$serializer->deserialize($json, Paginated::class . '<' . Object1::class . '>', 'json');

Your (de)normalizer will then support the type matching the regex. Inside the denormalize method you would then take the array structure of your json, call something like `$denormalizedItems = $this->denormalizer->denormalize($data['items'], Object1::class . '[]'); and then put them into your Paginated object. Roughly like this:

public function denormalize($data, string $type, string $format = null, array $context = [])
{
    $extractedObjectType = ...; #extract class name inside <>

    $data['items'] = $this->denormalizer->denormalize($data['items'], $extractedType, $format, $context);

    // Option 1: Delegate denormalizing Paginated with the adjusted data
    return $this->denormalizer->denormalize($data, Paginated::class, $format, $context);

    // Option 2: Denormalize Paginated yourself and pass adjusted data as argument
    return new Paginated($data['items'], (int) $data['page']);
}
Related