mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-02-28 02:59:40 +00:00
Add documentation for docgen normalization: format requirements, attribute-based normalization, custom normalizer guidelines, and testing strategies.
This commit is contained in:
188
docs/source/development/normalizing-for-doc-gen.md
Normal file
188
docs/source/development/normalizing-for-doc-gen.md
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user