Skip to content

Commit 64b46b2

Browse files
authored
feat(jsonschema): support for normalization/denormalization with attributes (#7629)
1 parent 6edee72 commit 64b46b2

File tree

11 files changed

+650
-21
lines changed

11 files changed

+650
-21
lines changed

src/JsonSchema/DefinitionNameFactory.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,19 @@ public function create(string $className, string $format = 'json', ?string $inpu
6363
$name = \sprintf('%s%s', $prefix, $definitionName ? '-'.$definitionName : $definitionName);
6464
} else {
6565
$groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []);
66-
$name = $groups ? \sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix;
66+
$attributes = (array) ($serializerContext[AbstractNormalizer::ATTRIBUTES] ?? []);
67+
68+
$parts = [];
69+
70+
if ($groups) {
71+
$parts[] = implode('_', $groups);
72+
}
73+
74+
if ($attributes) {
75+
$parts[] = $this->getAttributesAsString($attributes);
76+
}
77+
78+
$name = $parts ? \sprintf('%s-%s', $prefix, implode('_', $parts)) : $prefix;
6779
}
6880

6981
if (false === ($serializerContext['gen_id'] ?? true)) {
@@ -99,4 +111,26 @@ private function createPrefixFromClass(string $fullyQualifiedClassName, int $nam
99111

100112
return $name;
101113
}
114+
115+
private function getAttributesAsString(array $attributes): string
116+
{
117+
$parts = [];
118+
119+
foreach ($attributes as $key => $value) {
120+
if (\is_array($value)) {
121+
$childString = $this->getAttributesAsString($value);
122+
$children = explode('_', $childString);
123+
124+
foreach ($children as $child) {
125+
$parts[] = $key.'.'.$child;
126+
}
127+
} elseif (\is_string($key)) {
128+
$parts[] = $key;
129+
} else {
130+
$parts[] = $value;
131+
}
132+
}
133+
134+
return implode('_', $parts);
135+
}
102136
}

src/JsonSchema/SchemaFactory.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,24 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
245245
continue;
246246
}
247247

248+
$childSerializerContext = $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true];
249+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
250+
$attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES];
251+
if (\is_array($attributes) && \array_key_exists($normalizedPropertyName, $attributes) && \is_array($attributes[$normalizedPropertyName])) {
252+
$childSerializerContext[AbstractNormalizer::ATTRIBUTES] = $attributes[$normalizedPropertyName];
253+
} else {
254+
unset($childSerializerContext[AbstractNormalizer::ATTRIBUTES]);
255+
}
256+
}
257+
248258
$subSchemaFactory = $this->schemaFactory ?: $this;
249259
$subSchema = $subSchemaFactory->buildSchema(
250260
$className,
251261
$format,
252262
$parentType,
253263
null,
254264
$subSchema,
255-
$serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true],
265+
$childSerializerContext,
256266
false,
257267
);
258268

@@ -427,6 +437,16 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
427437
continue;
428438
}
429439

440+
$childSerializerContext = $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true];
441+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
442+
$attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES];
443+
if (\is_array($attributes) && \array_key_exists($normalizedPropertyName, $attributes) && \is_array($attributes[$normalizedPropertyName])) {
444+
$childSerializerContext[AbstractNormalizer::ATTRIBUTES] = $attributes[$normalizedPropertyName];
445+
} else {
446+
unset($childSerializerContext[AbstractNormalizer::ATTRIBUTES]);
447+
}
448+
}
449+
430450
$subSchemaInstance = new Schema($version);
431451
$subSchemaInstance->setDefinitions($schema->getDefinitions());
432452
$subSchemaFactory = $this->schemaFactory ?: $this;
@@ -436,7 +456,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
436456
$parentType,
437457
null,
438458
$subSchemaInstance,
439-
$serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true],
459+
$childSerializerContext,
440460
false,
441461
);
442462
if (!isset($subSchemaResult['$ref'])) {
@@ -510,6 +530,18 @@ private function getFactoryOptions(array $serializerContext, array $validationGr
510530
$options['denormalization_groups'] = $denormalizationGroups;
511531
}
512532

533+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
534+
$options['serializer_attributes'] = (array) $serializerContext[AbstractNormalizer::ATTRIBUTES];
535+
}
536+
537+
if ($operation && ($normalizationAttributes = $operation->getNormalizationContext()['attributes'] ?? null)) {
538+
$options['normalization_attributes'] = $normalizationAttributes;
539+
}
540+
541+
if ($operation && ($denormalizationAttributes = $operation->getDenormalizationContext()['attributes'] ?? null)) {
542+
$options['denormalization_attributes'] = $denormalizationAttributes;
543+
}
544+
513545
if ($validationGroups) {
514546
$options['validation_groups'] = $validationGroups;
515547
}

src/JsonSchema/Tests/DefinitionNameFactoryTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,46 @@ public static function providerDefinitions(): iterable
7070
yield ['Bar.DtoOutput.jsonapi-read_write', Dummy::class, 'jsonapi', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]];
7171
yield ['Bar.DtoOutput.jsonhal-read_write', Dummy::class, 'jsonhal', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]];
7272
yield ['Bar.DtoOutput.jsonld-read_write', Dummy::class, 'jsonld', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]];
73+
74+
yield ['Dummy-id', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['id']]];
75+
yield ['Dummy.jsonapi-id', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['id']]];
76+
yield ['Dummy.jsonhal-id', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['id']]];
77+
yield ['Dummy.jsonld-id', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['id']]];
78+
79+
yield ['Dummy-id_name', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['id', 'name']]];
80+
yield ['Dummy.jsonapi-id_name', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['id', 'name']]];
81+
yield ['Dummy.jsonhal-id_name', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['id', 'name']]];
82+
yield ['Dummy.jsonld-id_name', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['id', 'name']]];
83+
84+
yield ['Dummy-title_author.name', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name']]]];
85+
yield ['Dummy.jsonapi-title_author.name', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name']]]];
86+
yield ['Dummy.jsonhal-title_author.name', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name']]]];
87+
yield ['Dummy.jsonld-title_author.name', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name']]]];
88+
89+
yield ['Dummy-title_author', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => 'name']]];
90+
yield ['Dummy.jsonapi-title_author', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => 'name']]];
91+
yield ['Dummy.jsonhal-title_author', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => 'name']]];
92+
yield ['Dummy.jsonld-title_author', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => 'name']]];
93+
94+
yield ['Dummy-title_author_name', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author', 'name']]];
95+
yield ['Dummy.jsonapi-title_author_name', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author', 'name']]];
96+
yield ['Dummy.jsonhal-title_author_name', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author', 'name']]];
97+
yield ['Dummy.jsonld-title_author_name', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author', 'name']]];
98+
99+
yield ['Dummy-title_author.name_name', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
100+
yield ['Dummy.jsonapi-title_author.name_name', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
101+
yield ['Dummy.jsonhal-title_author.name_name', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
102+
yield ['Dummy.jsonld-title_author.name_name', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
103+
104+
yield ['Dummy-title_author.name_author.id_name', Dummy::class, 'json', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name', 'id'], 'name']]];
105+
yield ['Dummy.jsonapi-title_author.name_author.id_name', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name', 'id'], 'name']]];
106+
yield ['Dummy.jsonhal-title_author.name_author.id_name', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name', 'id'], 'name']]];
107+
yield ['Dummy.jsonld-title_author.name_author.id_name', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name', 'id'], 'name']]];
108+
109+
yield ['Bar.DtoOutput-title_author.name_name', Dummy::class, 'json', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
110+
yield ['Bar.DtoOutput.jsonapi-title_author.name_name', Dummy::class, 'jsonapi', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
111+
yield ['Bar.DtoOutput.jsonhal-title_author.name_name', Dummy::class, 'jsonhal', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
112+
yield ['Bar.DtoOutput.jsonld-title_author.name_name', Dummy::class, 'jsonld', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::ATTRIBUTES => ['title', 'author' => ['name'], 'name']]];
73113
}
74114

75115
#[\PHPUnit\Framework\Attributes\DataProvider('providerDefinitions')]
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\JsonSchema\Tests\Fixtures\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
#[ORM\Entity]
20+
#[ApiResource]
21+
class ChildAttributeDummy
22+
{
23+
#[ORM\Id]
24+
#[ORM\GeneratedValue]
25+
#[ORM\Column]
26+
private ?int $id = null;
27+
28+
#[ORM\Column(length: 255)]
29+
private ?string $name = null;
30+
31+
#[ORM\Column(length: 50)]
32+
private ?string $code = null;
33+
34+
public function getId(): ?int
35+
{
36+
return $this->id;
37+
}
38+
39+
public function getName(): ?string
40+
{
41+
return $this->name;
42+
}
43+
44+
public function setName(string $name): self
45+
{
46+
$this->name = $name;
47+
48+
return $this;
49+
}
50+
51+
public function getCode(): ?string
52+
{
53+
return $this->code;
54+
}
55+
56+
public function setCode(string $code): self
57+
{
58+
$this->code = $code;
59+
60+
return $this;
61+
}
62+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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\JsonSchema\Tests\Fixtures\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use Doctrine\ORM\Mapping as ORM;
19+
20+
#[ORM\Entity]
21+
#[ApiResource(
22+
operations: [
23+
new Get(
24+
normalizationContext: ['attributes' => ['title', 'child' => ['name']]]
25+
),
26+
]
27+
)]
28+
class ParentAttributeDummy
29+
{
30+
#[ORM\Id]
31+
#[ORM\GeneratedValue]
32+
#[ORM\Column]
33+
private ?int $id = null;
34+
35+
#[ORM\Column(length: 255)]
36+
private ?string $title = null;
37+
38+
#[ORM\Column(type: 'text')]
39+
private ?string $description = null;
40+
41+
#[ORM\ManyToOne(targetEntity: ChildAttributeDummy::class)]
42+
private ?ChildAttributeDummy $child = null;
43+
44+
public function getId(): ?int
45+
{
46+
return $this->id;
47+
}
48+
49+
public function getTitle(): ?string
50+
{
51+
return $this->title;
52+
}
53+
54+
public function setTitle(string $title): self
55+
{
56+
$this->title = $title;
57+
58+
return $this;
59+
}
60+
61+
public function getDescription(): ?string
62+
{
63+
return $this->description;
64+
}
65+
66+
public function setDescription(string $description): self
67+
{
68+
$this->description = $description;
69+
70+
return $this;
71+
}
72+
73+
public function getChild(): ?ChildAttributeDummy
74+
{
75+
return $this->child;
76+
}
77+
78+
public function setChild(?ChildAttributeDummy $child): self
79+
{
80+
$this->child = $child;
81+
82+
return $this;
83+
}
84+
}

0 commit comments

Comments
 (0)