Skip to content

Commit 3aecd5c

Browse files
committed
Merge 4.2
2 parents 4a72073 + 6561eb1 commit 3aecd5c

File tree

18 files changed

+503
-22
lines changed

18 files changed

+503
-22
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
* [21aa2572d](https://github.com/api-platform/core/commit/21aa2572d8fef2b3f05f7307c51348a6c9767e45) feat(elasticsearch): add SSL options for Elasticsearch configuration (#4059)
88
* [ba0c76c](https://github.com/api-platform/core/commit/ba0c76c6f5c8afa8622e87a155b8b99f453d6453) feat(doctrine): remove put & path for readonly entity (#7019)
99

10+
## v4.2.9
11+
12+
### Bug fixes
13+
14+
* [502c0e2de](https://github.com/api-platform/core/commit/502c0e2def2c9021851bce799432429217839c7c) fix(symfony): skip argument resolver when context is not api platform (#7579)
15+
* [834064fc6](https://github.com/api-platform/core/commit/834064fc6c9dbe5492f5d2667b6cfba4de8e5e1a) fix(metadata): filter interface context php doc (#7560)
16+
* [85458e0d9](https://github.com/api-platform/core/commit/85458e0d935936b319df6572903dc40a763b6dc0) fix(symfony): use app.request.query.get() directly instead of app.request.get() in swagger (#7578)
17+
* [ef3127000](https://github.com/api-platform/core/commit/ef3127000ee1a6a3ca7282868dfc132a576827a3) fix(symfony): use current route for footer links (#7580)
18+
* [49d80c840](https://github.com/api-platform/core/commit/49d80c840524241f0403390cdc47a135cd6a0ec9) fix(serializer): render BCMath\Number (PHP 8.4+) as string instead of object (#7555)
19+
1020
## v4.2.7
1121

1222
**Symfony 8.0 compatible.**
@@ -373,6 +383,12 @@ TypeInfo:
373383
* [cff61eab8](https://github.com/api-platform/core/commit/cff61eab8643f8ed08d59c0684e77740d0d81b04) fix(metadata): append php file resource extractor (#7193)
374384
* [f3d4afe03](https://github.com/api-platform/core/commit/f3d4afe032385f3b665131a365e42706930f0730) fix(symfony): validator type-info
375385

386+
## v4.1.28
387+
388+
### Bug fixes
389+
390+
* [dfe42b385](https://github.com/api-platform/core/commit/dfe42b385cd6c14db862423fc75c731c11bfaf44) fix(symfony): skip argument resolver when context is not api platform
391+
376392
## v4.1.27
377393

378394
### Bug fixes

src/Laravel/Eloquent/Metadata/ModelMetadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function getAttributes(Model $model): array
7777
$indexes = $schema->getIndexes($table);
7878
$relations = $this->getRelations($model);
7979

80-
$foreignKeys = array_flip(array_column($relations, 'foreign_key'));
80+
$foreignKeys = array_flip(array_filter(array_column($relations, 'foreign_key')));
8181
$attributes = [];
8282

8383
foreach ($columns as $column) {

src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
143143
continue;
144144
}
145145

146+
if (!$parameter->getKey()) {
147+
$parameter = $parameter->withKey($key);
148+
}
149+
146150
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
147151
$parameter = $parameter->withProperties($propertyNames);
148152

@@ -170,7 +174,7 @@ private function getDefaultParameters(Operation $operation, string $resourceClas
170174
$parameter = $parameter->withProvider($f->getParameterProvider());
171175
}
172176

173-
$key = $parameter->getKey() ?? $key;
177+
$key = $parameter->getKey();
174178

175179
['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass, $parameter);
176180

src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTest.php

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
namespace ApiPlatform\Metadata\Tests\Resource\Factory;
1515

1616
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\ApiResource;
1718
use ApiPlatform\Metadata\FilterInterface;
19+
use ApiPlatform\Metadata\GetCollection;
1820
use ApiPlatform\Metadata\Parameters;
1921
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2022
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -36,8 +38,12 @@ public function testParameterFactory(): void
3638
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
3739
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
3840
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(
39-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true),
40-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true)
41+
new ApiProperty(identifier: true),
42+
new ApiProperty(readable: true),
43+
new ApiProperty(readable: true),
44+
new ApiProperty(identifier: true),
45+
new ApiProperty(readable: true),
46+
new ApiProperty(readable: true)
4147
);
4248
$filterLocator = $this->createStub(ContainerInterface::class);
4349
$filterLocator->method('has')->willReturn(true);
@@ -77,14 +83,71 @@ public function getDescription(string $resourceClass): array
7783
$this->assertNull($everywhere->getOpenApi());
7884
}
7985

86+
public function testQueryParameterWithPropertyPlaceholder(): void
87+
{
88+
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
89+
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'name', 'description']));
90+
91+
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
92+
$propertyMetadata->method('create')->willReturn(
93+
new ApiProperty(readable: true),
94+
);
95+
96+
$filterLocator = $this->createStub(ContainerInterface::class);
97+
$filterLocator->method('has')->willReturn(false); // No specific filter logic needed for this test
98+
99+
$parameterFactory = new ParameterResourceMetadataCollectionFactory(
100+
$nameCollection,
101+
$propertyMetadata,
102+
new AttributesResourceMetadataCollectionFactory(),
103+
$filterLocator
104+
);
105+
106+
$resourceMetadataCollection = $parameterFactory->create(HasParameterAttribute::class);
107+
$operation = $resourceMetadataCollection->getOperation(forceCollection: true);
108+
$parameters = $operation->getParameters();
109+
110+
$this->assertInstanceOf(Parameters::class, $parameters);
111+
112+
// Assert that the original parameter with ':property' is removed
113+
$this->assertFalse($parameters->has('search[:property]'));
114+
115+
// Assert that the new parameters are created and have the correct properties
116+
$this->assertTrue($parameters->has('search[name]'));
117+
$this->assertTrue($parameters->has('search[description]'));
118+
$this->assertTrue($parameters->has('static_param'));
119+
120+
$searchNameParam = $parameters->get('search[name]');
121+
$this->assertInstanceOf(QueryParameter::class, $searchNameParam);
122+
$this->assertSame('Search by property', $searchNameParam->getDescription());
123+
$this->assertSame('name', $searchNameParam->getProperty());
124+
$this->assertSame('search[name]', $searchNameParam->getKey());
125+
126+
$searchDescriptionParam = $parameters->get('search[description]');
127+
$this->assertInstanceOf(QueryParameter::class, $searchDescriptionParam);
128+
$this->assertSame('Search by property', $searchDescriptionParam->getDescription());
129+
$this->assertSame('description', $searchDescriptionParam->getProperty());
130+
$this->assertSame('search[description]', $searchDescriptionParam->getKey());
131+
132+
$staticParam = $parameters->get('static_param');
133+
$this->assertInstanceOf(QueryParameter::class, $staticParam);
134+
$this->assertSame('A static parameter', $staticParam->getDescription());
135+
$this->assertNull($staticParam->getProperty());
136+
$this->assertSame('static_param', $staticParam->getKey());
137+
}
138+
80139
public function testParameterFactoryNoFilter(): void
81140
{
82141
$nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class);
83142
$nameCollection->method('create')->willReturn(new PropertyNameCollection(['id', 'hydra', 'everywhere']));
84143
$propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class);
85144
$propertyMetadata->method('create')->willReturnOnConsecutiveCalls(
86-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true),
87-
new ApiProperty(identifier: true), new ApiProperty(readable: true), new ApiProperty(readable: true)
145+
new ApiProperty(identifier: true),
146+
new ApiProperty(readable: true),
147+
new ApiProperty(readable: true),
148+
new ApiProperty(identifier: true),
149+
new ApiProperty(readable: true),
150+
new ApiProperty(readable: true)
88151
);
89152
$filterLocator = $this->createStub(ContainerInterface::class);
90153
$filterLocator->method('has')->willReturn(false);
@@ -135,3 +198,25 @@ public function testParameterFactoryWithLimitedProperties(): void
135198
$this->assertSame(['name'], $param->getProperties());
136199
}
137200
}
201+
202+
#[ApiResource(
203+
operations: [
204+
new GetCollection(
205+
parameters: [
206+
'search[:property]' => new QueryParameter(
207+
description: 'Search by property',
208+
properties: ['name', 'description']
209+
),
210+
'static_param' => new QueryParameter(
211+
description: 'A static parameter'
212+
),
213+
]
214+
),
215+
]
216+
)]
217+
class HasParameterAttribute
218+
{
219+
public $id;
220+
public $name;
221+
public $description;
222+
}
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 <[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\State\Pagination;
15+
16+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
17+
18+
final class MappedObjectPaginator implements \IteratorAggregate, PaginatorInterface
19+
{
20+
public function __construct(
21+
private readonly iterable $entities,
22+
private readonly ObjectMapperInterface $mapper,
23+
private readonly string $resourceClass,
24+
private readonly float $totalItems = 0.0,
25+
private readonly float $currentPage = 1.0,
26+
private readonly float $lastPage = 1.0,
27+
private readonly float $itemsPerPage = 30.0,
28+
) {
29+
}
30+
31+
public function count(): int
32+
{
33+
return (int) $this->totalItems;
34+
}
35+
36+
public function getLastPage(): float
37+
{
38+
return $this->lastPage;
39+
}
40+
41+
public function getTotalItems(): float
42+
{
43+
return $this->totalItems;
44+
}
45+
46+
public function getCurrentPage(): float
47+
{
48+
return $this->currentPage;
49+
}
50+
51+
public function getItemsPerPage(): float
52+
{
53+
return $this->itemsPerPage;
54+
}
55+
56+
public function getIterator(): \Traversable
57+
{
58+
foreach ($this->entities as $entity) {
59+
yield $this->mapper->map($entity, $this->resourceClass);
60+
}
61+
}
62+
}

src/State/Provider/ObjectMapperProvider.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
use ApiPlatform\Metadata\Operation;
1717
use ApiPlatform\Metadata\Util\CloneTrait;
18-
use ApiPlatform\State\Pagination\ArrayPaginator;
18+
use ApiPlatform\State\Pagination\MappedObjectPaginator;
1919
use ApiPlatform\State\Pagination\PaginatorInterface;
2020
use ApiPlatform\State\ProviderInterface;
2121
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
@@ -39,6 +39,7 @@ public function __construct(
3939
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
4040
{
4141
$data = $this->decorated->provide($operation, $uriVariables, $context);
42+
$class = $operation->getOutput()['class'] ?? $operation->getClass();
4243

4344
if (!$this->objectMapper || !$operation->canMap()) {
4445
return $data;
@@ -52,15 +53,23 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5253
$request?->attributes->set('mapped_data', $data);
5354

5455
if ($data instanceof PaginatorInterface) {
55-
$data = new ArrayPaginator(array_map(fn ($v) => $this->objectMapper->map($v, $operation->getClass()), iterator_to_array($data)), 0, \count($data));
56+
$data = new MappedObjectPaginator(
57+
iterator_to_array($data),
58+
$this->objectMapper,
59+
$class,
60+
$data->getTotalItems(),
61+
$data->getCurrentPage(),
62+
$data->getLastPage(),
63+
$data->getItemsPerPage(),
64+
);
5665
} elseif (\is_array($data)) {
5766
foreach ($data as &$v) {
5867
if (\is_object($v)) {
59-
$v = $this->objectMapper->map($v, $operation->getClass());
68+
$v = $this->objectMapper->map($v, $class);
6069
}
6170
}
6271
} else {
63-
$data = $this->objectMapper->map($data, $operation->getClass());
72+
$data = $this->objectMapper->map($data, $class);
6473
}
6574

6675
$request?->attributes->set('data', $data);

src/Symfony/Action/DocumentationAction.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function __construct(
5050
?Negotiator $negotiator = null,
5151
private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']],
5252
private readonly bool $swaggerUiEnabled = true,
53+
private readonly bool $docsEnabled = true,
5354
) {
5455
$this->negotiator = $negotiator ?? new Negotiator();
5556
}
@@ -59,6 +60,10 @@ public function __construct(
5960
*/
6061
public function __invoke(?Request $request = null)
6162
{
63+
if (false === $this->docsEnabled) {
64+
throw new NotFoundHttpException();
65+
}
66+
6267
if (null === $request) {
6368
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version);
6469
}

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Doctrine\Persistence\ManagerRegistry;
5151
use PHPStan\PhpDocParser\Parser\PhpDocParser;
5252
use Ramsey\Uuid\Uuid;
53+
use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand;
5354
use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
5455
use Symfony\Component\Config\FileLocator;
5556
use Symfony\Component\Config\Resource\DirectoryResource;
@@ -177,7 +178,8 @@ public function load(array $configs, ContainerBuilder $container): void
177178
$this->registerLinkSecurityConfiguration($loader, $config);
178179
$this->registerJsonStreamerConfiguration($container, $loader, $formats, $config);
179180

180-
if (class_exists(ObjectMapper::class)) {
181+
// TranslationExtractCommand was introduced in framework-bundle/7.3 with the object mapper service
182+
if (class_exists(ObjectMapper::class) && class_exists(TranslationExtractCommand::class)) {
181183
$loader->load('state/object_mapper.php');
182184
}
183185
$container->registerForAutoconfiguration(FilterInterface::class)
@@ -587,6 +589,9 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array
587589
}
588590

589591
if (!$config['enable_swagger']) {
592+
$container->setParameter('api_platform.enable_swagger_ui', false);
593+
$container->setParameter('api_platform.enable_re_doc', false);
594+
590595
return;
591596
}
592597

src/Symfony/Bundle/Resources/config/state/object_mapper.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@
1818

1919
$services->set('api_platform.object_mapper.metadata_factory', 'Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory');
2020

21-
$services->set('api_platform.object_mapper', 'Symfony\Component\ObjectMapper\ObjectMapper')
22-
->args([
23-
service('api_platform.object_mapper.metadata_factory'),
24-
service('property_accessor')->nullOnInvalid(),
25-
tagged_locator('object_mapper.transform_callable'),
26-
tagged_locator('object_mapper.condition_callable'),
27-
]);
21+
$services->alias('api_platform.object_mapper', 'object_mapper');
2822

2923
$services->set('api_platform.object_mapper.relation', 'ApiPlatform\State\ObjectMapper\ObjectMapper')
3024
->decorate('api_platform.object_mapper', null, -255)

src/Symfony/Bundle/Resources/config/symfony/controller.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@
4848
service('api_platform.negotiator')->nullOnInvalid(),
4949
'%api_platform.docs_formats%',
5050
'%api_platform.enable_swagger_ui%',
51+
'%api_platform.enable_docs%',
5152
]);
5253
};

0 commit comments

Comments
 (0)