Skip to content

Commit 374c61d

Browse files
committed
Merge 4.3
2 parents 67b1c51 + 30f1f97 commit 374c61d

15 files changed

Lines changed: 212 additions & 74 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ jobs:
224224
composer global config allow-plugins.soyuka/pmu true --no-interaction
225225
composer global link .
226226
- name: Setup Node.js
227-
uses: actions/setup-node@v4
227+
uses: actions/setup-node@v6
228228
with:
229229
node-version: '20'
230230
- name: Install Redocly CLI

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## v4.3.7
4+
5+
### Bug fixes
6+
7+
* [90ae5142b](https://github.com/api-platform/core/commit/90ae5142b38a9aee78cd5dcbd787181af42b6359) fix(openapi): include jsonapi collection schema (#8190)
8+
* [ab8bfd529](https://github.com/api-platform/core/commit/ab8bfd5296711b6db882919a4baa259ced65fe9a) fix(symfony): isolate api_platform.property_info tags (#8206)
9+
310
## v4.3.6
411

512
### Bug fixes

phpstan.neon.dist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,13 @@ parameters:
182182
identifier: method.notFound
183183
paths:
184184
- tests/Functional/JsonStreamerTest.php
185+
186+
# Symfony Console 7.4 added Application::addCommand(); 8.1 removed add(). One branch is unreachable depending on installed version.
187+
-
188+
message: '#Call to an undefined method Symfony\\Component\\Console\\Application::(add|addCommand)\(\)\.#'
189+
path: tests/Symfony/Bundle/Command/DebugResourceCommandTest.php
190+
reportUnmatched: false
191+
-
192+
identifier: function.alreadyNarrowedType
193+
path: tests/Symfony/Bundle/Command/DebugResourceCommandTest.php
194+
reportUnmatched: false

src/HttpCache/Tests/State/AddTagsProcessorTest.php

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,65 +22,48 @@
2222
use ApiPlatform\Metadata\UrlGeneratorInterface;
2323
use ApiPlatform\State\ProcessorInterface;
2424
use PHPUnit\Framework\TestCase;
25-
use Symfony\Component\HttpFoundation\ParameterBag;
2625
use Symfony\Component\HttpFoundation\Request;
2726
use Symfony\Component\HttpFoundation\Response;
28-
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
2927

3028
class AddTagsProcessorTest extends TestCase
3129
{
3230
public function testAddTags(): void
3331
{
3432
$operation = new Get();
35-
$response = $this->createMock(Response::class);
36-
$response->method('isCacheable')->willReturn(true);
37-
$response->headers = $this->createMock(ResponseHeaderBag::class);
38-
$response->headers->expects($this->once())->method('set')->with('Cache-Tags', 'a,b');
39-
$request = $this->createMock(Request::class);
40-
$request->method('isMethodCacheable')->willReturn(true);
41-
$request->attributes = $this->createMock(ParameterBag::class);
42-
$request->attributes->method('get')->with('_resources', [])->willReturn(['a', 'b']);
33+
$response = new Response('', 200, ['Cache-Control' => 'public, max-age=10']);
34+
$request = new Request(attributes: ['_resources' => ['a', 'b']]);
4335
$context = ['request' => $request];
4436
$decorated = $this->createMock(ProcessorInterface::class);
4537
$decorated->method('process')->willReturn($response);
4638
$iriConverter = $this->createMock(IriConverterInterface::class);
4739
$iriConverter->expects($this->never())->method('getIriFromResource');
4840
$processor = new AddTagsProcessor($decorated, $iriConverter);
4941
$processor->process($response, $operation, [], $context);
42+
43+
$this->assertSame('a,b', $response->headers->get('Cache-Tags'));
5044
}
5145

5246
public function testAddTagsCollection(): void
5347
{
5448
$operation = new GetCollection(class: \stdClass::class, uriVariables: ['id' => new Link()]);
55-
$response = $this->createMock(Response::class);
56-
$response->method('isCacheable')->willReturn(true);
57-
$response->headers = $this->createMock(ResponseHeaderBag::class);
58-
$response->headers->expects($this->once())->method('set')->with('Cache-Tags', 'a,b,/foos/1/bars');
59-
$request = $this->createMock(Request::class);
60-
$request->method('isMethodCacheable')->willReturn(true);
61-
$request->attributes = $this->createMock(ParameterBag::class);
62-
$request->attributes->method('get')->with('_resources', [])->willReturn(['a', 'b']);
63-
$request->attributes->method('all')->willReturn(['id' => 1]);
49+
$response = new Response('', 200, ['Cache-Control' => 'public, max-age=10']);
50+
$request = new Request(attributes: ['_resources' => ['a', 'b'], 'id' => 1]);
6451
$context = ['request' => $request];
6552
$decorated = $this->createMock(ProcessorInterface::class);
6653
$decorated->method('process')->willReturn($response);
6754
$iriConverter = $this->createMock(IriConverterInterface::class);
6855
$iriConverter->expects($this->once())->method('getIriFromResource')->with(\stdClass::class, UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => 1]])->willReturn('/foos/1/bars');
6956
$processor = new AddTagsProcessor($decorated, $iriConverter);
7057
$processor->process($response, $operation, [], $context);
58+
59+
$this->assertSame('a,b,/foos/1/bars', $response->headers->get('Cache-Tags'));
7160
}
7261

7362
public function testAddTagsWithPurger(): void
7463
{
7564
$operation = new Get();
76-
$response = $this->createMock(Response::class);
77-
$response->method('isCacheable')->willReturn(true);
78-
$response->headers = $this->createMock(ResponseHeaderBag::class);
79-
$response->headers->expects($this->once())->method('set')->with('Cache-Tags', 'a,b');
80-
$request = $this->createMock(Request::class);
81-
$request->method('isMethodCacheable')->willReturn(true);
82-
$request->attributes = $this->createMock(ParameterBag::class);
83-
$request->attributes->method('get')->with('_resources', [])->willReturn(['a', 'b']);
65+
$response = new Response('', 200, ['Cache-Control' => 'public, max-age=10']);
66+
$request = new Request(attributes: ['_resources' => ['a', 'b']]);
8467
$context = ['request' => $request];
8568
$decorated = $this->createMock(ProcessorInterface::class);
8669
$decorated->method('process')->willReturn($response);
@@ -90,5 +73,7 @@ public function testAddTagsWithPurger(): void
9073
$purger->expects($this->once())->method('getResponseHeaders')->willReturn(['Cache-Tags' => 'a,b']);
9174
$processor = new AddTagsProcessor($decorated, $iriConverter, $purger);
9275
$processor->process($response, $operation, [], $context);
76+
77+
$this->assertSame('a,b', $response->headers->get('Cache-Tags'));
9378
}
9479
}

src/Hydra/Tests/State/HydraLinkProcessorTest.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
use ApiPlatform\Metadata\UrlGeneratorInterface;
2020
use ApiPlatform\State\ProcessorInterface;
2121
use PHPUnit\Framework\TestCase;
22-
use Symfony\Component\HttpFoundation\ParameterBag;
2322
use Symfony\Component\HttpFoundation\Request;
2423
use Symfony\Component\WebLink\GenericLinkProvider;
2524
use Symfony\Component\WebLink\Link;
@@ -30,19 +29,16 @@ public function testProcess(): void
3029
{
3130
$data = new \stdClass();
3231
$operation = new Get(links: [new Link('a', 'b')]);
33-
$request = $this->createMock(Request::class);
34-
$request->attributes = $this->createMock(ParameterBag::class);
32+
$request = new Request();
3533

36-
$request->attributes->expects($this->once())->method('set')->with('_api_platform_links', $this->callback(function ($linkProvider) {
37-
$this->assertInstanceOf(GenericLinkProvider::class, $linkProvider);
38-
$this->assertEquals($linkProvider->getLinks(), [new Link('a', 'b'), new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', '/docs')]);
39-
40-
return true;
41-
}));
4234
$context = ['request' => $request];
4335
$decorated = $this->createMock(ProcessorInterface::class);
4436
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
4537
$urlGenerator->expects($this->once())->method('generate')->with('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL)->willReturn('/docs');
4638
(new HydraLinkProcessor($decorated, $urlGenerator))->process($data, $operation, [], $context);
39+
40+
$linkProvider = $request->attributes->get('_api_platform_links');
41+
$this->assertInstanceOf(GenericLinkProvider::class, $linkProvider);
42+
$this->assertEquals([new Link('a', 'b'), new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', '/docs')], $linkProvider->getLinks());
4743
}
4844
}

src/JsonApi/Tests/State/JsonApiProviderTest.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,22 @@
1717
use ApiPlatform\Metadata\Get;
1818
use ApiPlatform\State\ProviderInterface;
1919
use PHPUnit\Framework\TestCase;
20-
use Symfony\Component\HttpFoundation\InputBag;
21-
use Symfony\Component\HttpFoundation\ParameterBag;
2220
use Symfony\Component\HttpFoundation\Request;
2321

2422
class JsonApiProviderTest extends TestCase
2523
{
2624
public function testProvide(): void
2725
{
28-
$request = $this->createMock(Request::class);
29-
$request->expects($this->once())->method('getRequestFormat')->willReturn('jsonapi');
30-
$request->attributes = $this->createMock(ParameterBag::class);
31-
$request->attributes->expects($this->once())->method('get')->with('_api_filters', [])->willReturn([]);
32-
$request->attributes->method('set')->with($this->logicalOr('_api_filter_property', '_api_included', '_api_filters'), $this->logicalOr(['id', 'name', 'dummyFloat', 'relatedDummy' => ['id', 'name']], ['relatedDummy'], []));
33-
$request->query = new InputBag(['fields' => ['dummy' => 'id,name,dummyFloat', 'relatedDummy' => 'id,name'], 'include' => 'relatedDummy,foo']);
26+
$request = new Request(query: ['fields' => ['dummy' => 'id,name,dummyFloat', 'relatedDummy' => 'id,name'], 'include' => 'relatedDummy,foo']);
27+
$request->setRequestFormat('jsonapi');
3428
$operation = new Get(class: \stdClass::class, shortName: 'dummy');
3529
$context = ['request' => $request];
3630
$decorated = $this->createMock(ProviderInterface::class);
3731
$provider = new JsonApiProvider($decorated);
3832
$provider->provide($operation, [], $context);
33+
34+
$this->assertSame(['id', 'name', 'dummyFloat', 'relatedDummy' => ['id', 'name']], $request->attributes->get('_api_filter_property'));
35+
$this->assertSame(['relatedDummy'], $request->attributes->get('_api_included'));
3936
}
4037

4138
public function testProvideMergesFlatPaginationWithBracketFilter(): void

src/State/Tests/Provider/SecurityParameterProviderTest.php

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
use ApiPlatform\State\Provider\SecurityParameterProvider;
2020
use ApiPlatform\State\ProviderInterface;
2121
use PHPUnit\Framework\TestCase;
22-
use Symfony\Component\HttpFoundation\ParameterBag;
2322
use Symfony\Component\HttpFoundation\Request;
2423
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
2524

@@ -34,10 +33,7 @@ public function testIsGrantedLink(): void
3433
], class: \stdClass::class);
3534
$decorated = $this->createMock(ProviderInterface::class);
3635
$decorated->method('provide')->willReturn($obj);
37-
$request = $this->createMock(Request::class);
38-
$parameterBag = new ParameterBag();
39-
$request->attributes = $parameterBag;
40-
$request->attributes->set('bar', $barObj);
36+
$request = new Request(attributes: ['bar' => $barObj]);
4137
$resourceAccessChecker = $this->createMock(ResourceAccessCheckerInterface::class);
4238
$resourceAccessChecker->expects($this->once())->method('isGranted')->with('Bar', 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(true);
4339
$accessChecker = new SecurityParameterProvider($decorated, $resourceAccessChecker);
@@ -55,10 +51,7 @@ public function testIsNotGrantedLink(): void
5551
], class: \stdClass::class);
5652
$decorated = $this->createMock(ProviderInterface::class);
5753
$decorated->method('provide')->willReturn($obj);
58-
$request = $this->createMock(Request::class);
59-
$parameterBag = new ParameterBag();
60-
$request->attributes = $parameterBag;
61-
$request->attributes->set('bar', $barObj);
54+
$request = new Request(attributes: ['bar' => $barObj]);
6255
$resourceAccessChecker = $this->createMock(ResourceAccessCheckerInterface::class);
6356
$resourceAccessChecker->expects($this->once())->method('isGranted')->with('Bar', 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(false);
6457
$accessChecker = new SecurityParameterProvider($decorated, $resourceAccessChecker);
@@ -77,10 +70,7 @@ public function testSecurityMessageLink(): void
7770
], class: \stdClass::class);
7871
$decorated = $this->createMock(ProviderInterface::class);
7972
$decorated->method('provide')->willReturn($obj);
80-
$request = $this->createMock(Request::class);
81-
$parameterBag = new ParameterBag();
82-
$request->attributes = $parameterBag;
83-
$request->attributes->set('bar', $barObj);
73+
$request = new Request(attributes: ['bar' => $barObj]);
8474
$resourceAccessChecker = $this->createMock(ResourceAccessCheckerInterface::class);
8575
$resourceAccessChecker->expects($this->once())->method('isGranted')->with('Bar', 'is_granted("some_voter", "bar")', ['object' => $obj, 'previous_object' => null, 'request' => $request, 'bar' => $barObj, 'barId' => 1, 'operation' => $operation])->willReturn(false);
8676
$accessChecker = new SecurityParameterProvider($decorated, $resourceAccessChecker);

src/Symfony/Bundle/ApiPlatformBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\JsonStreamerTransformerPass;
2525
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
2626
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MutatorPass;
27+
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\PropertyInfoTagPass;
2728
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass;
2829
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass;
2930
use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass;
@@ -60,6 +61,7 @@ public function build(ContainerBuilder $container): void
6061
$container->addCompilerPass(new AuthenticatorManagerPass());
6162
$container->addCompilerPass(new SerializerMappingLoaderPass());
6263
$container->addCompilerPass(new MutatorPass());
64+
$container->addCompilerPass(new PropertyInfoTagPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -100);
6365
// Must run after Symfony's TransformerPass so we can rely on the value_object_transformer tag being processed.
6466
$container->addCompilerPass(new JsonStreamerTransformerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10);
6567
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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\Symfony\Bundle\DependencyInjection\Compiler;
15+
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
19+
/**
20+
* Bridges Symfony's public `property_info.*` tags to API Platform's private
21+
* `api_platform.property_info.*` tags so that `api_platform.property_info`
22+
* inherits framework- and third-party-registered extractors (e.g. Doctrine's
23+
* `DoctrineExtractor`) without API Platform's own extractors leaking back into
24+
* Symfony's `property_info` service.
25+
*
26+
* @internal
27+
*
28+
* @see https://github.com/api-platform/core/issues/8201
29+
*/
30+
final class PropertyInfoTagPass implements CompilerPassInterface
31+
{
32+
private const TAG_SUFFIXES = [
33+
'list_extractor',
34+
'type_extractor',
35+
'description_extractor',
36+
'access_extractor',
37+
'initializable_extractor',
38+
];
39+
40+
public function process(ContainerBuilder $container): void
41+
{
42+
foreach (self::TAG_SUFFIXES as $suffix) {
43+
$publicTag = 'property_info.'.$suffix;
44+
$privateTag = 'api_platform.property_info.'.$suffix;
45+
46+
foreach ($container->findTaggedServiceIds($publicTag) as $serviceId => $tags) {
47+
$definition = $container->getDefinition($serviceId);
48+
if ($definition->hasTag($privateTag)) {
49+
continue;
50+
}
51+
foreach ($tags as $attributes) {
52+
$definition->addTag($privateTag, $attributes);
53+
}
54+
}
55+
}
56+
}
57+
}

src/Symfony/Bundle/Resources/config/api.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,29 +78,29 @@
7878
$services->alias('api_platform.property_accessor', 'property_accessor');
7979

8080
$services->set('api_platform.property_info.reflection_extractor', ReflectionExtractor::class)
81-
->tag('property_info.list_extractor', ['priority' => -1000])
82-
->tag('property_info.type_extractor', ['priority' => -1002])
83-
->tag('property_info.access_extractor', ['priority' => -1000])
84-
->tag('property_info.initializable_extractor', ['priority' => -1000]);
81+
->tag('api_platform.property_info.list_extractor', ['priority' => -1000])
82+
->tag('api_platform.property_info.type_extractor', ['priority' => -1002])
83+
->tag('api_platform.property_info.access_extractor', ['priority' => -1000])
84+
->tag('api_platform.property_info.initializable_extractor', ['priority' => -1000]);
8585

8686
if (class_exists(DocBlockFactory::class)) {
8787
$services->set('api_platform.property_info.php_doc_extractor', PhpDocExtractor::class)
88-
->tag('property_info.description_extractor', ['priority' => -1000])
89-
->tag('property_info.type_extractor', ['priority' => -1001]);
88+
->tag('api_platform.property_info.description_extractor', ['priority' => -1000])
89+
->tag('api_platform.property_info.type_extractor', ['priority' => -1001]);
9090
}
9191

9292
if (class_exists(PhpDocParser::class) && class_exists(ContextFactory::class)) {
9393
$services->set('api_platform.property_info.phpstan_extractor', PhpStanExtractor::class)
94-
->tag('property_info.type_extractor', ['priority' => -1000]);
94+
->tag('api_platform.property_info.type_extractor', ['priority' => -1000]);
9595
}
9696

9797
$services->set('api_platform.property_info', PropertyInfoExtractor::class)
9898
->args([
99-
tagged_iterator('property_info.list_extractor'),
100-
tagged_iterator('property_info.type_extractor'),
101-
tagged_iterator('property_info.description_extractor'),
102-
tagged_iterator('property_info.access_extractor'),
103-
tagged_iterator('property_info.initializable_extractor'),
99+
tagged_iterator('api_platform.property_info.list_extractor'),
100+
tagged_iterator('api_platform.property_info.type_extractor'),
101+
tagged_iterator('api_platform.property_info.description_extractor'),
102+
tagged_iterator('api_platform.property_info.access_extractor'),
103+
tagged_iterator('api_platform.property_info.initializable_extractor'),
104104
]);
105105

106106
$services->set('api_platform.property_info.cache', PropertyInfoCacheExtractor::class)

0 commit comments

Comments
 (0)