Skip to content

Commit 3766572

Browse files
committed
Merge branch '7.1' into 7.2
* 7.1: Fix resolve enum in string type resolver [PropertyInfo] Upmerge symfony#59012 [BeanstalkMessenger] Round delay to an integer to avoid deprecation warning [PropertyInfo] Fix interface handling in `PhpStanTypeHelper` [HttpClient] Test POST to GET redirects [HttpKernel] Denormalize request data using the csv format when using "#[MapQueryString]" or "#[MapRequestPayload]" (except for content data) fix: preserve and nowrap in profiler code highlighting
2 parents fb5a0f3 + 36bb990 commit 3766572

File tree

13 files changed

+282
-19
lines changed

13 files changed

+282
-19
lines changed

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#source .source-content ol li {
4141
margin: 0 0 2px 0;
4242
padding-left: 5px;
43+
white-space: preserve nowrap;
4344
}
4445
#source .source-content ol li::marker {
4546
color: var(--color-muted);

src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,4 +678,26 @@ public function testHeadRequestWithClosureBody()
678678
$this->assertIsArray($vars);
679679
$this->assertSame('HEAD', $vars['REQUEST_METHOD']);
680680
}
681+
682+
/**
683+
* @testWith [301]
684+
* [302]
685+
* [303]
686+
*/
687+
public function testPostToGetRedirect(int $status)
688+
{
689+
$p = TestHttpServer::start(8067);
690+
691+
try {
692+
$client = $this->getHttpClient(__FUNCTION__);
693+
694+
$response = $client->request('POST', 'http://localhost:8057/custom?status=' . $status . '&headers[]=Location%3A%20%2F');
695+
$body = $response->toArray();
696+
} finally {
697+
$p->stop();
698+
}
699+
700+
$this->assertSame('GET', $body['REQUEST_METHOD']);
701+
$this->assertSame('/', $body['REQUEST_URI']);
702+
}
681703
}

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,9 @@
4646
class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface
4747
{
4848
/**
49-
* @see \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT
5049
* @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS
5150
*/
5251
private const CONTEXT_DENORMALIZE = [
53-
'disable_type_enforcement' => true,
5452
'collect_denormalization_errors' => true,
5553
];
5654

@@ -190,7 +188,7 @@ private function mapQueryString(Request $request, ArgumentMetadata $argument, Ma
190188
return null;
191189
}
192190

193-
return $this->serializer->denormalize($data, $argument->getType(), null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
191+
return $this->serializer->denormalize($data, $argument->getType(), 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
194192
}
195193

196194
private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null
@@ -210,7 +208,7 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
210208
}
211209

212210
if ($data = $request->request->all()) {
213-
return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
211+
return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
214212
}
215213

216214
if ('' === ($data = $request->getContent()) && ($argument->isNullable() || $argument->hasDefaultValue())) {

src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\HttpKernel\Exception\HttpException;
2323
use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException;
2424
use Symfony\Component\HttpKernel\HttpKernelInterface;
25+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
2526
use Symfony\Component\Serializer\Encoder\JsonEncoder;
2627
use Symfony\Component\Serializer\Encoder\XmlEncoder;
2728
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -395,6 +396,38 @@ public function testQueryStringValidationPassed()
395396
$this->assertEquals([$payload], $event->getArguments());
396397
}
397398

399+
public function testQueryStringParameterTypeMismatch()
400+
{
401+
$query = ['price' => 'not a float'];
402+
403+
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
404+
$serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]);
405+
406+
$validator = $this->createMock(ValidatorInterface::class);
407+
$validator->expects($this->never())->method('validate');
408+
409+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
410+
411+
$argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [
412+
MapQueryString::class => new MapQueryString(),
413+
]);
414+
415+
$request = Request::create('/', 'GET', $query);
416+
417+
$kernel = $this->createMock(HttpKernelInterface::class);
418+
$arguments = $resolver->resolve($request, $argument);
419+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
420+
421+
try {
422+
$resolver->onKernelControllerArguments($event);
423+
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
424+
} catch (HttpException $e) {
425+
$validationFailedException = $e->getPrevious();
426+
$this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
427+
$this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage());
428+
}
429+
}
430+
398431
public function testRequestInputValidationPassed()
399432
{
400433
$input = ['price' => '50'];
@@ -457,6 +490,38 @@ public function testRequestArrayDenormalization()
457490
$this->assertEquals([$payload], $event->getArguments());
458491
}
459492

493+
public function testRequestInputTypeMismatch()
494+
{
495+
$input = ['price' => 'not a float'];
496+
497+
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
498+
$serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]);
499+
500+
$validator = $this->createMock(ValidatorInterface::class);
501+
$validator->expects($this->never())->method('validate');
502+
503+
$resolver = new RequestPayloadValueResolver($serializer, $validator);
504+
505+
$argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [
506+
MapRequestPayload::class => new MapRequestPayload(),
507+
]);
508+
509+
$request = Request::create('/', 'POST', $input);
510+
511+
$kernel = $this->createMock(HttpKernelInterface::class);
512+
$arguments = $resolver->resolve($request, $argument);
513+
$event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
514+
515+
try {
516+
$resolver->onKernelControllerArguments($event);
517+
$this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
518+
} catch (HttpException $e) {
519+
$validationFailedException = $e->getPrevious();
520+
$this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
521+
$this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage());
522+
}
523+
}
524+
460525
public function testItThrowsOnMissingAttributeType()
461526
{
462527
$serializer = new Serializer();

src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,4 +363,25 @@ public function testKeepaliveWhenABeanstalkdExceptionOccurs()
363363
$this->expectExceptionObject(new TransportException($exception->getMessage(), 0, $exception));
364364
$connection->keepalive((string) $id);
365365
}
366+
367+
public function testSendWithRoundedDelay()
368+
{
369+
$tube = 'xyz';
370+
$body = 'foo';
371+
$headers = ['test' => 'bar'];
372+
$delay = 920;
373+
$expectedDelay = 0;
374+
375+
$client = $this->createMock(PheanstalkInterface::class);
376+
$client->expects($this->once())->method('useTube')->with($tube)->willReturn($client);
377+
$client->expects($this->once())->method('put')->with(
378+
$this->anything(),
379+
$this->anything(),
380+
$expectedDelay,
381+
$this->anything(),
382+
);
383+
384+
$connection = new Connection(['tube_name' => $tube], $client);
385+
$connection->send($body, $headers, $delay);
386+
}
366387
}

src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public function send(string $body, array $headers, int $delay = 0): string
124124
$job = $this->client->useTube($this->tube)->put(
125125
$message,
126126
PheanstalkInterface::DEFAULT_PRIORITY,
127-
$delay / 1000,
127+
(int) ($delay / 1000),
128128
$this->ttr
129129
);
130130
} catch (Exception $exception) {

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1616
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
17+
use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz;
1718
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy;
1819
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock;
1920
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
2021
use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback;
2122
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
2223
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection;
24+
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyGeneric;
2325
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace;
2426
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyPropertyAndGetterWithDifferentTypes;
2527
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType;
28+
use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace;
2629
use Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy;
2730
use Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy;
2831
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
@@ -552,6 +555,77 @@ public static function allowPrivateAccessLegacyProvider(): array
552555
];
553556
}
554557

558+
/**
559+
* @param list<LegacyType> $expectedTypes
560+
*
561+
* @dataProvider legacyGenericsProvider
562+
*/
563+
public function testGenericsLegacy(string $property, array $expectedTypes)
564+
{
565+
$this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property));
566+
}
567+
568+
/**
569+
* @return iterable<array{0: string, 1: list<LegacyType>}>
570+
*/
571+
public static function legacyGenericsProvider(): iterable
572+
{
573+
yield [
574+
'basicClass',
575+
[
576+
new LegacyType(
577+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
578+
class: Clazz::class,
579+
collectionValueType: new LegacyType(
580+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
581+
class: Dummy::class,
582+
)
583+
),
584+
],
585+
];
586+
yield [
587+
'nullableClass',
588+
[
589+
new LegacyType(
590+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
591+
class: Clazz::class,
592+
nullable: true,
593+
collectionValueType: new LegacyType(
594+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
595+
class: Dummy::class,
596+
)
597+
),
598+
],
599+
];
600+
yield [
601+
'basicInterface',
602+
[
603+
new LegacyType(
604+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
605+
class: IFace::class,
606+
collectionValueType: new LegacyType(
607+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
608+
class: Dummy::class,
609+
)
610+
),
611+
],
612+
];
613+
yield [
614+
'nullableInterface',
615+
[
616+
new LegacyType(
617+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
618+
class: IFace::class,
619+
nullable: true,
620+
collectionValueType: new LegacyType(
621+
builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
622+
class: Dummy::class,
623+
)
624+
),
625+
],
626+
];
627+
}
628+
555629
/**
556630
* @dataProvider typesProvider
557631
*/
@@ -968,7 +1042,41 @@ public static function allowPrivateAccessProvider(): array
9681042

9691043
public function testGenericInterface()
9701044
{
971-
$this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface'));
1045+
$this->assertEquals(
1046+
Type::generic(Type::object(\BackedEnum::class), Type::string()),
1047+
$this->extractor->getType(Dummy::class, 'genericInterface'),
1048+
);
1049+
}
1050+
1051+
/**
1052+
* @dataProvider genericsProvider
1053+
*/
1054+
public function testGenerics(string $property, Type $expectedType)
1055+
{
1056+
$this->assertEquals($expectedType, $this->extractor->getType(DummyGeneric::class, $property));
1057+
}
1058+
1059+
/**
1060+
* @return iterable<array{0: string, 1: Type}>
1061+
*/
1062+
public static function genericsProvider(): iterable
1063+
{
1064+
yield [
1065+
'basicClass',
1066+
Type::generic(Type::object(Clazz::class), Type::object(Dummy::class)),
1067+
];
1068+
yield [
1069+
'nullableClass',
1070+
Type::nullable(Type::generic(Type::object(Clazz::class), Type::object(Dummy::class))),
1071+
];
1072+
yield [
1073+
'basicInterface',
1074+
Type::generic(Type::object(IFace::class), Type::object(Dummy::class)),
1075+
];
1076+
yield [
1077+
'nullableInterface',
1078+
Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))),
1079+
];
9721080
}
9731081
}
9741082

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
13+
14+
interface IFace {}
15+
16+
class Clazz {}
17+
18+
class DummyGeneric
19+
{
20+
21+
/**
22+
* @var Clazz<Dummy>
23+
*/
24+
public $basicClass;
25+
26+
/**
27+
* @var ?Clazz<Dummy>
28+
*/
29+
public $nullableClass;
30+
31+
/**
32+
* @var IFace<Dummy>
33+
*/
34+
public $basicInterface;
35+
36+
/**
37+
* @var ?IFace<Dummy>
38+
*/
39+
public $nullableInterface;
40+
41+
}

src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
128128
$collection = $mainType->isCollection() || \is_a($mainType->getClassName(), \Traversable::class, true) || \is_a($mainType->getClassName(), \ArrayAccess::class, true);
129129

130130
// it's safer to fall back to other extractors if the generic type is too abstract
131-
if (!$collection && !class_exists($mainType->getClassName())) {
131+
if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) {
132132
return [];
133133
}
134134

src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
1717
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
1818
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
19+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
1920
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection;
21+
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
2022
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
2123
use Symfony\Component\TypeInfo\Type;
2224
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
@@ -138,6 +140,8 @@ public static function resolveDataProvider(): iterable
138140
yield [Type::object(Dummy::class), 'static', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)];
139141
yield [Type::object(AbstractDummy::class), 'parent', $typeContextFactory->createFromClassName(Dummy::class)];
140142
yield [Type::object(Dummy::class), 'Dummy', $typeContextFactory->createFromClassName(Dummy::class)];
143+
yield [Type::enum(DummyEnum::class), 'DummyEnum', $typeContextFactory->createFromClassName(DummyEnum::class)];
144+
yield [Type::enum(DummyBackedEnum::class), 'DummyBackedEnum', $typeContextFactory->createFromClassName(DummyBackedEnum::class)];
141145
yield [Type::template('T', Type::union(Type::int(), Type::string())), 'T', $typeContextFactory->createFromClassName(DummyWithTemplates::class)];
142146
yield [Type::template('V'), 'V', $typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))];
143147

0 commit comments

Comments
 (0)