6.4 KiB
Normalizing for DocGen
In Chill, some entities can be normalized in the docgen format, which is specifically used for document generation.
The docgen Format Requirements
This format has specific requirements regarding null values. When serializing a null value, it must not be serialized as a literal null. Instead, it must be serialized as an object (or array) containing all the keys that would be present if the object were not null.
Each key must have, as value:
- An empty string value (for scalars).
- A boolean
- An empty array (for collections).
- Or, if the expected type is another object, it must contain all the keys for that object, recursively.
This ensures that the document generator always finds the expected keys, even if the data is missing.
Additionally, every normalized form must include an isNull key (boolean). This helps the document template distinguish between an actual object and its "null" representation.
Attribute-Based Normalization
The simplest way to support docgen normalization is to use Symfony Serializer attributes.
- Use the group
docgen:readon properties that should be included. - For translatable fields (stored as JSON/array of translations), you must also add the context
is-translatable => true.
Example: Country.php
The Country entity demonstrates this approach:
namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\SerializedName;
#[ORM\Entity]
class Country
{
#[Groups(['read', 'docgen:read'])]
#[SerializedName('code')]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 3)]
private string $countryCode = '';
#[Groups(['read', 'docgen:read'])]
#[ORM\Id]
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
private ?int $id = null;
#[Groups(['read', 'docgen:read'])]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
#[Context(['is-translatable' => true], groups: ['docgen:read'])]
private array $name = [];
// ...
}
For working properly, the property or return type must be properly set.
Custom Normalizer
For more complex entities, you may need to implement a custom normalizer.
Requirements for docgen Normalizers
-
Handle
nullinsupportsNormalization: The normalizer must returntrueif the data isnullAND the context contains adocgen:expectskey matching the class the normalizer handles.public function supportsNormalization($data, $format = null, array $context = []): bool { if ('docgen' === $format) { return $data instanceof MyEntity || (null === $data && MyEntity::class === ($context['docgen:expects'] ?? null)); } return false; } -
Implementation of
getSupportedTypes: To avoid side effects and optimize performance, you must return'*' => falsealong with the specific class.public function getSupportedTypes(?string $format): array { if ('docgen' === $format) { return [ MyEntity::class => true, '*' => false, ]; } return []; } -
Handle
nullinnormalize: Thenormalizemethod must detect when the input isnulland return the "empty" structure with all keys.
Using NormalizeNullValueHelper
To help with normalizing null values recursively, you can use the \Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper class.
This helper takes an array defining the keys and their expected types. If a type is a class-string, it will call the serializer again to normalize a null value for that class.
Example: AddressNormalizer.php
The AddressNormalizer is a good example of a custom normalizer handling docgen:
class AddressNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private const array NULL_VALUE = [
'address_id' => 'int',
'text' => 'string',
'street' => 'string',
// ...
'validFrom' => \DateTimeInterface::class,
'postcode' => PostalCode::class,
];
public function normalize($address, $format = null, array $context = []): array
{
if ($address instanceof Address) {
// ... normal normalization logic
if ('docgen' === $format) {
$data['postcode'] = $this->normalizer->normalize(
$address->getPostcode(),
$format,
[...$context, 'docgen:expects' => PostalCode::class]
);
}
return $data;
}
if (null === $address && 'docgen' === $format) {
$helper = new NormalizeNullValueHelper($this->normalizer);
return $helper->normalize(self::NULL_VALUE, $format, $context);
}
// ...
}
// ... supportsNormalization and getSupportedTypes as described above
}
Testing docgen Normalization
To ensure that your normalizer strictly follows the docgen requirements, you should extend \Chill\DocGeneratorBundle\Test\DocGenNormalizerTestAbstract.
This abstract test class performs several critical checks:
- It ensures that the normalized form of a non-null object and a
nullvalue have exactly the same keys (same array shape). - It verifies that the
isNullkey is present and correctly set (falsefor objects,truefornull). - It recursively checks that sub-objects also have consistent keys.
- It ensures that
nullvalues are never returned (they should be empty strings, empty arrays, etc.).
Example Test: AddressDocGenNormalizerTest.php
You need to implement two methods: provideNotNullObject() and provideDocGenExpectClass().
namespace Chill\MainBundle\Tests\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Test\DocGenNormalizerTestAbstract;
use Chill\MainBundle\Entity\Address;
class AddressDocGenNormalizerTest extends DocGenNormalizerTestAbstract
{
public function provideNotNullObject(): object
{
// Return a fully populated instance of the entity
return new Address()
->setStreet('Rue de la Loi')
// ...
;
}
public function provideDocGenExpectClass(): string
{
return Address::class;
}
}