Skip to content

Commit 9941b65

Browse files
authored
Merge pull request #8401 from soyuka/mergeup-4.4-main-round2
Merge 4.4 into main
2 parents 7823146 + f7ee06d commit 9941b65

23 files changed

Lines changed: 475 additions & 4 deletions

File tree

src/Hydra/State/JsonStreamerProcessor.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public function __construct(
5959
private readonly string $enabledParameterName = 'pagination',
6060
private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH,
6161
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
62+
private readonly bool $enableHeadRequestOptimization = true,
6263
) {
6364
$this->resourceClassResolver = $resourceClassResolver;
6465
$this->iriConverter = $iriConverter;
@@ -79,6 +80,16 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
7980
return $this->processor?->process($data, $operation, $uriVariables, $context);
8081
}
8182

83+
if ($this->enableHeadRequestOptimization && $request->isMethod('HEAD')) {
84+
$response = new Response(
85+
null,
86+
$this->getStatus($request, $operation, $context),
87+
$this->getHeaders($request, $operation, $context)
88+
);
89+
90+
return $this->processor ? $this->processor->process($response, $operation, $uriVariables, $context) : $response;
91+
}
92+
8293
if ($operation instanceof CollectionOperationInterface) {
8394
$requestUri = $request->getRequestUri() ?? '';
8495
$collection = new Collection();

src/Laravel/ApiPlatformProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ public function register(): void
553553
});
554554

555555
$this->app->singleton(SerializeProcessor::class, static function (Application $app) {
556-
return new SerializeProcessor($app->make(RespondProcessor::class), $app->make(Serializer::class), $app->make(SerializerContextBuilderInterface::class));
556+
return new SerializeProcessor($app->make(RespondProcessor::class), $app->make(Serializer::class), $app->make(SerializerContextBuilderInterface::class), $app['config']->get('api-platform.enable_head_request_optimization', true));
557557
});
558558

559559
$this->app->singleton(WriteProcessor::class, static function (Application $app) {

src/Laravel/config/api-platform.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
// on PATCH operations, allowing partial updates without requiring all fields.
5050
'partial_patch_validation' => false,
5151

52+
// When true (default), HEAD requests skip response body construction so
53+
// collections are not iterated. Set to false to process HEAD like GET.
54+
'enable_head_request_optimization' => true,
55+
5256
'docs_formats' => [
5357
'jsonld' => ['application/ld+json'],
5458
// 'jsonapi' => ['application/vnd.api+json'],

src/Serializer/State/JsonStreamerProcessor.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?ResourceClassResolverInterface $resourceClassResolver = null,
4949
?OperationMetadataFactoryInterface $operationMetadataFactory = null,
5050
?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null,
51+
private readonly bool $enableHeadRequestOptimization = true,
5152
) {
5253
$this->resourceClassResolver = $resourceClassResolver;
5354
$this->iriConverter = $iriConverter;
@@ -68,6 +69,16 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
6869
return $this->processor?->process($data, $operation, $uriVariables, $context);
6970
}
7071

72+
if ($this->enableHeadRequestOptimization && $request->isMethod('HEAD')) {
73+
$response = new Response(
74+
null,
75+
$this->getStatus($request, $operation, $context),
76+
$this->getHeaders($request, $operation, $context)
77+
);
78+
79+
return $this->processor ? $this->processor->process($response, $operation, $uriVariables, $context) : $response;
80+
}
81+
7182
if ($operation instanceof CollectionOperationInterface) {
7283
$data = $this->jsonStreamer->write(
7384
$data,

src/State/Processor/SerializeProcessor.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function __construct(
4646
private readonly ?ProcessorInterface $processor,
4747
private readonly SerializerInterface $serializer,
4848
private readonly SerializerContextBuilderInterface $serializerContextBuilder,
49+
private readonly bool $enableHeadRequestOptimization = true,
4950
) {
5051
}
5152

@@ -60,6 +61,12 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
6061
// @see ApiPlatform\State\Processor\RespondProcessor
6162
$context['original_data'] = $data;
6263

64+
if ($this->enableHeadRequestOptimization && $request->isMethod('HEAD')) {
65+
$this->stopwatch?->stop('api_platform.processor.serialize');
66+
67+
return $this->processor?->process(null, $operation, $uriVariables, $context);
68+
}
69+
6370
$class = $operation->getClass();
6471
$serializerContext = $this->serializerContextBuilder->createFromRequest($request, true, [
6572
'resource_class' => $class,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\State\Tests\Processor;
15+
16+
use ApiPlatform\Metadata\Get;
17+
use ApiPlatform\State\Processor\SerializeProcessor;
18+
use ApiPlatform\State\ProcessorInterface;
19+
use ApiPlatform\State\SerializerContextBuilderInterface;
20+
use PHPUnit\Framework\TestCase;
21+
use Symfony\Component\HttpFoundation\Request;
22+
use Symfony\Component\Serializer\SerializerInterface;
23+
24+
class SerializeProcessorTest extends TestCase
25+
{
26+
public function testHeadRequestSkipsSerializationAndForwardsNull(): void
27+
{
28+
$request = Request::create('/foos', 'HEAD');
29+
30+
$serializer = $this->createMock(SerializerInterface::class);
31+
$serializer->expects($this->never())->method('serialize');
32+
33+
$inner = $this->createMock(ProcessorInterface::class);
34+
$inner->expects($this->once())
35+
->method('process')
36+
->with($this->isNull())
37+
->willReturn(null);
38+
39+
$processor = new SerializeProcessor($inner, $serializer, $this->createStub(SerializerContextBuilderInterface::class));
40+
$operation = (new Get())->withSerialize(true);
41+
42+
$this->assertNull($processor->process(new \stdClass(), $operation, [], ['request' => $request]));
43+
}
44+
45+
public function testHeadRequestSerializesWhenOptimizationDisabled(): void
46+
{
47+
$request = Request::create('/foos', 'HEAD');
48+
49+
$serializer = $this->createMock(SerializerInterface::class);
50+
$serializer->expects($this->once())->method('serialize')->willReturn('');
51+
52+
$inner = $this->createMock(ProcessorInterface::class);
53+
$inner->method('process')->willReturn('forwarded');
54+
55+
$contextBuilder = $this->createStub(SerializerContextBuilderInterface::class);
56+
$contextBuilder->method('createFromRequest')->willReturn([]);
57+
58+
$processor = new SerializeProcessor($inner, $serializer, $contextBuilder, false);
59+
$operation = (new Get())->withSerialize(true);
60+
61+
$this->assertSame('forwarded', $processor->process(new \stdClass(), $operation, [], ['request' => $request]));
62+
}
63+
}

src/State/Util/HttpResponseHeadersTrait.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ private function addLinkedDataPlatformHeaders(array &$headers, HttpOperation $op
155155
}
156156

157157
$acceptPost = null;
158-
$allowedMethods = ['OPTIONS', 'HEAD'];
158+
$allowedMethods = [];
159159
$resourceCollection = $this->resourceMetadataCollectionFactory->create($operation->getClass());
160160
foreach ($resourceCollection as $resource) {
161161
foreach ($resource->getOperations() as $op) {
@@ -172,6 +172,7 @@ private function addLinkedDataPlatformHeaders(array &$headers, HttpOperation $op
172172
$headers['Accept-Post'] = $acceptPost;
173173
}
174174

175-
$headers['Allow'] = implode(', ', $allowedMethods);
175+
$head = \in_array('GET', $allowedMethods, true) ? ['HEAD'] : [];
176+
$headers['Allow'] = implode(', ', array_merge(['OPTIONS'], $head, $allowedMethods));
176177
}
177178
}

src/State/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"symfony/web-link": "^7.4 || ^8.0",
4646
"willdurand/negotiation": "^3.1"
4747
},
48-
"conflicts": {
48+
"conflict": {
4949
"symfony/object-mapper": "<7.3.4"
5050
},
5151
"autoload": {

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
359359

360360
$container->setParameter('api_platform.enable_entrypoint', $config['enable_entrypoint']);
361361
$container->setParameter('api_platform.enable_docs', $config['enable_docs']);
362+
$container->setParameter('api_platform.enable_head_request_optimization', $config['enable_head_request_optimization']);
362363
$container->setParameter('api_platform.title', $config['title']);
363364
$container->setParameter('api_platform.description', $config['description']);
364365
$container->setParameter('api_platform.version', $config['version']);

src/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public function getConfigTreeBuilder(): TreeBuilder
126126
->booleanNode('enable_scalar')->defaultValue(class_exists(TwigBundle::class))->info('Enable Scalar API Reference')->end()
127127
->booleanNode('enable_entrypoint')->defaultTrue()->info('Enable the entrypoint')->end()
128128
->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end()
129+
->booleanNode('enable_head_request_optimization')->defaultTrue()->info('Skip response body construction on HEAD requests so collections are not iterated. Disable to process HEAD identically to GET.')->end()
129130
->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end()
130131
->booleanNode('enable_phpdoc_parser')->defaultTrue()->info('Enable resource metadata collector using PHPStan PhpDocParser.')->end()
131132
->arrayNode('collection')

0 commit comments

Comments
 (0)