vendor/symfony/serializer/Mapping/Loader/AttributeLoader.php line 51

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Serializer\Mapping\Loader;
  11. use Doctrine\Common\Annotations\Reader;
  12. use Symfony\Component\Serializer\Attribute\Context;
  13. use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
  14. use Symfony\Component\Serializer\Attribute\Groups;
  15. use Symfony\Component\Serializer\Attribute\Ignore;
  16. use Symfony\Component\Serializer\Attribute\MaxDepth;
  17. use Symfony\Component\Serializer\Attribute\SerializedName;
  18. use Symfony\Component\Serializer\Attribute\SerializedPath;
  19. use Symfony\Component\Serializer\Exception\MappingException;
  20. use Symfony\Component\Serializer\Mapping\AttributeMetadata;
  21. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  22. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
  23. use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
  24. /**
  25. * Loader for PHP attributes.
  26. *
  27. * @author Kévin Dunglas <dunglas@gmail.com>
  28. * @author Alexander M. Turek <me@derrabus.de>
  29. * @author Alexandre Daubois <alex.daubois@gmail.com>
  30. */
  31. class AttributeLoader implements LoaderInterface
  32. {
  33. private const KNOWN_ATTRIBUTES = [
  34. DiscriminatorMap::class,
  35. Groups::class,
  36. Ignore::class,
  37. MaxDepth::class,
  38. SerializedName::class,
  39. SerializedPath::class,
  40. Context::class,
  41. ];
  42. public function __construct(
  43. private readonly ?Reader $reader = null,
  44. ) {
  45. if ($reader) {
  46. trigger_deprecation('symfony/serializer', '6.4', 'Passing a "%s" instance as argument 1 to "%s()" is deprecated, pass null or omit the parameter instead.', get_debug_type($reader), __METHOD__);
  47. }
  48. }
  49. public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
  50. {
  51. $reflectionClass = $classMetadata->getReflectionClass();
  52. $className = $reflectionClass->name;
  53. $loaded = false;
  54. $classGroups = [];
  55. $classContextAnnotation = null;
  56. $attributesMetadata = $classMetadata->getAttributesMetadata();
  57. foreach ($this->loadAttributes($reflectionClass) as $annotation) {
  58. if ($annotation instanceof DiscriminatorMap) {
  59. $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
  60. $annotation->getTypeProperty(),
  61. $annotation->getMapping()
  62. ));
  63. continue;
  64. }
  65. if ($annotation instanceof Groups) {
  66. $classGroups = $annotation->getGroups();
  67. continue;
  68. }
  69. if ($annotation instanceof Context) {
  70. $classContextAnnotation = $annotation;
  71. }
  72. }
  73. foreach ($reflectionClass->getProperties() as $property) {
  74. if (!isset($attributesMetadata[$property->name])) {
  75. $attributesMetadata[$property->name] = new AttributeMetadata($property->name);
  76. $classMetadata->addAttributeMetadata($attributesMetadata[$property->name]);
  77. }
  78. if ($property->getDeclaringClass()->name === $className) {
  79. if ($classContextAnnotation) {
  80. $this->setAttributeContextsForGroups($classContextAnnotation, $attributesMetadata[$property->name]);
  81. }
  82. foreach ($classGroups as $group) {
  83. $attributesMetadata[$property->name]->addGroup($group);
  84. }
  85. foreach ($this->loadAttributes($property) as $annotation) {
  86. if ($annotation instanceof Groups) {
  87. foreach ($annotation->getGroups() as $group) {
  88. $attributesMetadata[$property->name]->addGroup($group);
  89. }
  90. } elseif ($annotation instanceof MaxDepth) {
  91. $attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
  92. } elseif ($annotation instanceof SerializedName) {
  93. $attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
  94. } elseif ($annotation instanceof SerializedPath) {
  95. $attributesMetadata[$property->name]->setSerializedPath($annotation->getSerializedPath());
  96. } elseif ($annotation instanceof Ignore) {
  97. $attributesMetadata[$property->name]->setIgnore(true);
  98. } elseif ($annotation instanceof Context) {
  99. $this->setAttributeContextsForGroups($annotation, $attributesMetadata[$property->name]);
  100. }
  101. $loaded = true;
  102. }
  103. }
  104. }
  105. foreach ($reflectionClass->getMethods() as $method) {
  106. if ($method->getDeclaringClass()->name !== $className) {
  107. continue;
  108. }
  109. if (0 === stripos($method->name, 'get') && $method->getNumberOfRequiredParameters()) {
  110. continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */
  111. }
  112. $accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches);
  113. if ($accessorOrMutator && !ctype_lower($matches[2][0])) {
  114. $attributeName = lcfirst($matches[2]);
  115. if (isset($attributesMetadata[$attributeName])) {
  116. $attributeMetadata = $attributesMetadata[$attributeName];
  117. } else {
  118. $attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName);
  119. $classMetadata->addAttributeMetadata($attributeMetadata);
  120. }
  121. }
  122. foreach ($this->loadAttributes($method) as $annotation) {
  123. if ($annotation instanceof Groups) {
  124. if (!$accessorOrMutator) {
  125. throw new MappingException(\sprintf('Groups on "%s::%s()" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
  126. }
  127. foreach ($annotation->getGroups() as $group) {
  128. $attributeMetadata->addGroup($group);
  129. }
  130. } elseif ($annotation instanceof MaxDepth) {
  131. if (!$accessorOrMutator) {
  132. throw new MappingException(\sprintf('MaxDepth on "%s::%s()" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
  133. }
  134. $attributeMetadata->setMaxDepth($annotation->getMaxDepth());
  135. } elseif ($annotation instanceof SerializedName) {
  136. if (!$accessorOrMutator) {
  137. throw new MappingException(\sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
  138. }
  139. $attributeMetadata->setSerializedName($annotation->getSerializedName());
  140. } elseif ($annotation instanceof SerializedPath) {
  141. if (!$accessorOrMutator) {
  142. throw new MappingException(\sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
  143. }
  144. $attributeMetadata->setSerializedPath($annotation->getSerializedPath());
  145. } elseif ($annotation instanceof Ignore) {
  146. if ($accessorOrMutator) {
  147. $attributeMetadata->setIgnore(true);
  148. }
  149. } elseif ($annotation instanceof Context) {
  150. if (!$accessorOrMutator) {
  151. throw new MappingException(\sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
  152. }
  153. $this->setAttributeContextsForGroups($annotation, $attributeMetadata);
  154. }
  155. $loaded = true;
  156. }
  157. }
  158. return $loaded;
  159. }
  160. private function loadAttributes(\ReflectionMethod|\ReflectionClass|\ReflectionProperty $reflector): iterable
  161. {
  162. foreach ($reflector->getAttributes() as $attribute) {
  163. if ($this->isKnownAttribute($attribute->getName())) {
  164. try {
  165. yield $attribute->newInstance();
  166. } catch (\Error $e) {
  167. if (\Error::class !== $e::class) {
  168. throw $e;
  169. }
  170. $on = match (true) {
  171. $reflector instanceof \ReflectionClass => ' on class '.$reflector->name,
  172. $reflector instanceof \ReflectionMethod => \sprintf(' on "%s::%s()"', $reflector->getDeclaringClass()->name, $reflector->name),
  173. $reflector instanceof \ReflectionProperty => \sprintf(' on "%s::$%s"', $reflector->getDeclaringClass()->name, $reflector->name),
  174. default => '',
  175. };
  176. throw new MappingException(\sprintf('Could not instantiate attribute "%s"%s.', $attribute->getName(), $on), 0, $e);
  177. }
  178. }
  179. }
  180. if (null === $this->reader) {
  181. return;
  182. }
  183. if ($reflector instanceof \ReflectionClass) {
  184. yield from $this->getClassAnnotations($reflector);
  185. }
  186. if ($reflector instanceof \ReflectionMethod) {
  187. yield from $this->getMethodAnnotations($reflector);
  188. }
  189. if ($reflector instanceof \ReflectionProperty) {
  190. yield from $this->getPropertyAnnotations($reflector);
  191. }
  192. }
  193. /**
  194. * @deprecated since Symfony 6.4 without replacement
  195. */
  196. public function loadAnnotations(\ReflectionMethod|\ReflectionClass|\ReflectionProperty $reflector): iterable
  197. {
  198. trigger_deprecation('symfony/serializer', '6.4', 'Method "%s()" is deprecated without replacement.', __METHOD__);
  199. return $this->loadAttributes($reflector);
  200. }
  201. private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void
  202. {
  203. if ($annotation->getContext()) {
  204. $attributeMetadata->setNormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
  205. $attributeMetadata->setDenormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
  206. }
  207. if ($annotation->getNormalizationContext()) {
  208. $attributeMetadata->setNormalizationContextForGroups($annotation->getNormalizationContext(), $annotation->getGroups());
  209. }
  210. if ($annotation->getDenormalizationContext()) {
  211. $attributeMetadata->setDenormalizationContextForGroups($annotation->getDenormalizationContext(), $annotation->getGroups());
  212. }
  213. }
  214. private function isKnownAttribute(string $attributeName): bool
  215. {
  216. foreach (self::KNOWN_ATTRIBUTES as $knownAttribute) {
  217. if (is_a($attributeName, $knownAttribute, true)) {
  218. return true;
  219. }
  220. }
  221. return false;
  222. }
  223. /**
  224. * @return object[]
  225. */
  226. private function getClassAnnotations(\ReflectionClass $reflector): array
  227. {
  228. if ($annotations = array_filter(
  229. $this->reader->getClassAnnotations($reflector),
  230. fn (object $annotation): bool => $this->isKnownAttribute($annotation::class),
  231. )) {
  232. trigger_deprecation('symfony/serializer', '6.4', 'Class "%s" uses Doctrine Annotations to configure serialization, which is deprecated. Use PHP attributes instead.', $reflector->getName());
  233. }
  234. return $annotations;
  235. }
  236. /**
  237. * @return object[]
  238. */
  239. private function getMethodAnnotations(\ReflectionMethod $reflector): array
  240. {
  241. if ($annotations = array_filter(
  242. $this->reader->getMethodAnnotations($reflector),
  243. fn (object $annotation): bool => $this->isKnownAttribute($annotation::class),
  244. )) {
  245. trigger_deprecation('symfony/serializer', '6.4', 'Method "%s::%s()" uses Doctrine Annotations to configure serialization, which is deprecated. Use PHP attributes instead.', $reflector->getDeclaringClass()->getName(), $reflector->getName());
  246. }
  247. return $annotations;
  248. }
  249. /**
  250. * @return object[]
  251. */
  252. private function getPropertyAnnotations(\ReflectionProperty $reflector): array
  253. {
  254. if ($annotations = array_filter(
  255. $this->reader->getPropertyAnnotations($reflector),
  256. fn (object $annotation): bool => $this->isKnownAttribute($annotation::class),
  257. )) {
  258. trigger_deprecation('symfony/serializer', '6.4', 'Property "%s::$%s" uses Doctrine Annotations to configure serialization, which is deprecated. Use PHP attributes instead.', $reflector->getDeclaringClass()->getName(), $reflector->getName());
  259. }
  260. return $annotations;
  261. }
  262. }
  263. if (!class_exists(AnnotationLoader::class, false)) {
  264. class_alias(AttributeLoader::class, AnnotationLoader::class);
  265. }