# 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:read` on 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: ```php 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 1. **Handle `null` in `supportsNormalization`**: The normalizer must return `true` if the data is `null` AND the context contains a `docgen:expects` key matching the class the normalizer handles. ```php 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; } ``` 2. **Implementation of `getSupportedTypes`**: To avoid side effects and optimize performance, you must return `'*' => false` along with the specific class. ```php public function getSupportedTypes(?string $format): array { if ('docgen' === $format) { return [ MyEntity::class => true, '*' => false, ]; } return []; } ``` 3. **Handle `null` in `normalize`**: The `normalize` method must detect when the input is `null` and 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`: ```php 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 `null` value have **exactly the same keys** (same array shape). - It verifies that the `isNull` key is present and correctly set (`false` for objects, `true` for `null`). - It recursively checks that sub-objects also have consistent keys. - It ensures that `null` values are never returned (they should be empty strings, empty arrays, etc.). ### Example Test: `AddressDocGenNormalizerTest.php` You need to implement two methods: `provideNotNullObject()` and `provideDocGenExpectClass()`. ```php 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; } } ```