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

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;
  13. use ApiPlatform\Api\UrlGeneratorInterface;
  14. use ApiPlatform\Core\Api\OperationMethodResolverInterface;
  15. use ApiPlatform\Core\Api\OperationType;
  16. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface as LegacyPropertyMetadataFactoryInterface;
  17. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  18. use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
  19. use ApiPlatform\Core\Metadata\Property\SubresourceMetadata;
  20. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  21. use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
  22. use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
  23. use ApiPlatform\Documentation\Documentation;
  24. use ApiPlatform\JsonLd\ContextBuilderInterface;
  25. use ApiPlatform\Metadata\ApiProperty;
  26. use ApiPlatform\Metadata\ApiResource;
  27. use ApiPlatform\Metadata\CollectionOperationInterface;
  28. use ApiPlatform\Metadata\HttpOperation;
  29. use ApiPlatform\Metadata\Operation;
  30. use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  31. use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
  32. use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
  33. use Symfony\Component\PropertyInfo\Type;
  34. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  35. use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
  36. use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
  37. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  38. /**
  39.  * Creates a machine readable Hydra API documentation.
  40.  *
  41.  * @author Kévin Dunglas <dunglas@gmail.com>
  42.  */
  43. final class DocumentationNormalizer implements NormalizerInterfaceCacheableSupportsMethodInterface
  44. {
  45.     public const FORMAT 'jsonld';
  46.     /**
  47.      * @var ResourceMetadataFactoryInterface|ResourceMetadataCollectionFactoryInterface
  48.      */
  49.     private $resourceMetadataFactory;
  50.     private $propertyNameCollectionFactory;
  51.     /**
  52.      * @var PropertyMetadataFactoryInterface|LegacyPropertyMetadataFactoryInterface
  53.      */
  54.     private $propertyMetadataFactory;
  55.     private $resourceClassResolver;
  56.     private $operationMethodResolver;
  57.     private $urlGenerator;
  58.     private $subresourceOperationFactory;
  59.     private $nameConverter;
  60.     public function __construct($resourceMetadataFactoryPropertyNameCollectionFactoryInterface $propertyNameCollectionFactory$propertyMetadataFactoryResourceClassResolverInterface $resourceClassResolverOperationMethodResolverInterface $operationMethodResolver nullUrlGeneratorInterface $urlGeneratorSubresourceOperationFactoryInterface $subresourceOperationFactory nullNameConverterInterface $nameConverter null)
  61.     {
  62.         if ($operationMethodResolver) {
  63.             @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.'OperationMethodResolverInterface::class, __METHOD__), \E_USER_DEPRECATED);
  64.         }
  65.         $this->resourceMetadataFactory $resourceMetadataFactory;
  66.         if (!$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  67.             trigger_deprecation('api-platform/core''2.7'sprintf('Use "%s" instead of "%s".'ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class));
  68.         }
  69.         if ($subresourceOperationFactory) {
  70.             trigger_deprecation('api-platform/core''2.7'sprintf('Using "%s" is deprecated and will be removed.'SubresourceOperationFactoryInterface::class));
  71.         }
  72.         $this->propertyNameCollectionFactory $propertyNameCollectionFactory;
  73.         $this->propertyMetadataFactory $propertyMetadataFactory;
  74.         $this->resourceClassResolver $resourceClassResolver;
  75.         $this->operationMethodResolver $operationMethodResolver;
  76.         $this->urlGenerator $urlGenerator;
  77.         $this->subresourceOperationFactory $subresourceOperationFactory;
  78.         $this->nameConverter $nameConverter;
  79.     }
  80.     /**
  81.      * {@inheritdoc}
  82.      *
  83.      * @return array|string|int|float|bool|\ArrayObject|null
  84.      */
  85.     public function normalize($object$format null, array $context = [])
  86.     {
  87.         $classes = [];
  88.         $entrypointProperties = [];
  89.         foreach ($object->getResourceNameCollection() as $resourceClass) {
  90.             $resourceMetadataCollection $this->resourceMetadataFactory->create($resourceClass);
  91.             if ($resourceMetadataCollection instanceof ResourceMetadata) {
  92.                 $shortName $resourceMetadataCollection->getShortName();
  93.                 $prefixedShortName $resourceMetadataCollection->getIri() ?? "#$shortName";
  94.                 $this->populateEntrypointProperties($resourceClass$resourceMetadataCollection$shortName$prefixedShortName$entrypointProperties);
  95.                 $classes[] = $this->getClass($resourceClass$resourceMetadataCollection$shortName$prefixedShortName$context);
  96.                 continue;
  97.             }
  98.             $resourceMetadata $resourceMetadataCollection[0];
  99.             $shortName $resourceMetadata->getShortName();
  100.             $prefixedShortName $resourceMetadata->getTypes()[0] ?? "#$shortName";
  101.             $this->populateEntrypointProperties($resourceClass$resourceMetadata$shortName$prefixedShortName$entrypointProperties$resourceMetadataCollection);
  102.             $classes[] = $this->getClass($resourceClass$resourceMetadata$shortName$prefixedShortName$context$resourceMetadataCollection);
  103.         }
  104.         return $this->computeDoc($object$this->getClasses($entrypointProperties$classes));
  105.     }
  106.     /**
  107.      * Populates entrypoint properties.
  108.      *
  109.      * @param ResourceMetadata|ApiResource $resourceMetadata
  110.      */
  111.     private function populateEntrypointProperties(string $resourceClass$resourceMetadatastring $shortNamestring $prefixedShortName, array &$entrypointProperties, ?ResourceMetadataCollection $resourceMetadataCollection null)
  112.     {
  113.         $hydraCollectionOperations $this->getHydraOperations($resourceClass$resourceMetadata$prefixedShortNametrue$resourceMetadataCollection);
  114.         if (empty($hydraCollectionOperations)) {
  115.             return;
  116.         }
  117.         $entrypointProperty = [
  118.             '@type' => 'hydra:SupportedProperty',
  119.             'hydra:property' => [
  120.                 '@id' => sprintf('#Entrypoint/%s'lcfirst($shortName)),
  121.                 '@type' => 'hydra:Link',
  122.                 'domain' => '#Entrypoint',
  123.                 'rdfs:label' => "The collection of $shortName resources",
  124.                 'rdfs:range' => [
  125.                     ['@id' => 'hydra:Collection'],
  126.                     [
  127.                         'owl:equivalentClass' => [
  128.                             'owl:onProperty' => ['@id' => 'hydra:member'],
  129.                             'owl:allValuesFrom' => ['@id' => $prefixedShortName],
  130.                         ],
  131.                     ],
  132.                 ],
  133.                 'hydra:supportedOperation' => $hydraCollectionOperations,
  134.             ],
  135.             'hydra:title' => "The collection of $shortName resources",
  136.             'hydra:readable' => true,
  137.             'hydra:writeable' => false,
  138.         ];
  139.         if ($resourceMetadata instanceof ResourceMetadata $resourceMetadata->getCollectionOperationAttribute('GET''deprecation_reason'nulltrue) : $resourceMetadata->getDeprecationReason()) {
  140.             $entrypointProperty['owl:deprecated'] = true;
  141.         }
  142.         $entrypointProperties[] = $entrypointProperty;
  143.     }
  144.     /**
  145.      * Gets a Hydra class.
  146.      *
  147.      * @param ResourceMetadata|ApiResource $resourceMetadata
  148.      */
  149.     private function getClass(string $resourceClass$resourceMetadatastring $shortNamestring $prefixedShortName, array $context, ?ResourceMetadataCollection $resourceMetadataCollection null): array
  150.     {
  151.         if ($resourceMetadata instanceof ApiResource) {
  152.             $description $resourceMetadata->getDescription();
  153.             $isDeprecated $resourceMetadata->getDeprecationReason();
  154.         } else {
  155.             $description $resourceMetadata->getDescription();
  156.             $isDeprecated $resourceMetadata->getAttribute('deprecation_reason');
  157.         }
  158.         $class = [
  159.             '@id' => $prefixedShortName,
  160.             '@type' => 'hydra:Class',
  161.             'rdfs:label' => $shortName,
  162.             'hydra:title' => $shortName,
  163.             'hydra:supportedProperty' => $this->getHydraProperties($resourceClass$resourceMetadata$shortName$prefixedShortName$context),
  164.             'hydra:supportedOperation' => $this->getHydraOperations($resourceClass$resourceMetadata$prefixedShortNamefalse$resourceMetadataCollection),
  165.         ];
  166.         if (null !== $description) {
  167.             $class['hydra:description'] = $description;
  168.         }
  169.         if ($isDeprecated) {
  170.             $class['owl:deprecated'] = true;
  171.         }
  172.         return $class;
  173.     }
  174.     /**
  175.      * Gets the context for the property name factory.
  176.      */
  177.     private function getPropertyNameCollectionFactoryContext(ResourceMetadata $resourceMetadata): array
  178.     {
  179.         $attributes $resourceMetadata->getAttributes();
  180.         $context = [];
  181.         if (isset($attributes['normalization_context'][AbstractNormalizer::GROUPS])) {
  182.             $context['serializer_groups'] = (array) $attributes['normalization_context'][AbstractNormalizer::GROUPS];
  183.         }
  184.         if (!isset($attributes['denormalization_context'][AbstractNormalizer::GROUPS])) {
  185.             return $context;
  186.         }
  187.         if (isset($context['serializer_groups'])) {
  188.             foreach ((array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS] as $groupName) {
  189.                 $context['serializer_groups'][] = $groupName;
  190.             }
  191.             return $context;
  192.         }
  193.         $context['serializer_groups'] = (array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS];
  194.         return $context;
  195.     }
  196.     /**
  197.      * Creates context for property metatata factories.
  198.      */
  199.     private function getPropertyMetadataFactoryContext(ApiResource $resourceMetadata): array
  200.     {
  201.         $normalizationGroups $resourceMetadata->getNormalizationContext()[AbstractNormalizer::GROUPS] ?? null;
  202.         $denormalizationGroups $resourceMetadata->getDenormalizationContext()[AbstractNormalizer::GROUPS] ?? null;
  203.         $propertyContext = [
  204.             'normalization_groups' => $normalizationGroups,
  205.             'denormalization_groups' => $denormalizationGroups,
  206.         ];
  207.         $propertyNameContext = [];
  208.         if ($normalizationGroups) {
  209.             $propertyNameContext['serializer_groups'] = $normalizationGroups;
  210.         }
  211.         if (!$denormalizationGroups) {
  212.             return [$propertyNameContext$propertyContext];
  213.         }
  214.         if (!isset($propertyNameContext['serializer_groups'])) {
  215.             $propertyNameContext['serializer_groups'] = $denormalizationGroups;
  216.             return [$propertyNameContext$propertyContext];
  217.         }
  218.         foreach ($denormalizationGroups as $group) {
  219.             $propertyNameContext['serializer_groups'][] = $group;
  220.         }
  221.         return [$propertyNameContext$propertyContext];
  222.     }
  223.     /**
  224.      * Gets Hydra properties.
  225.      *
  226.      * @param ResourceMetadata|ApiResource $resourceMetadata
  227.      */
  228.     private function getHydraProperties(string $resourceClass$resourceMetadatastring $shortNamestring $prefixedShortName, array $context): array
  229.     {
  230.         $classes = [];
  231.         if ($resourceMetadata instanceof ResourceMetadata) {
  232.             foreach ($resourceMetadata->getCollectionOperations() as $operationName => $operation) {
  233.                 $inputMetadata $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION$operationName'input', ['class' => $resourceClass], true);
  234.                 if (null !== $inputClass $inputMetadata['class'] ?? null) {
  235.                     $classes[$inputClass] = true;
  236.                 }
  237.                 $outputMetadata $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION$operationName'output', ['class' => $resourceClass], true);
  238.                 if (null !== $outputClass $outputMetadata['class'] ?? null) {
  239.                     $classes[$outputClass] = true;
  240.                 }
  241.             }
  242.         } else {
  243.             $classes[$resourceClass] = true;
  244.             foreach ($resourceMetadata->getOperations() as $operation) {
  245.                 /** @var Operation $operation */
  246.                 if (!$operation instanceof CollectionOperationInterface) {
  247.                     continue;
  248.                 }
  249.                 $inputMetadata $operation->getInput();
  250.                 if (null !== $inputClass $inputMetadata['class'] ?? null) {
  251.                     $classes[$inputClass] = true;
  252.                 }
  253.                 $outputMetadata $operation->getOutput();
  254.                 if (null !== $outputClass $outputMetadata['class'] ?? null) {
  255.                     $classes[$outputClass] = true;
  256.                 }
  257.             }
  258.         }
  259.         /** @var string[] $classes */
  260.         $classes array_keys($classes);
  261.         $properties = [];
  262.         if ($resourceMetadata instanceof ResourceMetadata) {
  263.             $propertyNameContext $this->getPropertyNameCollectionFactoryContext($resourceMetadata);
  264.             $propertyContext = [];
  265.         } else {
  266.             [$propertyNameContext$propertyContext] = $this->getPropertyMetadataFactoryContext($resourceMetadata);
  267.         }
  268.         foreach ($classes as $class) {
  269.             foreach ($this->propertyNameCollectionFactory->create($class$propertyNameContext) as $propertyName) {
  270.                 $propertyMetadata $this->propertyMetadataFactory->create($class$propertyName$propertyContext);
  271.                 if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
  272.                     continue;
  273.                 }
  274.                 if ($this->nameConverter) {
  275.                     $propertyName $this->nameConverter->normalize($propertyName$classself::FORMAT$context);
  276.                 }
  277.                 $properties[] = $this->getProperty($propertyMetadata$propertyName$prefixedShortName$shortName);
  278.             }
  279.         }
  280.         return $properties;
  281.     }
  282.     /**
  283.      * Gets Hydra operations.
  284.      *
  285.      * @param ResourceMetadata|ApiResource $resourceMetadata
  286.      */
  287.     private function getHydraOperations(string $resourceClass$resourceMetadatastring $prefixedShortNamebool $collection, ?ResourceMetadataCollection $resourceMetadataCollection null): array
  288.     {
  289.         if ($resourceMetadata instanceof ResourceMetadata) {
  290.             if (null === $operations $collection $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
  291.                 return [];
  292.             }
  293.             $hydraOperations = [];
  294.             foreach ($operations as $operationName => $operation) {
  295.                 $hydraOperations[] = $this->getHydraOperation($resourceClass$resourceMetadata$operationName$operation$prefixedShortName$collection OperationType::COLLECTION OperationType::ITEM);
  296.             }
  297.         } else {
  298.             $hydraOperations = [];
  299.             foreach ($resourceMetadataCollection as $resourceMetadata) {
  300.                 foreach ($resourceMetadata->getOperations() as $operationName => $operation) {
  301.                     if ((HttpOperation::METHOD_POST === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
  302.                         continue;
  303.                     }
  304.                     $hydraOperations[] = $this->getHydraOperation($resourceClass$resourceMetadata$operationName$operation$operation->getTypes()[0] ?? "#{$operation->getShortName()}"null);
  305.                 }
  306.             }
  307.         }
  308.         if (null !== $this->subresourceOperationFactory && !$this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  309.             foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) {
  310.                 $subresourceMetadata $this->resourceMetadataFactory->create($operation['resource_class']);
  311.                 $propertyMetadata $this->propertyMetadataFactory->create(end($operation['identifiers'])[0], $operation['property']);
  312.                 $hydraOperations[] = $this->getHydraOperation($resourceClass$subresourceMetadata$operation['route_name'], $operation"#{$subresourceMetadata->getShortName()}"OperationType::SUBRESOURCE$propertyMetadata->getSubresource());
  313.             }
  314.         }
  315.         return $hydraOperations;
  316.     }
  317.     /**
  318.      * Gets and populates if applicable a Hydra operation.
  319.      *
  320.      * @param ResourceMetadata|ApiResource $resourceMetadata
  321.      * @param SubresourceMetadata          $subresourceMetadata
  322.      * @param array|HttpOperation          $operation
  323.      */
  324.     private function getHydraOperation(string $resourceClass$resourceMetadatastring $operationName$operationstring $prefixedShortName, ?string $operationType nullSubresourceMetadata $subresourceMetadata null): array
  325.     {
  326.         if ($operation instanceof HttpOperation) {
  327.             $method $operation->getMethod() ?: HttpOperation::METHOD_GET;
  328.         } elseif ($this->operationMethodResolver) {
  329.             if (OperationType::COLLECTION === $operationType) {
  330.                 $method $this->operationMethodResolver->getCollectionOperationMethod($resourceClass$operationName);
  331.             } elseif (OperationType::ITEM === $operationType) {
  332.                 $method $this->operationMethodResolver->getItemOperationMethod($resourceClass$operationName);
  333.             } else {
  334.                 $method 'GET';
  335.             }
  336.         } else {
  337.             $method $resourceMetadata->getTypedOperationAttribute($operationType$operationName'method''GET');
  338.         }
  339.         $hydraOperation $operation instanceof HttpOperation ? ($operation->getHydraContext() ?? []) : ($operation['hydra_context'] ?? []);
  340.         if ($operation instanceof HttpOperation $operation->getDeprecationReason() : $resourceMetadata->getTypedOperationAttribute($operationType$operationName'deprecation_reason'nulltrue)) {
  341.             $hydraOperation['owl:deprecated'] = true;
  342.         }
  343.         if ($operation instanceof HttpOperation) {
  344.             $shortName $operation->getShortName();
  345.             $inputMetadata $operation->getInput() ?? [];
  346.             $outputMetadata $operation->getOutput() ?? [];
  347.             $operationType $operation instanceof CollectionOperationInterface OperationType::COLLECTION OperationType::ITEM;
  348.         } else {
  349.             $shortName $resourceMetadata->getShortName();
  350.             $inputMetadata $resourceMetadata->getTypedOperationAttribute($operationType$operationName'input', ['class' => false]);
  351.             $outputMetadata $resourceMetadata->getTypedOperationAttribute($operationType$operationName'output', ['class' => false]);
  352.         }
  353.         $inputClass = \array_key_exists('class'$inputMetadata) ? $inputMetadata['class'] : false;
  354.         $outputClass = \array_key_exists('class'$outputMetadata) ? $outputMetadata['class'] : false;
  355.         if ('GET' === $method && OperationType::COLLECTION === $operationType) {
  356.             $hydraOperation += [
  357.                 '@type' => ['hydra:Operation''schema:FindAction'],
  358.                 'hydra:title' => "Retrieves the collection of $shortName resources.",
  359.                 'returns' => 'hydra:Collection',
  360.             ];
  361.         } elseif ('GET' === $method && OperationType::SUBRESOURCE === $operationType) {
  362.             $hydraOperation += [
  363.                 '@type' => ['hydra:Operation''schema:FindAction'],
  364.                 'hydra:title' => $subresourceMetadata && $subresourceMetadata->isCollection() ? "Retrieves the collection of $shortName resources." "Retrieves a $shortName resource.",
  365.                 'returns' => null === $outputClass 'owl:Nothing' "#$shortName",
  366.             ];
  367.         } elseif ('GET' === $method) {
  368.             $hydraOperation += [
  369.                 '@type' => ['hydra:Operation''schema:FindAction'],
  370.                 'hydra:title' => "Retrieves a $shortName resource.",
  371.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  372.             ];
  373.         } elseif ('PATCH' === $method) {
  374.             $hydraOperation += [
  375.                 '@type' => 'hydra:Operation',
  376.                 'hydra:title' => "Updates the $shortName resource.",
  377.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  378.                 'expects' => null === $inputClass 'owl:Nothing' $prefixedShortName,
  379.             ];
  380.         } elseif ('POST' === $method) {
  381.             $hydraOperation += [
  382.                 '@type' => ['hydra:Operation''schema:CreateAction'],
  383.                 'hydra:title' => "Creates a $shortName resource.",
  384.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  385.                 'expects' => null === $inputClass 'owl:Nothing' $prefixedShortName,
  386.             ];
  387.         } elseif ('PUT' === $method) {
  388.             $hydraOperation += [
  389.                 '@type' => ['hydra:Operation''schema:ReplaceAction'],
  390.                 'hydra:title' => "Replaces the $shortName resource.",
  391.                 'returns' => null === $outputClass 'owl:Nothing' $prefixedShortName,
  392.                 'expects' => null === $inputClass 'owl:Nothing' $prefixedShortName,
  393.             ];
  394.         } elseif ('DELETE' === $method) {
  395.             $hydraOperation += [
  396.                 '@type' => ['hydra:Operation''schema:DeleteAction'],
  397.                 'hydra:title' => "Deletes the $shortName resource.",
  398.                 'returns' => 'owl:Nothing',
  399.             ];
  400.         }
  401.         $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method;
  402.         if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) {
  403.             $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title'];
  404.         }
  405.         ksort($hydraOperation);
  406.         return $hydraOperation;
  407.     }
  408.     /**
  409.      * Gets the range of the property.
  410.      *
  411.      * @param ApiProperty|PropertyMetadata $propertyMetadata
  412.      */
  413.     private function getRange($propertyMetadata): ?string
  414.     {
  415.         $jsonldContext $propertyMetadata instanceof PropertyMetadata $propertyMetadata->getAttributes()['jsonld_context'] ?? [] : $propertyMetadata->getJsonldContext();
  416.         if (isset($jsonldContext['@type'])) {
  417.             return $jsonldContext['@type'];
  418.         }
  419.         // TODO: 3.0 support multiple types, default value of types will be [] instead of null
  420.         $type $propertyMetadata instanceof PropertyMetadata $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null;
  421.         if (null === $type) {
  422.             return null;
  423.         }
  424.         if ($type->isCollection() && null !== $collectionType method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType()) {
  425.             $type $collectionType;
  426.         }
  427.         switch ($type->getBuiltinType()) {
  428.             case Type::BUILTIN_TYPE_STRING:
  429.                 return 'xmls:string';
  430.             case Type::BUILTIN_TYPE_INT:
  431.                 return 'xmls:integer';
  432.             case Type::BUILTIN_TYPE_FLOAT:
  433.                 return 'xmls:decimal';
  434.             case Type::BUILTIN_TYPE_BOOL:
  435.                 return 'xmls:boolean';
  436.             case Type::BUILTIN_TYPE_OBJECT:
  437.                 if (null === $className $type->getClassName()) {
  438.                     return null;
  439.                 }
  440.                 if (is_a($className, \DateTimeInterface::class, true)) {
  441.                     return 'xmls:dateTime';
  442.                 }
  443.                 if ($this->resourceClassResolver->isResourceClass($className)) {
  444.                     $resourceMetadata $this->resourceMetadataFactory->create($className);
  445.                     if ($resourceMetadata instanceof ResourceMetadataCollection) {
  446.                         $operation $resourceMetadata->getOperation();
  447.                         if (!$operation instanceof HttpOperation) {
  448.                             return "#{$operation->getShortName()}";
  449.                         }
  450.                         return $operation->getTypes()[0] ?? "#{$operation->getShortName()}";
  451.                     }
  452.                     return $resourceMetadata->getIri() ?? "#{$resourceMetadata->getShortName()}";
  453.                 }
  454.         }
  455.         return null;
  456.     }
  457.     /**
  458.      * Builds the classes array.
  459.      */
  460.     private function getClasses(array $entrypointProperties, array $classes): array
  461.     {
  462.         $classes[] = [
  463.             '@id' => '#Entrypoint',
  464.             '@type' => 'hydra:Class',
  465.             'hydra:title' => 'The API entrypoint',
  466.             'hydra:supportedProperty' => $entrypointProperties,
  467.             'hydra:supportedOperation' => [
  468.                 '@type' => 'hydra:Operation',
  469.                 'hydra:method' => 'GET',
  470.                 'rdfs:label' => 'The API entrypoint.',
  471.                 'returns' => '#EntryPoint',
  472.             ],
  473.         ];
  474.         // Constraint violation
  475.         $classes[] = [
  476.             '@id' => '#ConstraintViolation',
  477.             '@type' => 'hydra:Class',
  478.             'hydra:title' => 'A constraint violation',
  479.             'hydra:supportedProperty' => [
  480.                 [
  481.                     '@type' => 'hydra:SupportedProperty',
  482.                     'hydra:property' => [
  483.                         '@id' => '#ConstraintViolation/propertyPath',
  484.                         '@type' => 'rdf:Property',
  485.                         'rdfs:label' => 'propertyPath',
  486.                         'domain' => '#ConstraintViolation',
  487.                         'range' => 'xmls:string',
  488.                     ],
  489.                     'hydra:title' => 'propertyPath',
  490.                     'hydra:description' => 'The property path of the violation',
  491.                     'hydra:readable' => true,
  492.                     'hydra:writeable' => false,
  493.                 ],
  494.                 [
  495.                     '@type' => 'hydra:SupportedProperty',
  496.                     'hydra:property' => [
  497.                         '@id' => '#ConstraintViolation/message',
  498.                         '@type' => 'rdf:Property',
  499.                         'rdfs:label' => 'message',
  500.                         'domain' => '#ConstraintViolation',
  501.                         'range' => 'xmls:string',
  502.                     ],
  503.                     'hydra:title' => 'message',
  504.                     'hydra:description' => 'The message associated with the violation',
  505.                     'hydra:readable' => true,
  506.                     'hydra:writeable' => false,
  507.                 ],
  508.             ],
  509.         ];
  510.         // Constraint violation list
  511.         $classes[] = [
  512.             '@id' => '#ConstraintViolationList',
  513.             '@type' => 'hydra:Class',
  514.             'subClassOf' => 'hydra:Error',
  515.             'hydra:title' => 'A constraint violation list',
  516.             'hydra:supportedProperty' => [
  517.                 [
  518.                     '@type' => 'hydra:SupportedProperty',
  519.                     'hydra:property' => [
  520.                         '@id' => '#ConstraintViolationList/violations',
  521.                         '@type' => 'rdf:Property',
  522.                         'rdfs:label' => 'violations',
  523.                         'domain' => '#ConstraintViolationList',
  524.                         'range' => '#ConstraintViolation',
  525.                     ],
  526.                     'hydra:title' => 'violations',
  527.                     'hydra:description' => 'The violations',
  528.                     'hydra:readable' => true,
  529.                     'hydra:writeable' => false,
  530.                 ],
  531.             ],
  532.         ];
  533.         return $classes;
  534.     }
  535.     /**
  536.      * Gets a property definition.
  537.      *
  538.      * @param ApiProperty|PropertyMetadata $propertyMetadata
  539.      */
  540.     private function getProperty($propertyMetadatastring $propertyNamestring $prefixedShortNamestring $shortName): array
  541.     {
  542.         if ($propertyMetadata instanceof PropertyMetadata) {
  543.             $iri $propertyMetadata->getIri();
  544.         } else {
  545.             if ($iri $propertyMetadata->getIris()) {
  546.                 $iri === \count($iri) ? $iri[0] : $iri;
  547.             }
  548.         }
  549.         if (!isset($iri)) {
  550.             $iri "#$shortName/$propertyName";
  551.         }
  552.         $propertyData = [
  553.             '@id' => $iri,
  554.             '@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' 'rdf:Property',
  555.             'rdfs:label' => $propertyName,
  556.             'domain' => $prefixedShortName,
  557.         ];
  558.         // TODO: 3.0 support multiple types, default value of types will be [] instead of null
  559.         $type $propertyMetadata instanceof PropertyMetadata $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null;
  560.         if (null !== $type && !$type->isCollection() && (null !== $className $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) {
  561.             $propertyData['owl:maxCardinality'] = 1;
  562.         }
  563.         $property = [
  564.             '@type' => 'hydra:SupportedProperty',
  565.             'hydra:property' => $propertyData,
  566.             'hydra:title' => $propertyName,
  567.             'hydra:required' => $propertyMetadata->isRequired(),
  568.             'hydra:readable' => $propertyMetadata->isReadable(),
  569.             'hydra:writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
  570.         ];
  571.         if (null !== $range $this->getRange($propertyMetadata)) {
  572.             $property['hydra:property']['range'] = $range;
  573.         }
  574.         if (null !== $description $propertyMetadata->getDescription()) {
  575.             $property['hydra:description'] = $description;
  576.         }
  577.         if ($deprecationReason $propertyMetadata instanceof PropertyMetadata $propertyMetadata->getAttribute('deprecation_reason') : $propertyMetadata->getDeprecationReason()) {
  578.             $property['owl:deprecated'] = true;
  579.         }
  580.         return $property;
  581.     }
  582.     /**
  583.      * Computes the documentation.
  584.      */
  585.     private function computeDoc(Documentation $object, array $classes): array
  586.     {
  587.         $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation'];
  588.         if ('' !== $object->getTitle()) {
  589.             $doc['hydra:title'] = $object->getTitle();
  590.         }
  591.         if ('' !== $object->getDescription()) {
  592.             $doc['hydra:description'] = $object->getDescription();
  593.         }
  594.         $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
  595.         $doc['hydra:supportedClass'] = $classes;
  596.         return $doc;
  597.     }
  598.     /**
  599.      * Builds the JSON-LD context for the API documentation.
  600.      */
  601.     private function getContext(): array
  602.     {
  603.         return [
  604.             '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
  605.             'hydra' => ContextBuilderInterface::HYDRA_NS,
  606.             'rdf' => ContextBuilderInterface::RDF_NS,
  607.             'rdfs' => ContextBuilderInterface::RDFS_NS,
  608.             'xmls' => ContextBuilderInterface::XML_NS,
  609.             'owl' => ContextBuilderInterface::OWL_NS,
  610.             'schema' => ContextBuilderInterface::SCHEMA_ORG_NS,
  611.             'domain' => ['@id' => 'rdfs:domain''@type' => '@id'],
  612.             'range' => ['@id' => 'rdfs:range''@type' => '@id'],
  613.             'subClassOf' => ['@id' => 'rdfs:subClassOf''@type' => '@id'],
  614.             'expects' => ['@id' => 'hydra:expects''@type' => '@id'],
  615.             'returns' => ['@id' => 'hydra:returns''@type' => '@id'],
  616.         ];
  617.     }
  618.     /**
  619.      * {@inheritdoc}
  620.      */
  621.     public function supportsNormalization($data$format null, array $context = []): bool
  622.     {
  623.         return self::FORMAT === $format && $data instanceof Documentation;
  624.     }
  625.     /**
  626.      * {@inheritdoc}
  627.      */
  628.     public function hasCacheableSupportsMethod(): bool
  629.     {
  630.         return true;
  631.     }
  632. }
  633. class_alias(DocumentationNormalizer::class, \ApiPlatform\Core\Hydra\Serializer\DocumentationNormalizer::class);