Skip to content

Commit 2082645

Browse files
committed
fix(SchemaFactory): add support for normalization/denormalization with attributes
1 parent ec91360 commit 2082645

File tree

4 files changed

+261
-1
lines changed

4 files changed

+261
-1
lines changed

src/JsonSchema/SchemaFactory.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,16 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
365365
$className = $valueType->getWrappedType()->getClassName();
366366
}
367367

368+
$childSerializerContext = $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true];
369+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
370+
$attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES];
371+
if (\is_array($attributes) && \array_key_exists($normalizedPropertyName, $attributes) && \is_array($attributes[$normalizedPropertyName])) {
372+
$childSerializerContext[AbstractNormalizer::ATTRIBUTES] = $attributes[$normalizedPropertyName];
373+
} else {
374+
unset($childSerializerContext[AbstractNormalizer::ATTRIBUTES]);
375+
}
376+
}
377+
368378
$subSchemaInstance = new Schema($version);
369379
$subSchemaInstance->setDefinitions($schema->getDefinitions());
370380
$subSchemaFactory = $this->schemaFactory ?: $this;
@@ -374,7 +384,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
374384
$parentType,
375385
null,
376386
$subSchemaInstance,
377-
$serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true],
387+
$childSerializerContext,
378388
false,
379389
);
380390
if (!isset($subSchemaResult['$ref'])) {
@@ -448,6 +458,18 @@ private function getFactoryOptions(array $serializerContext, array $validationGr
448458
$options['denormalization_groups'] = $denormalizationGroups;
449459
}
450460

461+
if (isset($serializerContext[AbstractNormalizer::ATTRIBUTES])) {
462+
$options['serializer_attributes'] = (array) $serializerContext[AbstractNormalizer::ATTRIBUTES];
463+
}
464+
465+
if ($operation && ($normalizationAttributes = $operation->getNormalizationContext()['attributes'] ?? null)) {
466+
$options['normalization_attributes'] = $normalizationAttributes;
467+
}
468+
469+
if ($operation && ($denormalizationAttributes = $operation->getDenormalizationContext()['attributes'] ?? null)) {
470+
$options['denormalization_attributes'] = $denormalizationAttributes;
471+
}
472+
451473
if ($validationGroups) {
452474
$options['validation_groups'] = $validationGroups;
453475
}
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+
}

src/JsonSchema/Tests/SchemaFactoryTest.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use ApiPlatform\JsonSchema\DefinitionNameFactory;
1717
use ApiPlatform\JsonSchema\Schema;
1818
use ApiPlatform\JsonSchema\SchemaFactory;
19+
use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\ChildAttributeDummy;
1920
use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\OverriddenOperationDummy;
21+
use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\ParentAttributeDummy;
2022
use ApiPlatform\JsonSchema\Tests\Fixtures\DummyResourceInterface;
2123
use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\GenderTypeEnum;
2224
use ApiPlatform\JsonSchema\Tests\Fixtures\GenericChild;
@@ -25,6 +27,7 @@
2527
use ApiPlatform\JsonSchema\Tests\Fixtures\Serializable;
2628
use ApiPlatform\Metadata\ApiProperty;
2729
use ApiPlatform\Metadata\ApiResource;
30+
use ApiPlatform\Metadata\Get;
2831
use ApiPlatform\Metadata\Operations;
2932
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
3033
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -531,6 +534,95 @@ public function testBuildSchemaWithSerializerGroups(): void
531534
$this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']);
532535
}
533536

537+
public function testBuildSchemaWithSerializerAttributes(): void
538+
{
539+
$shortName = (new \ReflectionClass(ParentAttributeDummy::class))->getShortName();
540+
$childShortName = (new \ReflectionClass(ChildAttributeDummy::class))->getShortName();
541+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
542+
$serializerAttributes = ['title', 'child' => ['name']];
543+
$operation = (new Get())->withName('get')->withNormalizationContext([
544+
'attributes' => $serializerAttributes,
545+
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
546+
])->withShortName($shortName);
547+
$resourceMetadataFactoryProphecy->create(ParentAttributeDummy::class)
548+
->willReturn(
549+
new ResourceMetadataCollection(ParentAttributeDummy::class, [
550+
(new ApiResource())->withOperations(new Operations(['get' => $operation])),
551+
])
552+
);
553+
$childOperation = (new Get())->withName('get')->withShortName($childShortName);
554+
$resourceMetadataFactoryProphecy->create(ChildAttributeDummy::class)
555+
->willReturn(
556+
new ResourceMetadataCollection(ChildAttributeDummy::class, [
557+
(new ApiResource())->withOperations(new Operations(['get' => $childOperation])),
558+
])
559+
);
560+
561+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
562+
$propertyNameCollectionFactoryProphecy->create(ParentAttributeDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['title', 'child']));
563+
$propertyNameCollectionFactoryProphecy->create(ChildAttributeDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['name']));
564+
565+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
566+
$propertyMetadataFactoryProphecy->create(ParentAttributeDummy::class, 'title', Argument::type('array'))->willReturn(
567+
(new ApiProperty())
568+
->withNativeType(Type::string())
569+
->withReadable(true)
570+
->withSchema(['type' => 'string'])
571+
);
572+
$propertyMetadataFactoryProphecy->create(ParentAttributeDummy::class, 'child', Argument::type('array'))->willReturn(
573+
(new ApiProperty())
574+
->withNativeType(Type::object(ChildAttributeDummy::class))
575+
->withReadable(true)
576+
->withSchema(['type' => Schema::UNKNOWN_TYPE])
577+
);
578+
$propertyMetadataFactoryProphecy->create(ChildAttributeDummy::class, 'name', Argument::type('array'))->willReturn(
579+
(new ApiProperty())
580+
->withNativeType(Type::string())
581+
->withReadable(true)
582+
->withSchema(['type' => 'string'])
583+
);
584+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
585+
$resourceClassResolverProphecy->isResourceClass(ParentAttributeDummy::class)->willReturn(true);
586+
$resourceClassResolverProphecy->isResourceClass(ChildAttributeDummy::class)->willReturn(true);
587+
588+
$definitionNameFactory = new DefinitionNameFactory();
589+
590+
$schemaFactory = new SchemaFactory(
591+
resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(),
592+
propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(),
593+
propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(),
594+
resourceClassResolver: $resourceClassResolverProphecy->reveal(),
595+
definitionNameFactory: $definitionNameFactory,
596+
);
597+
$resultSchema = $schemaFactory->buildSchema(ParentAttributeDummy::class, 'json', Schema::TYPE_OUTPUT, null, null, ['attributes' => $serializerAttributes, AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false]);
598+
599+
$rootDefinitionKey = $resultSchema->getRootDefinitionKey();
600+
$definitions = $resultSchema->getDefinitions();
601+
602+
$this->assertSame((new \ReflectionClass(ParentAttributeDummy::class))->getShortName().'-title_child.name', $rootDefinitionKey);
603+
$this->assertTrue(isset($definitions[$rootDefinitionKey]));
604+
$this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]);
605+
$this->assertSame('object', $definitions[$rootDefinitionKey]['type']);
606+
$this->assertFalse($definitions[$rootDefinitionKey]['additionalProperties']);
607+
$this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]);
608+
$this->assertArrayHasKey('title', $definitions[$rootDefinitionKey]['properties']);
609+
$this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['title']);
610+
$this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['title']['type']);
611+
$this->assertArrayHasKey('child', $definitions[$rootDefinitionKey]['properties']);
612+
$this->assertArrayHasKey('$ref', $definitions[$rootDefinitionKey]['properties']['child']);
613+
$this->assertSame('#/definitions/ChildAttributeDummy-name', $definitions[$rootDefinitionKey]['properties']['child']['$ref']);
614+
615+
$childDefinitionKey = 'ChildAttributeDummy-name';
616+
$this->assertTrue(isset($definitions[$childDefinitionKey]));
617+
$this->assertArrayHasKey('type', $definitions[$childDefinitionKey]);
618+
$this->assertSame('object', $definitions[$childDefinitionKey]['type']);
619+
$this->assertFalse($definitions[$childDefinitionKey]['additionalProperties']);
620+
$this->assertArrayHasKey('properties', $definitions[$childDefinitionKey]);
621+
$this->assertArrayHasKey('name', $definitions[$childDefinitionKey]['properties']);
622+
$this->assertArrayHasKey('type', $definitions[$childDefinitionKey]['properties']['name']);
623+
$this->assertSame('string', $definitions[$childDefinitionKey]['properties']['name']['type']);
624+
}
625+
534626
#[IgnoreDeprecations]
535627
public function testBuildSchemaForAssociativeArrayLegacy(): void
536628
{

0 commit comments

Comments
 (0)