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

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 NormalizerInterface, CacheableSupportsMethodInterface
  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($resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, UrlGeneratorInterface $urlGenerator, SubresourceOperationFactoryInterface $subresourceOperationFactory = null, NameConverterInterface $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. * @param mixed|null $format
  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, $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties, ResourceMetadataCollection $resourceMetadataCollection = null)
  112. {
  113. $hydraCollectionOperations = $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, true, $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', null, true) : $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, $resourceMetadata, string $shortName, string $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, $prefixedShortName, false, $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, $resourceMetadata, string $shortName, string $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, $class, self::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, $resourceMetadata, string $prefixedShortName, bool $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 array|HttpOperation $operation
  322. */
  323. private function getHydraOperation(string $resourceClass, $resourceMetadata, string $operationName, $operation, string $prefixedShortName, string $operationType = null, SubresourceMetadata $subresourceMetadata = null): array
  324. {
  325. if ($operation instanceof HttpOperation) {
  326. $method = $operation->getMethod() ?: HttpOperation::METHOD_GET;
  327. } elseif ($this->operationMethodResolver) {
  328. if (OperationType::COLLECTION === $operationType) {
  329. $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
  330. } elseif (OperationType::ITEM === $operationType) {
  331. $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName);
  332. } else {
  333. $method = 'GET';
  334. }
  335. } else {
  336. $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET');
  337. }
  338. $hydraOperation = $operation instanceof HttpOperation ? ($operation->getHydraContext() ?? []) : ($operation['hydra_context'] ?? []);
  339. if ($operation instanceof HttpOperation ? $operation->getDeprecationReason() : $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
  340. $hydraOperation['owl:deprecated'] = true;
  341. }
  342. if ($operation instanceof HttpOperation) {
  343. $shortName = $operation->getShortName();
  344. $inputMetadata = $operation->getInput() ?? [];
  345. $outputMetadata = $operation->getOutput() ?? [];
  346. $operationType = $operation instanceof CollectionOperationInterface ? OperationType::COLLECTION : OperationType::ITEM;
  347. } else {
  348. $shortName = $resourceMetadata->getShortName();
  349. $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => false]);
  350. $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => false]);
  351. }
  352. $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
  353. $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
  354. if ('GET' === $method && OperationType::COLLECTION === $operationType) {
  355. $hydraOperation += [
  356. '@type' => ['hydra:Operation', 'schema:FindAction'],
  357. 'hydra:title' => "Retrieves the collection of $shortName resources.",
  358. 'returns' => 'hydra:Collection',
  359. ];
  360. } elseif ('GET' === $method && OperationType::SUBRESOURCE === $operationType) {
  361. $hydraOperation += [
  362. '@type' => ['hydra:Operation', 'schema:FindAction'],
  363. 'hydra:title' => $subresourceMetadata && $subresourceMetadata->isCollection() ? "Retrieves the collection of $shortName resources." : "Retrieves a $shortName resource.",
  364. 'returns' => null === $outputClass ? 'owl:Nothing' : "#$shortName",
  365. ];
  366. } elseif ('GET' === $method) {
  367. $hydraOperation += [
  368. '@type' => ['hydra:Operation', 'schema:FindAction'],
  369. 'hydra:title' => "Retrieves a $shortName resource.",
  370. 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
  371. ];
  372. } elseif ('PATCH' === $method) {
  373. $hydraOperation += [
  374. '@type' => 'hydra:Operation',
  375. 'hydra:title' => "Updates the $shortName resource.",
  376. 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
  377. 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
  378. ];
  379. } elseif ('POST' === $method) {
  380. $hydraOperation += [
  381. '@type' => ['hydra:Operation', 'schema:CreateAction'],
  382. 'hydra:title' => "Creates a $shortName resource.",
  383. 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
  384. 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
  385. ];
  386. } elseif ('PUT' === $method) {
  387. $hydraOperation += [
  388. '@type' => ['hydra:Operation', 'schema:ReplaceAction'],
  389. 'hydra:title' => "Replaces the $shortName resource.",
  390. 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
  391. 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
  392. ];
  393. } elseif ('DELETE' === $method) {
  394. $hydraOperation += [
  395. '@type' => ['hydra:Operation', 'schema:DeleteAction'],
  396. 'hydra:title' => "Deletes the $shortName resource.",
  397. 'returns' => 'owl:Nothing',
  398. ];
  399. }
  400. $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method;
  401. if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) {
  402. $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title'];
  403. }
  404. ksort($hydraOperation);
  405. return $hydraOperation;
  406. }
  407. /**
  408. * Gets the range of the property.
  409. *
  410. * @param ApiProperty|PropertyMetadata $propertyMetadata
  411. */
  412. private function getRange($propertyMetadata): ?string
  413. {
  414. $jsonldContext = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttributes()['jsonld_context'] ?? [] : $propertyMetadata->getJsonldContext();
  415. if (isset($jsonldContext['@type'])) {
  416. return $jsonldContext['@type'];
  417. }
  418. // TODO: 3.0 support multiple types, default value of types will be [] instead of null
  419. $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null;
  420. if (null === $type) {
  421. return null;
  422. }
  423. if ($type->isCollection() && null !== $collectionType = method_exists(Type::class, 'getCollectionValueTypes') ? ($type->getCollectionValueTypes()[0] ?? null) : $type->getCollectionValueType()) {
  424. $type = $collectionType;
  425. }
  426. switch ($type->getBuiltinType()) {
  427. case Type::BUILTIN_TYPE_STRING:
  428. return 'xmls:string';
  429. case Type::BUILTIN_TYPE_INT:
  430. return 'xmls:integer';
  431. case Type::BUILTIN_TYPE_FLOAT:
  432. return 'xmls:decimal';
  433. case Type::BUILTIN_TYPE_BOOL:
  434. return 'xmls:boolean';
  435. case Type::BUILTIN_TYPE_OBJECT:
  436. if (null === $className = $type->getClassName()) {
  437. return null;
  438. }
  439. if (is_a($className, \DateTimeInterface::class, true)) {
  440. return 'xmls:dateTime';
  441. }
  442. if ($this->resourceClassResolver->isResourceClass($className)) {
  443. $resourceMetadata = $this->resourceMetadataFactory->create($className);
  444. if ($resourceMetadata instanceof ResourceMetadataCollection) {
  445. $operation = $resourceMetadata->getOperation();
  446. if (!$operation instanceof HttpOperation) {
  447. return "#{$operation->getShortName()}";
  448. }
  449. return $operation->getTypes()[0] ?? "#{$operation->getShortName()}";
  450. }
  451. return $resourceMetadata->getIri() ?? "#{$resourceMetadata->getShortName()}";
  452. }
  453. }
  454. return null;
  455. }
  456. /**
  457. * Builds the classes array.
  458. */
  459. private function getClasses(array $entrypointProperties, array $classes): array
  460. {
  461. $classes[] = [
  462. '@id' => '#Entrypoint',
  463. '@type' => 'hydra:Class',
  464. 'hydra:title' => 'The API entrypoint',
  465. 'hydra:supportedProperty' => $entrypointProperties,
  466. 'hydra:supportedOperation' => [
  467. '@type' => 'hydra:Operation',
  468. 'hydra:method' => 'GET',
  469. 'rdfs:label' => 'The API entrypoint.',
  470. 'returns' => '#EntryPoint',
  471. ],
  472. ];
  473. // Constraint violation
  474. $classes[] = [
  475. '@id' => '#ConstraintViolation',
  476. '@type' => 'hydra:Class',
  477. 'hydra:title' => 'A constraint violation',
  478. 'hydra:supportedProperty' => [
  479. [
  480. '@type' => 'hydra:SupportedProperty',
  481. 'hydra:property' => [
  482. '@id' => '#ConstraintViolation/propertyPath',
  483. '@type' => 'rdf:Property',
  484. 'rdfs:label' => 'propertyPath',
  485. 'domain' => '#ConstraintViolation',
  486. 'range' => 'xmls:string',
  487. ],
  488. 'hydra:title' => 'propertyPath',
  489. 'hydra:description' => 'The property path of the violation',
  490. 'hydra:readable' => true,
  491. 'hydra:writeable' => false,
  492. ],
  493. [
  494. '@type' => 'hydra:SupportedProperty',
  495. 'hydra:property' => [
  496. '@id' => '#ConstraintViolation/message',
  497. '@type' => 'rdf:Property',
  498. 'rdfs:label' => 'message',
  499. 'domain' => '#ConstraintViolation',
  500. 'range' => 'xmls:string',
  501. ],
  502. 'hydra:title' => 'message',
  503. 'hydra:description' => 'The message associated with the violation',
  504. 'hydra:readable' => true,
  505. 'hydra:writeable' => false,
  506. ],
  507. ],
  508. ];
  509. // Constraint violation list
  510. $classes[] = [
  511. '@id' => '#ConstraintViolationList',
  512. '@type' => 'hydra:Class',
  513. 'subClassOf' => 'hydra:Error',
  514. 'hydra:title' => 'A constraint violation list',
  515. 'hydra:supportedProperty' => [
  516. [
  517. '@type' => 'hydra:SupportedProperty',
  518. 'hydra:property' => [
  519. '@id' => '#ConstraintViolationList/violations',
  520. '@type' => 'rdf:Property',
  521. 'rdfs:label' => 'violations',
  522. 'domain' => '#ConstraintViolationList',
  523. 'range' => '#ConstraintViolation',
  524. ],
  525. 'hydra:title' => 'violations',
  526. 'hydra:description' => 'The violations',
  527. 'hydra:readable' => true,
  528. 'hydra:writeable' => false,
  529. ],
  530. ],
  531. ];
  532. return $classes;
  533. }
  534. /**
  535. * Gets a property definition.
  536. *
  537. * @param ApiProperty|PropertyMetadata $propertyMetadata
  538. */
  539. private function getProperty($propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array
  540. {
  541. if ($propertyMetadata instanceof PropertyMetadata) {
  542. $iri = $propertyMetadata->getIri();
  543. } else {
  544. if ($iri = $propertyMetadata->getIris()) {
  545. $iri = 1 === \count($iri) ? $iri[0] : $iri;
  546. }
  547. }
  548. if (!isset($iri)) {
  549. $iri = "#$shortName/$propertyName";
  550. }
  551. $propertyData = [
  552. '@id' => $iri,
  553. '@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' : 'rdf:Property',
  554. 'rdfs:label' => $propertyName,
  555. 'domain' => $prefixedShortName,
  556. ];
  557. // TODO: 3.0 support multiple types, default value of types will be [] instead of null
  558. $type = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getType() : $propertyMetadata->getBuiltinTypes()[0] ?? null;
  559. if (null !== $type && !$type->isCollection() && (null !== $className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) {
  560. $propertyData['owl:maxCardinality'] = 1;
  561. }
  562. $property = [
  563. '@type' => 'hydra:SupportedProperty',
  564. 'hydra:property' => $propertyData,
  565. 'hydra:title' => $propertyName,
  566. 'hydra:required' => $propertyMetadata->isRequired(),
  567. 'hydra:readable' => $propertyMetadata->isReadable(),
  568. 'hydra:writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
  569. ];
  570. if (null !== $range = $this->getRange($propertyMetadata)) {
  571. $property['hydra:property']['range'] = $range;
  572. }
  573. if (null !== $description = $propertyMetadata->getDescription()) {
  574. $property['hydra:description'] = $description;
  575. }
  576. if ($deprecationReason = $propertyMetadata instanceof PropertyMetadata ? $propertyMetadata->getAttribute('deprecation_reason') : $propertyMetadata->getDeprecationReason()) {
  577. $property['owl:deprecated'] = true;
  578. }
  579. return $property;
  580. }
  581. /**
  582. * Computes the documentation.
  583. */
  584. private function computeDoc(Documentation $object, array $classes): array
  585. {
  586. $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation'];
  587. if ('' !== $object->getTitle()) {
  588. $doc['hydra:title'] = $object->getTitle();
  589. }
  590. if ('' !== $object->getDescription()) {
  591. $doc['hydra:description'] = $object->getDescription();
  592. }
  593. $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
  594. $doc['hydra:supportedClass'] = $classes;
  595. return $doc;
  596. }
  597. /**
  598. * Builds the JSON-LD context for the API documentation.
  599. */
  600. private function getContext(): array
  601. {
  602. return [
  603. '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
  604. 'hydra' => ContextBuilderInterface::HYDRA_NS,
  605. 'rdf' => ContextBuilderInterface::RDF_NS,
  606. 'rdfs' => ContextBuilderInterface::RDFS_NS,
  607. 'xmls' => ContextBuilderInterface::XML_NS,
  608. 'owl' => ContextBuilderInterface::OWL_NS,
  609. 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS,
  610. 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'],
  611. 'range' => ['@id' => 'rdfs:range', '@type' => '@id'],
  612. 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'],
  613. 'expects' => ['@id' => 'hydra:expects', '@type' => '@id'],
  614. 'returns' => ['@id' => 'hydra:returns', '@type' => '@id'],
  615. ];
  616. }
  617. public function supportsNormalization($data, $format = null, array $context = []): bool
  618. {
  619. return self::FORMAT === $format && $data instanceof Documentation;
  620. }
  621. public function hasCacheableSupportsMethod(): bool
  622. {
  623. return true;
  624. }
  625. }
  626. class_alias(DocumentationNormalizer::class, \ApiPlatform\Core\Hydra\Serializer\DocumentationNormalizer::class);