Skip to content

Commit 6d049cf

Browse files
committed
feat: add operation to hydra response
1 parent 61c6dee commit 6d049cf

3 files changed

Lines changed: 152 additions & 101 deletions

File tree

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 1 addition & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use ApiPlatform\Metadata\ApiResource;
2222
use ApiPlatform\Metadata\CollectionOperationInterface;
2323
use ApiPlatform\Metadata\ErrorResource;
24-
use ApiPlatform\Metadata\HttpOperation;
2524
use ApiPlatform\Metadata\Operation;
2625
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2726
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -48,6 +47,7 @@
4847
*/
4948
final class DocumentationNormalizer implements NormalizerInterface
5049
{
50+
use HydraOperationsTrait;
5151
use HydraPrefixTrait;
5252
public const FORMAT = 'jsonld';
5353

@@ -254,106 +254,6 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource
254254
return $properties;
255255
}
256256

257-
/**
258-
* Gets Hydra operations.
259-
*/
260-
private function getHydraOperations(bool $collection, ApiResource $resourceMetadata, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
261-
{
262-
$hydraOperations = [];
263-
foreach ($resourceMetadata->getOperations() as $operation) {
264-
if (true === $operation->getHideHydraOperation()) {
265-
continue;
266-
}
267-
268-
if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
269-
continue;
270-
}
271-
272-
$hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
273-
}
274-
275-
return $hydraOperations;
276-
}
277-
278-
/**
279-
* Gets and populates if applicable a Hydra operation.
280-
*/
281-
private function getHydraOperation(HttpOperation $operation, string $prefixedShortName, string $hydraPrefix): array
282-
{
283-
$method = $operation->getMethod() ?: 'GET';
284-
285-
$hydraOperation = $operation->getHydraContext() ?? [];
286-
if ($operation->getDeprecationReason()) {
287-
$hydraOperation['owl:deprecated'] = true;
288-
}
289-
290-
$shortName = $operation->getShortName();
291-
$inputMetadata = $operation->getInput() ?? [];
292-
$outputMetadata = $operation->getOutput() ?? [];
293-
294-
$inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
295-
$outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
296-
297-
if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
298-
$hydraOperation += [
299-
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
300-
$hydraPrefix.'description' => "Retrieves the collection of $shortName resources.",
301-
'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
302-
];
303-
} elseif ('GET' === $method) {
304-
$hydraOperation += [
305-
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
306-
$hydraPrefix.'description' => "Retrieves a $shortName resource.",
307-
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
308-
];
309-
} elseif ('PATCH' === $method) {
310-
$hydraOperation += [
311-
'@type' => $hydraPrefix.'Operation',
312-
$hydraPrefix.'description' => "Updates the $shortName resource.",
313-
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
314-
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
315-
];
316-
317-
if (null !== $inputClass) {
318-
$possibleValue = [];
319-
foreach ($operation->getInputFormats() ?? [] as $mimeTypes) {
320-
foreach ($mimeTypes as $mimeType) {
321-
$possibleValue[] = $mimeType;
322-
}
323-
}
324-
325-
$hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]];
326-
}
327-
} elseif ('POST' === $method) {
328-
$hydraOperation += [
329-
'@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
330-
$hydraPrefix.'description' => "Creates a $shortName resource.",
331-
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
332-
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
333-
];
334-
} elseif ('PUT' === $method) {
335-
$hydraOperation += [
336-
'@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
337-
$hydraPrefix.'description' => "Replaces the $shortName resource.",
338-
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
339-
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
340-
];
341-
} elseif ('DELETE' === $method) {
342-
$hydraOperation += [
343-
'@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
344-
$hydraPrefix.'description' => "Deletes the $shortName resource.",
345-
'returns' => 'owl:Nothing',
346-
];
347-
}
348-
349-
$hydraOperation[$hydraPrefix.'method'] ??= $method;
350-
$hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');
351-
352-
ksort($hydraOperation);
353-
354-
return $hydraOperation;
355-
}
356-
357257
/**
358258
* Gets the range of the property.
359259
*/
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Hydra\Serializer;
15+
16+
use ApiPlatform\JsonLd\ContextBuilder;
17+
use ApiPlatform\Metadata\ApiResource;
18+
use ApiPlatform\Metadata\CollectionOperationInterface;
19+
use ApiPlatform\Metadata\HttpOperation;
20+
21+
/**
22+
* Generates Hydra operations for JSON-LD responses.
23+
*
24+
* @author Kévin Dunglas <dunglas@gmail.com>
25+
*/
26+
trait HydraOperationsTrait
27+
{
28+
/**
29+
* Gets Hydra operations.
30+
*/
31+
private function getHydraOperations(bool $collection, ApiResource $resourceMetadata, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
32+
{
33+
$hydraOperations = [];
34+
foreach ($resourceMetadata->getOperations() as $operation) {
35+
if (true === $operation->getHideHydraOperation()) {
36+
continue;
37+
}
38+
39+
if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
40+
continue;
41+
}
42+
43+
$hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
44+
}
45+
46+
return $hydraOperations;
47+
}
48+
49+
/**
50+
* Gets and populates if applicable a Hydra operation.
51+
*/
52+
private function getHydraOperation(HttpOperation $operation, string $prefixedShortName, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
53+
{
54+
$method = $operation->getMethod() ?: 'GET';
55+
56+
$hydraOperation = $operation->getHydraContext() ?? [];
57+
if ($operation->getDeprecationReason()) {
58+
$hydraOperation['owl:deprecated'] = true;
59+
}
60+
61+
$shortName = $operation->getShortName();
62+
$inputMetadata = $operation->getInput() ?? [];
63+
$outputMetadata = $operation->getOutput() ?? [];
64+
65+
$inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
66+
$outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
67+
68+
if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
69+
$hydraOperation += [
70+
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
71+
$hydraPrefix.'description' => "Retrieves the collection of $shortName resources.",
72+
'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
73+
];
74+
} elseif ('GET' === $method) {
75+
$hydraOperation += [
76+
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
77+
$hydraPrefix.'description' => "Retrieves a $shortName resource.",
78+
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
79+
];
80+
} elseif ('PATCH' === $method) {
81+
$hydraOperation += [
82+
'@type' => $hydraPrefix.'Operation',
83+
$hydraPrefix.'description' => "Updates the $shortName resource.",
84+
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
85+
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
86+
];
87+
88+
if (null !== $inputClass) {
89+
$possibleValue = [];
90+
foreach ($operation->getInputFormats() ?? [] as $mimeTypes) {
91+
foreach ($mimeTypes as $mimeType) {
92+
$possibleValue[] = $mimeType;
93+
}
94+
}
95+
96+
$hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]];
97+
}
98+
} elseif ('POST' === $method) {
99+
$hydraOperation += [
100+
'@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
101+
$hydraPrefix.'description' => "Creates a $shortName resource.",
102+
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
103+
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
104+
];
105+
} elseif ('PUT' === $method) {
106+
$hydraOperation += [
107+
'@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
108+
$hydraPrefix.'description' => "Replaces the $shortName resource.",
109+
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
110+
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
111+
];
112+
} elseif ('DELETE' === $method) {
113+
$hydraOperation += [
114+
'@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
115+
$hydraPrefix.'description' => "Deletes the $shortName resource.",
116+
'returns' => 'owl:Nothing',
117+
];
118+
}
119+
120+
$hydraOperation[$hydraPrefix.'method'] ??= $method;
121+
$hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');
122+
123+
ksort($hydraOperation);
124+
125+
return $hydraOperation;
126+
}
127+
}

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
namespace ApiPlatform\JsonLd\Serializer;
1515

16+
use ApiPlatform\Hydra\Serializer\HydraOperationsTrait;
1617
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
1718
use ApiPlatform\JsonLd\ContextBuilderInterface;
19+
use ApiPlatform\Metadata\ErrorResourceInterface;
1820
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
1921
use ApiPlatform\Metadata\HttpOperation;
2022
use ApiPlatform\Metadata\IriConverterInterface;
@@ -45,6 +47,8 @@ final class ItemNormalizer extends AbstractItemNormalizer
4547
{
4648
use ClassInfoTrait;
4749
use ContextTrait;
50+
use HydraOperationsTrait;
51+
use HydraPrefixTrait;
4852
use JsonLdContextTrait;
4953

5054
public const FORMAT = 'jsonld';
@@ -72,8 +76,11 @@ final class ItemNormalizer extends AbstractItemNormalizer
7276
'@vocab',
7377
];
7478

79+
private array $itemNormalizerDefaultContext = [];
80+
7581
public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null, private ?OperationMetadataFactoryInterface $operationMetadataFactory = null, ?OperationResourceClassResolverInterface $operationResourceResolver = null)
7682
{
83+
$this->itemNormalizerDefaultContext = $defaultContext;
7784
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector, $operationResourceResolver);
7885
}
7986

@@ -110,6 +117,7 @@ public function normalize(mixed $data, ?string $format = null, array $context =
110117
// TODO: we should not remove the resource_class in the normalizeRawCollection as we would find out anyway that it's not the same as the requested one
111118
$previousResourceClass = $context['resource_class'] ?? null;
112119
$metadata = [];
120+
$isRootResource = !isset($context['jsonld_has_context']);
113121
if ($isResourceClass = $this->resourceClassResolver->isResourceClass($resourceClass) && (null === $previousResourceClass || $this->resourceClassResolver->isResourceClass($previousResourceClass))) {
114122
$resourceClass = $this->resourceClassResolver->getResourceClass($data, $previousResourceClass);
115123
if (isset($context['operation']) && $context['operation'] instanceof HttpOperation && $context['operation']->getClass() !== $resourceClass) {
@@ -184,6 +192,22 @@ public function normalize(mixed $data, ?string $format = null, array $context =
184192
$metadata['@type'] = 1 === \count($types) ? $types[0] : $types;
185193
}
186194

195+
if ($isResourceClass && !is_a($resourceClass, ErrorResourceInterface::class, true) && $isRootResource) {
196+
$isCollectionRoute = $context['api_collection_sub_level'] ?? false;
197+
$showItemHydraOperationsInCollection = $context['hydra_item_operations_in_collection'] ?? false;
198+
199+
if (!$isCollectionRoute || $showItemHydraOperationsInCollection) {
200+
$hydraOperations = $this->getHydraOperations(
201+
false,
202+
$this->resourceMetadataCollectionFactory->create($resourceClass)[0],
203+
$this->getHydraPrefix($context + $this->itemNormalizerDefaultContext)
204+
);
205+
if (!empty($hydraOperations)) {
206+
$metadata['operation'] = $hydraOperations;
207+
}
208+
}
209+
}
210+
187211
return $metadata + $normalizedData;
188212
}
189213

0 commit comments

Comments
 (0)