diff --git a/docs/source/development/normalizing-for-doc-gen.md b/docs/source/development/normalizing-for-doc-gen.md new file mode 100644 index 000000000..da0b95331 --- /dev/null +++ b/docs/source/development/normalizing-for-doc-gen.md @@ -0,0 +1,188 @@ +# 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; + } +} +``` +