Skip to content

Commit 5b4e851

Browse files
committed
fix(jsonschema): add support for normalization/denormalization with attributes
1 parent 6e83dd9 commit 5b4e851

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
@@ -244,14 +244,24 @@ private function buildLegacyPropertySchema(Schema $schema, string $definitionNam
244244
continue;
245245
}
246246

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

@@ -365,6 +375,16 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
365375
$className = $valueType->getWrappedType()->getClassName();
366376
}
367377

378+
$childSerializerContext = $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true];
379+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
380+
$attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES];
381+
if (\is_array($attributes) && \array_key_exists($normalizedPropertyName, $attributes) && \is_array($attributes[$normalizedPropertyName])) {
382+
$childSerializerContext[AbstractNormalizer::ATTRIBUTES] = $attributes[$normalizedPropertyName];
383+
} else {
384+
unset($childSerializerContext[AbstractNormalizer::ATTRIBUTES]);
385+
}
386+
}
387+
368388
$subSchemaInstance = new Schema($version);
369389
$subSchemaInstance->setDefinitions($schema->getDefinitions());
370390
$subSchemaFactory = $this->schemaFactory ?: $this;
@@ -374,7 +394,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
374394
$parentType,
375395
null,
376396
$subSchemaInstance,
377-
$serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true],
397+
$childSerializerContext,
378398
false,
379399
);
380400
if (!isset($subSchemaResult['$ref'])) {
@@ -448,6 +468,18 @@ private function getFactoryOptions(array $serializerContext, array $validationGr
448468
$options['denormalization_groups'] = $denormalizationGroups;
449469
}
450470

471+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
472+
$options['serializer_attributes'] = (array) $serializerContext[AbstractNormalizer::ATTRIBUTES];
473+
}
474+
475+
if ($operation && ($normalizationAttributes = $operation->getNormalizationContext()['attributes'] ?? null)) {
476+
$options['normalization_attributes'] = $normalizationAttributes;
477+
}
478+
479+
if ($operation && ($denormalizationAttributes = $operation->getDenormalizationContext()['attributes'] ?? null)) {
480+
$options['denormalization_attributes'] = $denormalizationAttributes;
481+
}
482+
451483
if ($validationGroups) {
452484
$options['validation_groups'] = $validationGroups;
453485
}

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)