Julien Fastré 8a684734e7
Fixed: fix docgen normalization on household with "old" members
When a household had old members, the indexes of each "current" members
should be numerical and contiguous, to be transformed in a list. If this
is not the case, the members are mapped to an associative array.

This commit alter the generic DocGenObjectNormalizer to ensure that
the ReadableCollection are normalized using the
CollectionDocGenNormalizer as default, which do not preserve keys.
2023-05-16 23:30:02 +02:00

310 lines
12 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\Common\Collections\ReadableCollection;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use function array_filter;
use function array_key_exists;
use function array_merge;
use function get_class;
use function implode;
use function in_array;
use function is_array;
use function is_object;
class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
use NormalizerAwareTrait;
private ClassMetadataFactoryInterface $classMetadataFactory;
private PropertyAccessor $propertyAccess;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
ClassMetadataFactoryInterface $classMetadataFactory,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->classMetadataFactory = $classMetadataFactory;
$this->propertyAccess = PropertyAccess::createPropertyAccessor();
$this->translatableStringHelper = $translatableStringHelper;
}
public function normalize($object, $format = null, array $context = [])
{
$classMetadataKey = $object ?? $context['docgen:expects'] ?? null;
if (null === $classMetadataKey) {
throw new RuntimeException('Could not determine the metadata for this object. Either provide a non-null object, or a "docgen:expects" key in the context');
}
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
throw new LogicException(sprintf(
'This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s',
is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/ ,
$format,
implode(', ', ($context['groups'] ?? []))
));
}
$metadata = $this->classMetadataFactory->getMetadataFor($classMetadataKey);
$expectedGroups = array_key_exists(AbstractNormalizer::GROUPS, $context) ?
is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]]
: [];
$attributes = array_filter(
$metadata->getAttributesMetadata(),
static function (AttributeMetadata $a) use ($expectedGroups) {
foreach ($a->getGroups() as $g) {
if (in_array($g, $expectedGroups, true)) {
return true;
}
}
return false;
}
);
if (null === $object) {
return $this->normalizeNullData($format, $context, $metadata, $attributes);
}
return $this->normalizeObject($object, $format, $context, $expectedGroups, $metadata, $attributes);
}
public function supportsNormalization($data, $format = null): bool
{
return 'docgen' === $format && (is_object($data) || null === $data);
}
private function getExpectedType(AttributeMetadata $attribute, ReflectionClass $reflection): string
{
$type = null;
do {
// we have to get the expected content
if ($reflection->hasProperty($attribute->getName())) {
if (!$reflection->getProperty($attribute->getName())->hasType()) {
throw new \LogicException(sprintf(
'Could not determine how the content is determined for the attribute %s on class %s. Add a type on this property',
$attribute->getName(),
$reflection->getName()
));
}
$type = $reflection->getProperty($attribute->getName())->getType();
} elseif ($reflection->hasMethod($method = 'get' . ucfirst($attribute->getName()))) {
if (!$reflection->getMethod($method)->hasReturnType()) {
throw new \LogicException(sprintf(
'Could not determine how the content is determined for the attribute %s on class %s. Add a return type on the method',
$attribute->getName(),
$reflection->getName()
));
}
$type = $reflection->getMethod($method)->getReturnType();
} elseif ($reflection->hasMethod($method = 'is' . ucfirst($attribute->getName()))) {
if (!$reflection->getMethod($method)->hasReturnType()) {
throw new \LogicException(sprintf(
'Could not determine how the content is determined for the attribute %s on class %s. Add a return type on the method',
$attribute->getName(),
$reflection->getName()
));
}
$type = $reflection->getMethod($method)->getReturnType();
} elseif ($reflection->hasMethod($attribute->getName())) {
if (!$reflection->getMethod($attribute->getName())->hasReturnType()) {
throw new \LogicException(sprintf(
'Could not determine how the content is determined for the attribute %s on class %s. Add a return type on the method',
$attribute->getName(),
$reflection->getName()
));
}
$type = $reflection->getMethod($attribute->getName())->getReturnType();
} else {
$reflection = $reflection->getParentClass();
}
} while (null === $type && $reflection instanceof ReflectionClass);
if (null === $type ?? null) {
throw new \LogicException(sprintf(
'Could not determine the type for this attribute: %s. Add a return type to the method or property declaration',
$attribute->getName()
));
}
return $type->getName();
}
/**
* @param array|AttributeMetadata[] $attributes
*/
private function normalizeNullData(string $format, array $context, ClassMetadata $metadata, array $attributes): array
{
$keys = [];
// add a discriminator
if (null !== $discriminator = $metadata->getClassDiscriminatorMapping()) {
$typeKey = $discriminator->getTypeProperty();
$typeValue = null;
foreach ($discriminator->getTypesMapping() as $type => $typeClass) {
if ($typeClass === $context['docgen:expects']) {
$typeValue = $type;
break;
}
}
if (null === $typeValue) {
$typeKey = null;
}
} else {
$typeKey = $typeValue = null;
}
foreach ($attributes as $attribute) {
$key = $attribute->getSerializedName() ?? $attribute->getName();
$keys[$key] = $this->getExpectedType($attribute, $metadata->getReflectionClass());
}
$normalizer = new NormalizeNullValueHelper($this->normalizer, $typeKey, $typeValue);
return $normalizer->normalize($keys, $format, $context, $metadata);
}
/**
* @param mixed $format
*/
private function normalizeNullOutputValue($format, array $context, AttributeMetadata $attribute, ReflectionClass $reflection)
{
$type = $this->getExpectedType($attribute, $reflection);
switch ($type) {
case 'array':
if (in_array('is-translatable', $attribute->getNormalizationContextForGroups(['docgen:read']), true)) {
return '';
}
return [];
case 'bool':
case 'double':
case 'float':
case 'int':
case 'resource':
return null;
case 'string':
return '';
default:
return $this->normalizer->normalize(
null,
$format,
array_merge(
$context,
['docgen:expects' => $type]
)
);
}
}
/**
* @param $object
* @param $format
* @param array|AttributeMetadata[] $attributes
*
* @throws ExceptionInterface
*
* @return array
*/
private function normalizeObject($object, $format, array $context, array $expectedGroups, ClassMetadata $metadata, array $attributes)
{
$data = [];
$data['isNull'] = false;
$reflection = $metadata->getReflectionClass();
// add a discriminator
if (null !== $discriminator = $metadata->getClassDiscriminatorMapping()) {
$data[$discriminator->getTypeProperty()] = $discriminator->getMappedObjectType($object);
}
foreach ($attributes as $attribute) {
/** @var AttributeMetadata $attribute */
$value = $this->propertyAccess->getValue($object, $attribute->getName());
$key = $attribute->getSerializedName() ?? $attribute->getName();
$objectContext = array_merge(
$context,
$attribute->getNormalizationContextForGroups(
is_array($context['groups']) ? $context['groups'] : [$context['groups']]
)
);
$isTranslatable = $objectContext['is-translatable'] ?? false;
if ($isTranslatable) {
$data[$key] = $this->translatableStringHelper
->localize($value);
} elseif ($value instanceof ReadableCollection) {
// when normalizing collection, we should not preserve keys (to ensure that the result is a list)
// this is why we make call to the normalizer again to use the CollectionDocGenNormalizer
$data[$key] =
$this->normalizer->normalize($value, $format, array_merge(
$objectContext,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (is_iterable($value)) {
$arr = [];
foreach ($value as $k => $v) {
$arr[$k] =
$this->normalizer->normalize($v, $format, array_merge(
$objectContext,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
}
$data[$key] = $arr;
} elseif (is_object($value)) {
$data[$key] =
$this->normalizer->normalize($value, $format, array_merge(
$objectContext,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (null === $value) {
$data[$key] = $this->normalizeNullOutputValue($format, $objectContext, $attribute, $reflection);
} else {
$data[$key] = $value;
}
}
return $data;
}
}