Skip to content

Commit c5767c9

Browse files
committed
Merge 3.2 into main
2 parents 304951a + 29844f1 commit c5767c9

File tree

18 files changed

+262
-22
lines changed

18 files changed

+262
-22
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Feature: Update properties of a resource that are inherited with standard PUT operation
2+
3+
@!mongodb
4+
@createSchema
5+
Scenario: Update properties of a resource that are inherited with standard PUT operation
6+
Given there is a dummy entity with a mapped superclass
7+
When I add "Content-Type" header equal to "application/ld+json"
8+
And I send a "PUT" request to "/dummy_mapped_subclasses/1" with body:
9+
"""
10+
{
11+
"foo": "updated value"
12+
}
13+
"""
14+
Then the response status code should be 200
15+
And the response should be in JSON
16+
And the JSON should be equal to:
17+
"""
18+
{
19+
"@context": "/contexts/DummyMappedSubclass",
20+
"@id": "/dummy_mapped_subclasses/1",
21+
"@type": "DummyMappedSubclass",
22+
"id": 1,
23+
"foo": "updated value"
24+
}
25+
"""

src/Doctrine/Common/State/PersistProcessor.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,15 @@ private function isDeferredExplicit(DoctrineObjectManager $manager, $data): bool
128128
private function getReflectionProperties(mixed $data): array
129129
{
130130
$ret = [];
131-
$props = (new \ReflectionObject($data))->getProperties(~\ReflectionProperty::IS_STATIC);
131+
$r = new \ReflectionObject($data);
132132

133-
foreach ($props as $prop) {
134-
$ret[$prop->getName()] = $prop;
135-
}
133+
do {
134+
$props = $r->getProperties(~\ReflectionProperty::IS_STATIC);
135+
136+
foreach ($props as $prop) {
137+
$ret[$prop->getName()] = $prop;
138+
}
139+
} while ($r = $r->getParentClass());
136140

137141
return $ret;
138142
}

src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
final class ElasticsearchProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface
2929
{
30-
public function __construct(private readonly Client|null $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true) // @phpstan-ignore-line
30+
public function __construct(private readonly ?Client $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true) // @phpstan-ignore-line
3131
{
3232
}
3333

src/GraphQl/State/Processor/NormalizeProcessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function __construct(private readonly NormalizerInterface $normalizer, pr
4040
{
4141
}
4242

43-
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): array|null
43+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ?array
4444
{
4545
if (!$operation instanceof GraphQlOperation) {
4646
return $data;

src/JsonSchema/BackwardCompatibleSchemaFactory.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,15 @@ public function buildSchema(string $className, string $format = 'json', string $
4343

4444
foreach ($schema->getDefinitions() as $definition) {
4545
foreach ($definition['properties'] ?? [] as $property) {
46-
if (isset($property['type']) && \in_array($property['type'], ['integer', 'number'], true)) {
46+
if (!isset($property['type'])) {
47+
continue;
48+
}
49+
50+
foreach ((array) $property['type'] as $type) {
51+
if ('integer' !== $type && 'number' !== $type) {
52+
continue;
53+
}
54+
4755
if (isset($property['exclusiveMinimum'])) {
4856
$property['minimum'] = $property['exclusiveMinimum'];
4957
$property['exclusiveMinimum'] = true;
@@ -52,6 +60,8 @@ public function buildSchema(string $className, string $format = 'json', string $
5260
$property['maximum'] = $property['exclusiveMaximum'];
5361
$property['exclusiveMaximum'] = true;
5462
}
63+
64+
break;
5565
}
5666
}
5767
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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;
15+
16+
use ApiPlatform\JsonSchema\BackwardCompatibleSchemaFactory;
17+
use ApiPlatform\JsonSchema\Schema;
18+
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
19+
use PHPUnit\Framework\TestCase;
20+
21+
class BackwardCompatibleSchemaFactoryTest extends TestCase
22+
{
23+
public function testWithSingleType(): void
24+
{
25+
$schema = new Schema();
26+
$schema->setDefinitions(new \ArrayObject([
27+
'a' => new \ArrayObject([
28+
'properties' => new \ArrayObject([
29+
'foo' => new \ArrayObject(['type' => 'integer', 'exclusiveMinimum' => 0, 'exclusiveMaximum' => 1]),
30+
]),
31+
]),
32+
]));
33+
$schemaFactory = $this->createMock(SchemaFactoryInterface::class);
34+
$schemaFactory->method('buildSchema')->willReturn($schema);
35+
$schemaFactory = new BackwardCompatibleSchemaFactory($schemaFactory);
36+
$schema = $schemaFactory->buildSchema('a', serializerContext: [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);
37+
$schema = $schema->getDefinitions()['a'];
38+
39+
$this->assertTrue($schema['properties']['foo']['exclusiveMinimum']);
40+
$this->assertTrue($schema['properties']['foo']['exclusiveMaximum']);
41+
$this->assertEquals($schema['properties']['foo']['minimum'], 0);
42+
$this->assertEquals($schema['properties']['foo']['maximum'], 1);
43+
}
44+
45+
public function testWithMultipleType(): void
46+
{
47+
$schema = new Schema();
48+
$schema->setDefinitions(new \ArrayObject([
49+
'a' => new \ArrayObject([
50+
'properties' => new \ArrayObject([
51+
'foo' => new \ArrayObject(['type' => ['number', 'null'], 'exclusiveMinimum' => 0, 'exclusiveMaximum' => 1]),
52+
]),
53+
]),
54+
]));
55+
$schemaFactory = $this->createMock(SchemaFactoryInterface::class);
56+
$schemaFactory->method('buildSchema')->willReturn($schema);
57+
$schemaFactory = new BackwardCompatibleSchemaFactory($schemaFactory);
58+
$schema = $schemaFactory->buildSchema('a', serializerContext: [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);
59+
$schema = $schema->getDefinitions()['a'];
60+
61+
$this->assertTrue($schema['properties']['foo']['exclusiveMinimum']);
62+
$this->assertTrue($schema['properties']['foo']['exclusiveMaximum']);
63+
$this->assertEquals($schema['properties']['foo']['minimum'], 0);
64+
$this->assertEquals($schema['properties']['foo']['maximum'], 1);
65+
}
66+
67+
public function testWithoutNumber(): void
68+
{
69+
$schema = new Schema();
70+
$schema->setDefinitions(new \ArrayObject([
71+
'a' => new \ArrayObject([
72+
'properties' => new \ArrayObject([
73+
'foo' => new \ArrayObject(['type' => ['string', 'null'], 'exclusiveMinimum' => 0, 'exclusiveMaximum' => 1]),
74+
]),
75+
]),
76+
]));
77+
$schemaFactory = $this->createMock(SchemaFactoryInterface::class);
78+
$schemaFactory->method('buildSchema')->willReturn($schema);
79+
$schemaFactory = new BackwardCompatibleSchemaFactory($schemaFactory);
80+
$schema = $schemaFactory->buildSchema('a', serializerContext: [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => true]);
81+
$schema = $schema->getDefinitions()['a'];
82+
83+
$this->assertEquals($schema['properties']['foo']['exclusiveMinimum'], 0);
84+
$this->assertEquals($schema['properties']['foo']['exclusiveMaximum'], 1);
85+
}
86+
87+
public function testWithoutFlag(): void
88+
{
89+
$schema = new Schema();
90+
$schema->setDefinitions(new \ArrayObject([
91+
'a' => new \ArrayObject([
92+
'properties' => new \ArrayObject([
93+
'foo' => new \ArrayObject(['type' => ['string', 'null'], 'exclusiveMinimum' => 0, 'exclusiveMaximum' => 1]),
94+
]),
95+
]),
96+
]));
97+
$schemaFactory = $this->createMock(SchemaFactoryInterface::class);
98+
$schemaFactory->method('buildSchema')->willReturn($schema);
99+
$schemaFactory = new BackwardCompatibleSchemaFactory($schemaFactory);
100+
$schema = $schemaFactory->buildSchema('a', serializerContext: [BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION => false]);
101+
$schema = $schema->getDefinitions()['a'];
102+
103+
$this->assertEquals($schema['properties']['foo']['exclusiveMinimum'], 0);
104+
$this->assertEquals($schema['properties']['foo']['exclusiveMaximum'], 1);
105+
}
106+
}

src/Metadata/Tests/Fixtures/ApiResource/DummyCar.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ class DummyCar
3232
#[Serializer\Groups(['colors'])]
3333
private iterable $colors;
3434
#[Serializer\Groups(['colors'])]
35-
private iterable|null $secondColors = null;
35+
private ?iterable $secondColors = null;
3636
#[Serializer\Groups(['colors'])]
37-
private iterable|null $thirdColors = null;
37+
private ?iterable $thirdColors = null;
3838
#[Serializer\Groups(['colors'])]
39-
private iterable|null $uuid = null;
39+
private ?iterable $uuid = null;
4040

4141
private string $name;
4242
private bool $canSell;

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
265265
continue;
266266
}
267267

268-
$parameter = new Parameter($parameterName, 'path', (new \ReflectionClass($uriVariable->getFromClass()))->getShortName().' identifier', true, false, false, ['type' => 'string']);
268+
$parameter = new Parameter($parameterName, 'path', "$resourceShortName identifier", true, false, false, ['type' => 'string']);
269269
if ($this->hasParameter($openapiOperation, $parameter)) {
270270
continue;
271271
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Symfony\Bundle\DependencyInjection;
1515

16+
use ApiPlatform\Api\FilterInterface as LegacyFilterInterface;
1617
use ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface;
1718
use ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface;
1819
use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter as DoctrineMongoDbOdmAbstractFilter;
@@ -174,6 +175,8 @@ public function load(array $configs, ContainerBuilder $container): void
174175

175176
$container->registerForAutoconfiguration(FilterInterface::class)
176177
->addTag('api_platform.filter');
178+
$container->registerForAutoconfiguration(LegacyFilterInterface::class)
179+
->addTag('api_platform.filter');
177180
$container->registerForAutoconfiguration(ProviderInterface::class)
178181
->addTag('api_platform.state_provider');
179182
$container->registerForAutoconfiguration(ProcessorInterface::class)

src/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ private function getPropertyConstraints(
193193
*/
194194
private function isRequired(Constraint $constraint): bool
195195
{
196+
if ($constraint instanceof NotBlank && $constraint->allowNull) {
197+
return false;
198+
}
199+
196200
foreach (self::REQUIRED_CONSTRAINTS as $requiredConstraint) {
197201
if ($constraint instanceof $requiredConstraint) {
198202
return true;

src/Util/AttributesExtractor.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ private function __construct()
3232
*/
3333
public static function extractAttributes(array $attributes): array
3434
{
35-
if (($attributes['_api_operation'] ?? null) && !isset($attributes['_api_resource_class'])) {
36-
$attributes['_api_resource_class'] = $attributes['_api_operation']->getClass();
37-
}
38-
3935
$result = ['resource_class' => $attributes['_api_resource_class'] ?? null, 'has_composite_identifier' => $attributes['_api_has_composite_identifier'] ?? false];
4036

4137
if (null === $result['resource_class']) {

tests/Behat/DoctrineContext.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
133133
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
134134
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate;
135+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMappedSubclass;
135136
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMercure;
136137
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyOffer;
137138
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyPassenger;
@@ -2285,6 +2286,16 @@ public function thereAreAFewLinkHandledDummies(): void
22852286
$this->manager->flush();
22862287
}
22872288

2289+
/**
2290+
* @Given there is a dummy entity with a mapped superclass
2291+
*/
2292+
public function thereIsADummyEntityWithAMappedSuperclass(): void
2293+
{
2294+
$entity = new DummyMappedSubclass();
2295+
$this->manager->persist($entity);
2296+
$this->manager->flush();
2297+
}
2298+
22882299
private function isOrm(): bool
22892300
{
22902301
return null !== $this->schemaTool;

tests/Fixtures/DummyValidatedEntity.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class DummyValidatedEntity
3939
* @var string
4040
*/
4141
#[Assert\Email]
42+
#[Assert\NotBlank(allowNull: true)]
4243
public $dummyEmail;
4344

4445
/**
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Metadata\Put;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
#[ORM\Entity]
20+
#[Put(
21+
extraProperties: [
22+
'standard_put' => true,
23+
],
24+
allowCreate: true
25+
)]
26+
class DummyMappedSubclass extends DummyMappedSuperclass
27+
{
28+
#[ORM\Id]
29+
#[ORM\GeneratedValue]
30+
#[ORM\Column]
31+
private ?int $id = null;
32+
33+
public function getId(): ?int
34+
{
35+
return $this->id;
36+
}
37+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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\Tests\Fixtures\TestBundle\Entity;
15+
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
#[ORM\MappedSuperclass]
19+
class DummyMappedSuperclass
20+
{
21+
#[ORM\Column(length: 255, nullable: true)]
22+
private ?string $foo = null;
23+
24+
public function getFoo(): ?string
25+
{
26+
return $this->foo;
27+
}
28+
29+
public function setFoo(?string $foo): static
30+
{
31+
$this->foo = $foo;
32+
33+
return $this;
34+
}
35+
}

tests/Fixtures/TestBundle/Entity/Issue5662/Book.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public static function getData(Operation $operation, array $uriVariables = [], a
3737
return [new self('a', 'hello'), new self('b', 'you')];
3838
}
3939

40-
public static function getDatum(Operation $operation, array $uriVariables = [], array $context = []): self|null
40+
public static function getDatum(Operation $operation, array $uriVariables = [], array $context = []): ?self
4141
{
4242
$id = $uriVariables['id'];
4343
foreach (static::getData($operation, $uriVariables, $context) as $datum) {

tests/Fixtures/TestBundle/Entity/Issue5662/Review.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public static function getData(Operation $operation, array $uriVariables = [], a
7575
];
7676
}
7777

78-
public static function getDatum(Operation $operation, array $uriVariables = [], array $context = []): self|null
78+
public static function getDatum(Operation $operation, array $uriVariables = [], array $context = []): ?self
7979
{
8080
$id = (int) $uriVariables['id'];
8181
foreach (static::getData($operation, $uriVariables, $context) as $datum) {

0 commit comments

Comments
 (0)