296 lines
11 KiB
PHP

<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
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, ?string $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, ?string $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);
}
/**
* @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();
$isTranslatable = $attribute->getNormalizationContextForGroups(
is_array($context['groups']) ? $context['groups'] : [$context['groups']]
)['is-translatable'] ?? false;
if ($isTranslatable) {
$data[$key] = $this->translatableStringHelper
->localize($value);
} elseif (is_iterable($value)) {
$arr = [];
foreach ($value as $k => $v) {
$arr[$k] =
$this->normalizer->normalize($v, $format, array_merge(
$context,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
}
$data[$key] = $arr;
} elseif (is_object($value)) {
$data[$key] =
$this->normalizer->normalize($value, $format, array_merge(
$context,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (null === $value) {
$data[$key] = $this->normalizeNullOutputValue($format, $context, $attribute, $reflection);
} else {
$data[$key] = $value;
}
}
return $data;
}
}