Files
chill-bundles/docs/source/development/normalizing-for-doc-gen.md

189 lines
6.4 KiB
Markdown

# 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;
}
}
```