classMetadataFactory = $classMetadataFactory; $this->propertyAccess = PropertyAccess::createPropertyAccessor(); } public function normalize($object, ?string $format = null, array $context = []) { $classMetadataKey = $object ?? $context['docgen:expects']; 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) : $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. Add a type on this property', $attribute->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. Add a return type on the method', $attribute->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. Add a return type on the method', $attribute->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. Add a return type on the method', $attribute->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 = []; foreach ($attributes as $attribute) { $key = $attribute->getSerializedName() ?? $attribute->getName(); $keys[$key] = $this->getExpectedType($attribute, $metadata->getReflectionClass()); } $normalizer = new NormalizeNullValueHelper($this->normalizer); 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': 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 = []; $reflection = $metadata->getReflectionClass(); foreach ($attributes as $attribute) { /** @var AttributeMetadata $attribute */ $value = $this->propertyAccess->getValue($object, $attribute->getName()); $key = $attribute->getSerializedName() ?? $attribute->getName(); if (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; } }