Skip to content

Commit 605eee4

Browse files
committed
fix(hydra): rdfs:label should not duplicate title
1 parent 01fd742 commit 605eee4

File tree

4 files changed

+74
-90
lines changed

4 files changed

+74
-90
lines changed

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 40 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,13 @@ public function normalize(mixed $object, ?string $format = null, array $context
7171
$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
7272

7373
$resourceMetadata = $resourceMetadataCollection[0];
74-
if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) {
75-
continue;
76-
}
77-
7874
if (true === $resourceMetadata->getHideHydraOperation()) {
7975
continue;
8076
}
8177

8278
$shortName = $resourceMetadata->getShortName();
83-
8479
$prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
80+
8581
$this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection);
8682
$classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection);
8783
}
@@ -105,8 +101,9 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
105101
'@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)),
106102
'@type' => $hydraPrefix.'Link',
107103
'domain' => '#Entrypoint',
108-
'rdfs:label' => "The collection of $shortName resources",
109-
'rdfs:range' => [
104+
'title' => "{$shortName}CollectionEntrypoint",
105+
'owl:maxCardinality' => 1,
106+
'range' => [
110107
['@id' => $hydraPrefix.'Collection'],
111108
[
112109
'owl:equivalentClass' => [
@@ -117,7 +114,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
117114
],
118115
$hydraPrefix.'supportedOperation' => $hydraCollectionOperations,
119116
],
120-
$hydraPrefix.'title' => "The collection of $shortName resources",
117+
$hydraPrefix.'title' => "get{$shortName}Collection",
118+
$hydraPrefix.'description' => "The collection of $shortName resources",
121119
$hydraPrefix.'readable' => true,
122120
$hydraPrefix.'writeable' => false,
123121
];
@@ -140,7 +138,6 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata,
140138
$class = [
141139
'@id' => $prefixedShortName,
142140
'@type' => $hydraPrefix.'Class',
143-
'rdfs:label' => $shortName,
144141
$hydraPrefix.'title' => $shortName,
145142
$hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix),
146143
$hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix),
@@ -150,6 +147,10 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata,
150147
$class[$hydraPrefix.'description'] = $description;
151148
}
152149

150+
if ($resourceMetadata instanceof ErrorResource) {
151+
$class['subClassOf'] = 'Error';
152+
}
153+
153154
if ($isDeprecated) {
154155
$class['owl:deprecated'] = true;
155156
}
@@ -232,6 +233,10 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource
232233
$propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
233234
}
234235

236+
if (false === $propertyMetadata->getHydra()) {
237+
continue;
238+
}
239+
235240
$properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
236241
}
237242
}
@@ -254,6 +259,7 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio
254259
if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
255260
continue;
256261
}
262+
257263
$hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
258264
}
259265
}
@@ -283,49 +289,57 @@ private function getHydraOperation(HttpOperation $operation, string $prefixedSho
283289
if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
284290
$hydraOperation += [
285291
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
286-
$hydraPrefix.'title' => "Retrieves the collection of $shortName resources.",
292+
$hydraPrefix.'description' => "Retrieves the collection of $shortName resources.",
287293
'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
288294
];
289295
} elseif ('GET' === $method) {
290296
$hydraOperation += [
291297
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
292-
$hydraPrefix.'title' => "Retrieves a $shortName resource.",
298+
$hydraPrefix.'description' => "Retrieves a $shortName resource.",
293299
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
294300
];
295301
} elseif ('PATCH' === $method) {
296302
$hydraOperation += [
297303
'@type' => $hydraPrefix.'Operation',
298-
$hydraPrefix.'title' => "Updates the $shortName resource.",
304+
$hydraPrefix.'description' => "Updates the $shortName resource.",
299305
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
300306
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
301307
];
308+
309+
if (null !== $inputClass) {
310+
$possibleValue = [];
311+
foreach ($operation->getInputFormats() as $mimeTypes) {
312+
foreach ($mimeTypes as $mimeType) {
313+
$possibleValue[] = $mimeType;
314+
}
315+
}
316+
317+
$hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]];
318+
}
302319
} elseif ('POST' === $method) {
303320
$hydraOperation += [
304321
'@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
305-
$hydraPrefix.'title' => "Creates a $shortName resource.",
322+
$hydraPrefix.'description' => "Creates a $shortName resource.",
306323
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
307324
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
308325
];
309326
} elseif ('PUT' === $method) {
310327
$hydraOperation += [
311328
'@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
312-
$hydraPrefix.'title' => "Replaces the $shortName resource.",
329+
$hydraPrefix.'description' => "Replaces the $shortName resource.",
313330
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
314331
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
315332
];
316333
} elseif ('DELETE' === $method) {
317334
$hydraOperation += [
318335
'@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
319-
$hydraPrefix.'title' => "Deletes the $shortName resource.",
336+
$hydraPrefix.'description' => "Deletes the $shortName resource.",
320337
'returns' => 'owl:Nothing',
321338
];
322339
}
323340

324-
$hydraOperation[$hydraPrefix.'method'] ?? $hydraOperation[$hydraPrefix.'method'] = $method;
325-
326-
if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation[$hydraPrefix.'title'])) {
327-
$hydraOperation['rdfs:label'] = $hydraOperation[$hydraPrefix.'title'];
328-
}
341+
$hydraOperation[$hydraPrefix.'method'] ??= $method;
342+
$hydraOperation[$hydraPrefix.'title'] ??= strtolower($method) . $shortName . ($operation instanceof CollectionOperationInterface ? 'Collection' : '');
329343

330344
ksort($hydraOperation);
331345

@@ -434,74 +448,15 @@ private function getClasses(array $entrypointProperties, array $classes, string
434448
$classes[] = [
435449
'@id' => '#Entrypoint',
436450
'@type' => $hydraPrefix.'Class',
437-
$hydraPrefix.'title' => 'The API entrypoint',
451+
$hydraPrefix.'title' => 'Entrypoint',
452+
$hydraPrefix.'description' => 'API Entrypoint',
438453
$hydraPrefix.'supportedProperty' => $entrypointProperties,
439454
$hydraPrefix.'supportedOperation' => [
440455
'@type' => $hydraPrefix.'Operation',
441456
$hydraPrefix.'method' => 'GET',
442-
'rdfs:label' => 'The API entrypoint.',
443-
'returns' => 'EntryPoint',
444-
],
445-
];
446-
447-
// Constraint violation
448-
$classes[] = [
449-
'@id' => '#ConstraintViolation',
450-
'@type' => $hydraPrefix.'Class',
451-
$hydraPrefix.'title' => 'A constraint violation',
452-
$hydraPrefix.'supportedProperty' => [
453-
[
454-
'@type' => $hydraPrefix.'SupportedProperty',
455-
$hydraPrefix.'property' => [
456-
'@id' => '#ConstraintViolation/propertyPath',
457-
'@type' => 'rdf:Property',
458-
'rdfs:label' => 'propertyPath',
459-
'domain' => '#ConstraintViolation',
460-
'range' => 'xmls:string',
461-
],
462-
$hydraPrefix.'title' => 'propertyPath',
463-
$hydraPrefix.'description' => 'The property path of the violation',
464-
$hydraPrefix.'readable' => true,
465-
$hydraPrefix.'writeable' => false,
466-
],
467-
[
468-
'@type' => $hydraPrefix.'SupportedProperty',
469-
$hydraPrefix.'property' => [
470-
'@id' => '#ConstraintViolation/message',
471-
'@type' => 'rdf:Property',
472-
'rdfs:label' => 'message',
473-
'domain' => '#ConstraintViolation',
474-
'range' => 'xmls:string',
475-
],
476-
$hydraPrefix.'title' => 'message',
477-
$hydraPrefix.'description' => 'The message associated with the violation',
478-
$hydraPrefix.'readable' => true,
479-
$hydraPrefix.'writeable' => false,
480-
],
481-
],
482-
];
483-
484-
// Constraint violation list
485-
$classes[] = [
486-
'@id' => '#ConstraintViolationList',
487-
'@type' => $hydraPrefix.'Class',
488-
'subClassOf' => $hydraPrefix.'Error',
489-
$hydraPrefix.'title' => 'A constraint violation list',
490-
$hydraPrefix.'supportedProperty' => [
491-
[
492-
'@type' => $hydraPrefix.'SupportedProperty',
493-
$hydraPrefix.'property' => [
494-
'@id' => '#ConstraintViolationList/violations',
495-
'@type' => 'rdf:Property',
496-
'rdfs:label' => 'violations',
497-
'domain' => '#ConstraintViolationList',
498-
'range' => '#ConstraintViolation',
499-
],
500-
$hydraPrefix.'title' => 'violations',
501-
$hydraPrefix.'description' => 'The violations',
502-
$hydraPrefix.'readable' => true,
503-
$hydraPrefix.'writeable' => false,
504-
],
457+
$hydraPrefix.'title' => 'Entrypoint',
458+
'rdfs:label' => 'index',
459+
$hydraPrefix.'returns' => 'Entrypoint',
505460
],
506461
];
507462

@@ -524,7 +479,7 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName
524479
$propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [
525480
'@id' => $iri,
526481
'@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property',
527-
'rdfs:label' => $propertyName,
482+
'title' => $propertyName,
528483
'domain' => $prefixedShortName,
529484
];
530485

src/Metadata/ApiProperty.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
*
2727
* @author Kévin Dunglas <dunglas@gmail.com>
2828
*/
29-
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS)]
29+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
3030
final class ApiProperty
3131
{
3232
private ?array $types;
@@ -216,6 +216,10 @@ public function __construct(
216216
private ?string $property = null,
217217
private ?string $policy = null,
218218
array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null,
219+
/**
220+
* Whether to document this property as a hydra:supportedProperty
221+
*/
222+
private ?bool $hydra = null,
219223
private array $extraProperties = [],
220224
) {
221225
$this->types = \is_string($types) ? (array) $types : $types;
@@ -517,7 +521,7 @@ public function withSchema(array $schema = []): self
517521
return $self;
518522
}
519523

520-
public function withInitializable(?bool $initializable): self
524+
public function withInitializable(bool $initializable): self
521525
{
522526
$self = clone $this;
523527
$self->initializable = $initializable;
@@ -626,4 +630,17 @@ public function withSerialize(array|Context|Groups|Ignore|SerializedName|Seriali
626630

627631
return $self;
628632
}
633+
634+
public function getHydra(): ?bool
635+
{
636+
return $this->hydra;
637+
}
638+
639+
public function withHydra(bool $hydra): static
640+
{
641+
$self = clone $this;
642+
$self->hydra = $hydra;
643+
644+
return $self;
645+
}
629646
}

src/Metadata/Error.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public function __construct(
9494
$provider = null,
9595
$processor = null,
9696
?OptionsInterface $stateOptions = null,
97+
?bool $hideHydraOperation = null,
9798
array $extraProperties = [],
9899
) {
99100
parent::__construct(
@@ -169,6 +170,7 @@ class: $class,
169170
provider: $provider,
170171
processor: $processor,
171172
stateOptions: $stateOptions,
173+
hideHydraOperation: $hideHydraOperation,
172174
extraProperties: $extraProperties,
173175
);
174176
}

src/State/ApiResource/Error.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
name: '_api_errors_problem',
3434
routeName: 'api_errors',
3535
outputFormats: ['json' => ['application/problem+json']],
36+
hideHydraOperation: true,
3637
normalizationContext: [
3738
'groups' => ['jsonproblem'],
3839
'skip_null_values' => true,
@@ -51,6 +52,7 @@
5152
new Operation(
5253
name: '_api_errors_jsonapi',
5354
routeName: 'api_errors',
55+
hideHydraOperation: true,
5456
outputFormats: ['jsonapi' => ['application/vnd.api+json']],
5557
normalizationContext: [
5658
'groups' => ['jsonapi'],
@@ -59,18 +61,21 @@
5961
),
6062
new Operation(
6163
name: '_api_errors',
62-
routeName: 'api_errors'
64+
routeName: 'api_errors',
65+
hideHydraOperation: true,
6366
),
6467
],
6568
provider: 'api_platform.state.error_provider',
6669
graphQlOperations: []
6770
)]
71+
#[ApiProperty(property: 'traceAsString', hydra: false)]
72+
#[ApiProperty(property: 'string', hydra: false)]
6873
class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface
6974
{
7075
public function __construct(
7176
private string $title,
7277
private string $detail,
73-
#[ApiProperty(identifier: true)] private int $status,
78+
#[ApiProperty(identifier: true, writable: false, initializable: false)] private int $status,
7479
?array $originalTrace = null,
7580
private ?string $instance = null,
7681
private string $type = 'about:blank',
@@ -98,9 +103,11 @@ public function getId(): string
98103

99104
#[SerializedName('trace')]
100105
#[Groups(['trace'])]
106+
#[ApiProperty(writable: false, initializable: false)]
101107
public ?array $originalTrace = null;
102108

103109
#[Groups(['jsonld'])]
110+
#[ApiProperty(writable: false, initializable: false)]
104111
public function getDescription(): ?string
105112
{
106113
return $this->detail;
@@ -121,7 +128,6 @@ public function getHeaders(): array
121128
}
122129

123130
#[Ignore]
124-
#[ApiProperty(readable: false)]
125131
public function getStatusCode(): int
126132
{
127133
return $this->status;
@@ -136,6 +142,7 @@ public function setHeaders(array $headers): void
136142
}
137143

138144
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
145+
#[ApiProperty(writable: false, initializable: false)]
139146
public function getType(): string
140147
{
141148
return $this->type;
@@ -147,6 +154,7 @@ public function setType(string $type): void
147154
}
148155

149156
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
157+
#[ApiProperty(writable: false, initializable: false)]
150158
public function getTitle(): ?string
151159
{
152160
return $this->title;
@@ -169,6 +177,7 @@ public function setStatus(int $status): void
169177
}
170178

171179
#[Groups(['jsonld', 'jsonproblem', 'jsonapi'])]
180+
#[ApiProperty(writable: false, initializable: false)]
172181
public function getDetail(): ?string
173182
{
174183
return $this->detail;
@@ -180,6 +189,7 @@ public function setDetail(?string $detail = null): void
180189
}
181190

182191
#[Groups(['jsonld', 'jsonproblem'])]
192+
#[ApiProperty(writable: false, initializable: false)]
183193
public function getInstance(): ?string
184194
{
185195
return $this->instance;

0 commit comments

Comments
 (0)