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