Files
chill-bundles/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php

281 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 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;
class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
use NormalizerAwareTrait;
private readonly PropertyAccessor $propertyAccess;
public function __construct(
private readonly ClassMetadataFactoryInterface $classMetadataFactory,
private readonly TranslatableStringHelperInterface $translatableStringHelper
) {
$this->propertyAccess = PropertyAccess::createPropertyAccessor();
}
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) ? $object::class : '(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 ($type instanceof \ReflectionNamedType) {
return $type->getName();
}
if ($type instanceof \ReflectionIntersectionType) {
foreach (array_map(fn (\ReflectionNamedType $t) => $t->getName(), $type->getTypes()) as $classString) {
if (ReadableCollection::class === $classString) {
return ReadableCollection::class;
}
$class = new \ReflectionClass($classString);
if ($class->implementsInterface(ReadableCollection::class)) {
return ReadableCollection::class;
}
}
throw new \LogicException(sprintf('The automatic normalization of intersection types is not supported, unless a %s is contained in the intersected types', ReadableCollection::class));
} elseif (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()));
}
throw new \LogicException(sprintf('The automatic normalization of %s is not supported', $type::class));
}
/**
* @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);
}
private function normalizeNullOutputValue(mixed $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 array|AttributeMetadata[] $attributes
*
* @return array
*
* @throws ExceptionInterface
*/
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;
}
}