vendor/api-platform/core/src/Hydra/Serializer/DocumentationNormalizer.php line 56

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the API Platform project.
  4.  *
  5.  * (c) Kévin Dunglas <dunglas@gmail.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. declare(strict_types=1);
  11. namespace ApiPlatform\Hydra\Serializer;
  12. use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface;
  13. use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface;
  14. use ApiPlatform\Documentation\Documentation;
  15. use ApiPlatform\JsonLd\ContextBuilderInterface;
  16. use ApiPlatform\Metadata\ApiProperty;
  17. use ApiPlatform\Metadata\ApiResource;
  18. use ApiPlatform\Metadata\CollectionOperationInterface;
  19. use ApiPlatform\Metadata\ErrorResource;
  20. use ApiPlatform\Metadata\HttpOperation;
  21. use ApiPlatform\Metadata\Operation;
  22. use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  23. use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  24. use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
  25. use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
  26. use ApiPlatform\Metadata\ResourceClassResolverInterface;
  27. use ApiPlatform\Metadata\UrlGeneratorInterface;
  28. use ApiPlatform\Serializer\CacheableSupportsMethodInterface;
  29. use ApiPlatform\Symfony\Validator\Exception\ValidationException;
  30. use Symfony\Component\PropertyInfo\Type;
  31. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  32. use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
  33. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  34. use Symfony\Component\Serializer\Serializer;
  35. /**
  36.  * Creates a machine readable Hydra API documentation.
  37.  *
  38.  * @author Kévin Dunglas <dunglas@gmail.com>
  39.  */
  40. final class DocumentationNormalizer implements NormalizerInterfaceCacheableSupportsMethodInterface
  41. {
  42.     public const FORMAT 'jsonld';
  43.     public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly ?NameConverterInterface $nameConverter null)
  44.     {
  45.     }
  46.     /**
  47.      * {@inheritdoc}
  48.      */
  49.     public function normalize(mixed $object, ?string $format null, array $context = []): array|string|int|float|bool|\ArrayObject|null
  50.     {
  51.         $classes = [];
  52.         $entrypointProperties = [];
  53.         foreach ($object->getResourceNameCollection() as $resourceClass) {
  54.             $resourceMetadataCollection $this->resourceMetadataFactory->create($resourceClass);
  55.             $resourceMetadata $resourceMetadataCollection[0];
  56.             if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) {
  57.                 continue;
  58.             }
  59.             $shortName $resourceMetadata->getShortName();
  60.             $prefixedShortName $resourceMetadata->getTypes()[0] ?? "#$shortName";
  61.             $this->populateEntrypointProperties($resourceMetadata$shortName$prefixedShortName$entrypointProperties$resourceMetadataCollection);
  62.             $classes[] = $this->getClass($resourceClass$resourceMetadata$shortName$prefixedShortName$context$resourceMetadataCollection);
  63.         }
  64.         return $this->computeDoc($object$this->getClasses($entrypointProperties$classes));
  65.     }
  66.     /**
  67.      * Populates entrypoint properties.
  68.      */
  69.     private function populateEntrypointProperties(ApiResource $resourceMetadatastring $shortNamestring $prefixedShortName, array &$entrypointProperties, ?ResourceMetadataCollection $resourceMetadataCollection null): void
  70.     {
  71.         $hydraCollectionOperations $this->getHydraOperations(true$resourceMetadataCollection);
  72.         if (empty($hydraCollectionOperations)) {
  73.             return;
  74.         }
  75.         $entrypointProperty = [
  76.             '@type' => 'hydra:SupportedProperty',
  77.             'hydra:property' => [
  78.                 '@id' => sprintf('#Entrypoint/%s'lcfirst($shortName)),
  79.                 '@type' => 'hydra:Link',
  80.                 'domain' => '#Entrypoint',
  81.                 'rdfs:label' => "The collection of $shortName resources",
  82.                 'rdfs:range' => [
  83.                     ['@id' => 'hydra:Collection'],
  84.                     [
  85.                         'owl:equivalentClass' => [
  86.                             'owl:onProperty' => ['@id' => 'hydra:member'],
  87.                             'owl:allValuesFrom' => ['@id' => $prefixedShortName],
  88.                         ],
  89.                     ],
  90.                 ],
  91.                 'hydra:supportedOperation' => $hydraCollectionOperations,
  92.             ],
  93.             'hydra:title' => "The collection of $shortName resources",
  94.             'hydra:readable' => true,
  95.             'hydra:writeable' => false,
  96.         ];
  97.         if ($resourceMetadata->getDeprecationReason()) {
  98.             $entrypointProperty['owl:deprecated'] = true;
  99.         }
  100.         $entrypointProperties[] = $entrypointProperty;
  101.     }
  102.     /**
  103.      * Gets a Hydra class.
  104.      */
  105.     private function getClass(string $resourceClassApiResource $resourceMetadatastring $shortNamestring $prefixedShortName, array $context, ?ResourceMetadataCollection $resourceMetadataCollection null): array
  106.     {
  107.         $description $resourceMetadata->getDescription();
  108.         $isDeprecated $resourceMetadata->getDeprecationReason();
  109.         $class = [
  110.             '@id' => $prefixedShortName,
  111.             '@type' => 'hydra:Class',
  112.             'rdfs:label' => $shortName,
  113.             'hydra:title' => $shortName,
  114.             'hydra:supportedProperty' => $this->getHydraProperties($resourceClass$resourceMetadata$shortName$prefixedShortName$context),
  115.             'hydra:supportedOperation' => $this->getHydraOperations(false$resourceMetadataCollection),
  116.         ];
  117.         if (null !== $description) {
  118.             $class['hydra:description'] = $description;
  119.         }
  120.         if ($isDeprecated) {
  121.             $class['owl:deprecated'] = true;
  122.         }
  123.         return $class;
  124.     }
  125.     /**
  126.      * Creates context for property metatata factories.
  127.      */
  128.     private function getPropertyMetadataFactoryContext(ApiResource $resourceMetadata): array
  129.     {
  130.         $normalizationGroups $resourceMetadata->getNormalizationContext()[AbstractNormalizer::GROUPS] ?? null;
  131.         $denormalizationGroups $resourceMetadata->getDenormalizationContext()[AbstractNormalizer::GROUPS] ?? null;
  132.         $propertyContext = [
  133.             'normalization_groups' => $normalizationGroups,
  134.             'denormalization_groups' => $denormalizationGroups,
  135.         ];
  136.         $propertyNameContext = [];
  137.         if ($normalizationGroups) {
  138.             $propertyNameContext['serializer_groups'] = $normalizationGroups;
  139.         }
  140.         if (!$denormalizationGroups) {
  141.             return [$propertyNameContext$propertyContext];
  142.         }
  143.         if (!isset($propertyNameContext['serializer_groups'])) {
  144.             $propertyNameContext['serializer_groups'] = $denormalizationGroups;
  145.             return [$propertyNameContext$propertyContext];
  146.         }
  147.         foreach ($denormalizationGroups as $group) {
  148.             $propertyNameContext['serializer_groups'][] = $group;
  149.         }
  150.         return [$propertyNameContext$propertyContext];
  151.     }
  152.     /**
  153.      * Gets Hydra properties.
  154.      */
  155.     private function getHydraProperties(string $resourceClassApiResource $resourceMetadatastring $shortNamestring $prefixedShortName, array $context): array
  156.     {
  157.         $classes = [];
  158.         $classes[$resourceClass] = true;
  159.         foreach ($resourceMetadata->getOperations() as $operation) {
  160.             /** @var Operation $operation */
  161.             if (!$operation instanceof CollectionOperationInterface) {
  162.                 continue;
  163.             }
  164.             $inputMetadata $operation->getInput();
  165.             if (null !== $inputClass $inputMetadata['class'] ?? null) {
  166.                 $classes[$inputClass] = true;
  167.             }
  168.             $outputMetadata $operation->getOutput();
  169.             if (null !== $outputClass $outputMetadata['class'] ?? null) {
  170.                 $classes[$outputClass] = true;
  171.             }
  172.         }
  173.         /** @var string[] $classes */
  174.         $classes array_keys($classes);
  175.         $properties = [];
  176.         [$propertyNameContext$propertyContext] = $this->getPropertyMetadataFactoryContext($resourceMetadata);
  177.         foreach ($classes as $class) {
  178.             foreach ($this->propertyNameCollectionFactory->create($class$propertyNameContext) as $propertyName) {
  179.                 $propertyMetadata $this->propertyMetadataFactory->create($class$propertyName$propertyContext);
  180.                 if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
  181.                     continue;
  182.                 }
  183.                 if ($this->nameConverter) {
  184.                     $propertyName $this->nameConverter->normalize($propertyName$classself::FORMAT$context);
  185.                 }
  186.                 $properties[] = $this->getProperty($propertyMetadata$propertyName$prefixedShortName$shortName);
  187.             }
  188.         }
  189.         return $properties;
  190.     }
  191.     /**
  192.      * Gets Hydra operations.
  193.      */
  194.     private function getHydraOperations(bool $collection, ?ResourceMetadataCollection $resourceMetadataCollection null): array
  195.     {
  196.         $hydraOperations = [];
  197.         foreach ($resourceMetadataCollection as $resourceMetadata) {
  198.             foreach ($resourceMetadata->getOperations() as $operation) {
  199.                 if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
  200.                     continue;
  201.                 }
  202.                 $hydraOperations[] = $this->getHydraOperation($operation$operation->getTypes()[0] ?? "#{$operation->getShortName()}");
  203.             }
  204.         }
  205.         return $hydraOperations;
  206.     }
  207.     /**
  208.      * Gets and populates if applicable a Hydra operation.
  209.      */
  210.     private function getHydraOperation(HttpOperation $operationstring $prefixedShortName): array
  211.     {
  212.         $method $operation->getMethod() ?: 'GET';
  213.         $hydraOperation $operation->getHydraContext() ?? [];
  214.         if ($operation->getDeprecationReason()) {
  215.             $hydraOperation['owl:deprecated'] = true;
  216.         }
  217.         $shortName $operation->getShortName();
  218.         $inputMetadata $operation->getInput() ?? [];
  219.         $outputMetadata $operation->getOutput() ?? [];
  220.         $inputClass \array_key_exists('class'$inputMetadata) ? $inputMetadata['class'] : false;
  221.         $outputClass \array_key_exists('class'$outputMetadata) ? $outputMetadata['class'] : false;
  222.         if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
  223.             $hydraOperation += [
  224.                 '@type' => ['hydra:Operation''schema:FindAction'],
  225.                 'hydra:title' => "Retrieves the collection of $shortName resources.",
  226.                 'returns' => null === $outputClass 'owl:Nothing' 'hydra:Collection',
  227.             ];
  228.         } elseif ('GET' === $method) {
  229.             $hydraOperation += [
  230.                 '@type' => ['hydra:Operation''schema:FindAction'],
  231.                 'hydra:title' => "Retrieves a $shortName resource.",
  232.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  233.             ];
  234.         } elseif ('PATCH' === $method) {
  235.             $hydraOperation += [
  236.                 '@type' => 'hydra:Operation',
  237.                 'hydra:title' => "Updates the $shortName resource.",
  238.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  239.                 'expects' => null === $inputClass 'owl:Nothing' $prefixedShortName,
  240.             ];
  241.         } elseif ('POST' === $method) {
  242.             $hydraOperation += [
  243.                 '@type' => ['hydra:Operation''schema:CreateAction'],
  244.                 'hydra:title' => "Creates a $shortName resource.",
  245.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  246.                 'expects' => null === $inputClass 'owl:Nothing' $prefixedShortName,
  247.             ];
  248.         } elseif ('PUT' === $method) {
  249.             $hydraOperation += [
  250.                 '@type' => ['hydra:Operation''schema:ReplaceAction'],
  251.                 'hydra:title' => "Replaces the $shortName resource.",
  252.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  253.                 'expects' => null === $inputClass 'owl:Nothing' $prefixedShortName,
  254.             ];
  255.         } elseif ('DELETE' === $method) {
  256.             $hydraOperation += [
  257.                 '@type' => ['hydra:Operation''schema:DeleteAction'],
  258.                 'hydra:title' => "Deletes the $shortName resource.",
  259.                 'returns' => 'owl:Nothing',
  260.             ];
  261.         }
  262.         $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method;
  263.         if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) {
  264.             $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title'];
  265.         }
  266.         ksort($hydraOperation);
  267.         return $hydraOperation;
  268.     }
  269.     /**
  270.      * Gets the range of the property.
  271.      */
  272.     private function getRange(ApiProperty $propertyMetadata): array|string|null
  273.     {
  274.         $jsonldContext $propertyMetadata->getJsonldContext();
  275.         if (isset($jsonldContext['@type'])) {
  276.             return $jsonldContext['@type'];
  277.         }
  278.         $builtInTypes $propertyMetadata->getBuiltinTypes() ?? [];
  279.         $types = [];
  280.         foreach ($builtInTypes as $type) {
  281.             if ($type->isCollection() && null !== $collectionType $type->getCollectionValueTypes()[0] ?? null) {
  282.                 $type $collectionType;
  283.             }
  284.             switch ($type->getBuiltinType()) {
  285.                 case Type::BUILTIN_TYPE_STRING:
  286.                     if (!\in_array('xmls:string'$typestrue)) {
  287.                         $types[] = 'xmls:string';
  288.                     }
  289.                     break;
  290.                 case Type::BUILTIN_TYPE_INT:
  291.                     if (!\in_array('xmls:integer'$typestrue)) {
  292.                         $types[] = 'xmls:integer';
  293.                     }
  294.                     break;
  295.                 case Type::BUILTIN_TYPE_FLOAT:
  296.                     if (!\in_array('xmls:decimal'$typestrue)) {
  297.                         $types[] = 'xmls:decimal';
  298.                     }
  299.                     break;
  300.                 case Type::BUILTIN_TYPE_BOOL:
  301.                     if (!\in_array('xmls:boolean'$typestrue)) {
  302.                         $types[] = 'xmls:boolean';
  303.                     }
  304.                     break;
  305.                 case Type::BUILTIN_TYPE_OBJECT:
  306.                     if (null === $className $type->getClassName()) {
  307.                         continue 2;
  308.                     }
  309.                     if (is_a($className\DateTimeInterface::class, true)) {
  310.                         if (!\in_array('xmls:dateTime'$typestrue)) {
  311.                             $types[] = 'xmls:dateTime';
  312.                         }
  313.                         break;
  314.                     }
  315.                     if ($this->resourceClassResolver->isResourceClass($className)) {
  316.                         $resourceMetadata $this->resourceMetadataFactory->create($className);
  317.                         $operation $resourceMetadata->getOperation();
  318.                         if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
  319.                             if (!\in_array("#{$operation->getShortName()}"$typestrue)) {
  320.                                 $types[] = "#{$operation->getShortName()}";
  321.                             }
  322.                             break;
  323.                         }
  324.                         $types array_unique(array_merge($types$operation->getTypes()));
  325.                         break;
  326.                     }
  327.             }
  328.         }
  329.         if ([] === $types) {
  330.             return null;
  331.         }
  332.         return === \count($types) ? $types[0] : $types;
  333.     }
  334.     private function isSingleRelation(ApiProperty $propertyMetadata): bool
  335.     {
  336.         $builtInTypes $propertyMetadata->getBuiltinTypes() ?? [];
  337.         foreach ($builtInTypes as $type) {
  338.             $className $type->getClassName();
  339.             if (!$type->isCollection()
  340.                 && null !== $className
  341.                 && $this->resourceClassResolver->isResourceClass($className)
  342.             ) {
  343.                 return true;
  344.             }
  345.         }
  346.         return false;
  347.     }
  348.     /**
  349.      * Builds the classes array.
  350.      */
  351.     private function getClasses(array $entrypointProperties, array $classes): array
  352.     {
  353.         $classes[] = [
  354.             '@id' => '#Entrypoint',
  355.             '@type' => 'hydra:Class',
  356.             'hydra:title' => 'The API entrypoint',
  357.             'hydra:supportedProperty' => $entrypointProperties,
  358.             'hydra:supportedOperation' => [
  359.                 '@type' => 'hydra:Operation',
  360.                 'hydra:method' => 'GET',
  361.                 'rdfs:label' => 'The API entrypoint.',
  362.                 'returns' => '#EntryPoint',
  363.             ],
  364.         ];
  365.         // Constraint violation
  366.         $classes[] = [
  367.             '@id' => '#ConstraintViolation',
  368.             '@type' => 'hydra:Class',
  369.             'hydra:title' => 'A constraint violation',
  370.             'hydra:supportedProperty' => [
  371.                 [
  372.                     '@type' => 'hydra:SupportedProperty',
  373.                     'hydra:property' => [
  374.                         '@id' => '#ConstraintViolation/propertyPath',
  375.                         '@type' => 'rdf:Property',
  376.                         'rdfs:label' => 'propertyPath',
  377.                         'domain' => '#ConstraintViolation',
  378.                         'range' => 'xmls:string',
  379.                     ],
  380.                     'hydra:title' => 'propertyPath',
  381.                     'hydra:description' => 'The property path of the violation',
  382.                     'hydra:readable' => true,
  383.                     'hydra:writeable' => false,
  384.                 ],
  385.                 [
  386.                     '@type' => 'hydra:SupportedProperty',
  387.                     'hydra:property' => [
  388.                         '@id' => '#ConstraintViolation/message',
  389.                         '@type' => 'rdf:Property',
  390.                         'rdfs:label' => 'message',
  391.                         'domain' => '#ConstraintViolation',
  392.                         'range' => 'xmls:string',
  393.                     ],
  394.                     'hydra:title' => 'message',
  395.                     'hydra:description' => 'The message associated with the violation',
  396.                     'hydra:readable' => true,
  397.                     'hydra:writeable' => false,
  398.                 ],
  399.             ],
  400.         ];
  401.         // Constraint violation list
  402.         $classes[] = [
  403.             '@id' => '#ConstraintViolationList',
  404.             '@type' => 'hydra:Class',
  405.             'subClassOf' => 'hydra:Error',
  406.             'hydra:title' => 'A constraint violation list',
  407.             'hydra:supportedProperty' => [
  408.                 [
  409.                     '@type' => 'hydra:SupportedProperty',
  410.                     'hydra:property' => [
  411.                         '@id' => '#ConstraintViolationList/violations',
  412.                         '@type' => 'rdf:Property',
  413.                         'rdfs:label' => 'violations',
  414.                         'domain' => '#ConstraintViolationList',
  415.                         'range' => '#ConstraintViolation',
  416.                     ],
  417.                     'hydra:title' => 'violations',
  418.                     'hydra:description' => 'The violations',
  419.                     'hydra:readable' => true,
  420.                     'hydra:writeable' => false,
  421.                 ],
  422.             ],
  423.         ];
  424.         return $classes;
  425.     }
  426.     /**
  427.      * Gets a property definition.
  428.      */
  429.     private function getProperty(ApiProperty $propertyMetadatastring $propertyNamestring $prefixedShortNamestring $shortName): array
  430.     {
  431.         if ($iri $propertyMetadata->getIris()) {
  432.             $iri === (is_countable($iri) ? \count($iri) : 0) ? $iri[0] : $iri;
  433.         }
  434.         if (!isset($iri)) {
  435.             $iri "#$shortName/$propertyName";
  436.         }
  437.         $propertyData = [
  438.             '@id' => $iri,
  439.             '@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' 'rdf:Property',
  440.             'rdfs:label' => $propertyName,
  441.             'domain' => $prefixedShortName,
  442.         ];
  443.         if ($propertyMetadata->getDeprecationReason()) {
  444.             $propertyData['owl:deprecated'] = true;
  445.         }
  446.         if ($this->isSingleRelation($propertyMetadata)) {
  447.             $propertyData['owl:maxCardinality'] = 1;
  448.         }
  449.         if (null !== $range $this->getRange($propertyMetadata)) {
  450.             $propertyData['range'] = $range;
  451.         }
  452.         $property = [
  453.             '@type' => 'hydra:SupportedProperty',
  454.             'hydra:property' => $propertyData,
  455.             'hydra:title' => $propertyName,
  456.             'hydra:required' => $propertyMetadata->isRequired(),
  457.             'hydra:readable' => $propertyMetadata->isReadable(),
  458.             'hydra:writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
  459.         ];
  460.         if (null !== $description $propertyMetadata->getDescription()) {
  461.             $property['hydra:description'] = $description;
  462.         }
  463.         return $property;
  464.     }
  465.     /**
  466.      * Computes the documentation.
  467.      */
  468.     private function computeDoc(Documentation $object, array $classes): array
  469.     {
  470.         $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation'];
  471.         if ('' !== $object->getTitle()) {
  472.             $doc['hydra:title'] = $object->getTitle();
  473.         }
  474.         if ('' !== $object->getDescription()) {
  475.             $doc['hydra:description'] = $object->getDescription();
  476.         }
  477.         $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
  478.         $doc['hydra:supportedClass'] = $classes;
  479.         return $doc;
  480.     }
  481.     /**
  482.      * Builds the JSON-LD context for the API documentation.
  483.      */
  484.     private function getContext(): array
  485.     {
  486.         return [
  487.             '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
  488.             'hydra' => ContextBuilderInterface::HYDRA_NS,
  489.             'rdf' => ContextBuilderInterface::RDF_NS,
  490.             'rdfs' => ContextBuilderInterface::RDFS_NS,
  491.             'xmls' => ContextBuilderInterface::XML_NS,
  492.             'owl' => ContextBuilderInterface::OWL_NS,
  493.             'schema' => ContextBuilderInterface::SCHEMA_ORG_NS,
  494.             'domain' => ['@id' => 'rdfs:domain''@type' => '@id'],
  495.             'range' => ['@id' => 'rdfs:range''@type' => '@id'],
  496.             'subClassOf' => ['@id' => 'rdfs:subClassOf''@type' => '@id'],
  497.             'expects' => ['@id' => 'hydra:expects''@type' => '@id'],
  498.             'returns' => ['@id' => 'hydra:returns''@type' => '@id'],
  499.         ];
  500.     }
  501.     /**
  502.      * {@inheritdoc}
  503.      */
  504.     public function supportsNormalization(mixed $data, ?string $format null, array $context = []): bool
  505.     {
  506.         return self::FORMAT === $format && $data instanceof Documentation;
  507.     }
  508.     public function getSupportedTypes($format): array
  509.     {
  510.         return self::FORMAT === $format ? [Documentation::class => true] : [];
  511.     }
  512.     public function hasCacheableSupportsMethod(): bool
  513.     {
  514.         if (method_exists(Serializer::class, 'getSupportedTypes')) {
  515.             trigger_deprecation(
  516.                 'api-platform/core',
  517.                 '3.1',
  518.                 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.',
  519.                 __METHOD__
  520.             );
  521.         }
  522.         return true;
  523.     }
  524. }