Skip to content

Commit 9bf9732

Browse files
Merge branch '7.3' into 7.4
* 7.3: [Serializer] Fix unknown type in denormalization errors when union type used in constructor [JsonPath] Add `Nothing` enum to support special nothing value [HttpKernel] Handle an array vary header in the http cache store for write [Console] Fix handling of `\E` in Bash completion
2 parents 87d4aed + a72def9 commit 9bf9732

File tree

8 files changed

+237
-23
lines changed

8 files changed

+237
-23
lines changed

src/Symfony/Component/Console/Resources/completion.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ _sf_{{ COMMAND_NAME }}() {
3737

3838
local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}")
3939
for w in ${words[@]}; do
40-
w=$(printf -- '%b' "$w")
40+
w="${w//\\\\/\\}"
4141
# remove quotes from typed values
4242
quote="${w:0:1}"
4343
if [ "$quote" == \' ]; then

src/Symfony/Component/HttpKernel/HttpCache/Store.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,9 @@ public function write(Request $request, Response $response): string
206206

207207
// read existing cache entries, remove non-varying, and add this one to the list
208208
$entries = [];
209-
$vary = $response->headers->get('vary');
209+
$vary = implode(', ', $response->headers->all('vary'));
210210
foreach ($this->getMetadata($key) as $entry) {
211-
if (!isset($entry[1]['vary'][0])) {
212-
$entry[1]['vary'] = [''];
213-
}
214-
215-
if ($entry[1]['vary'][0] != $vary || !$this->requestsMatch($vary ?? '', $entry[0], $storedEnv)) {
211+
if (!$this->requestsMatch($vary ?? '', $entry[0], $storedEnv)) {
216212
$entries[] = $entry;
217213
}
218214
}
@@ -278,7 +274,7 @@ public function invalidate(Request $request): void
278274
*/
279275
private function requestsMatch(?string $vary, array $env1, array $env2): bool
280276
{
281-
if (!$vary) {
277+
if ('' === ($vary ?? '')) {
282278
return true;
283279
}
284280

src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,127 @@ public function testLoadsBodyEval()
348348
$this->assertSame($content, $response->getContent());
349349
}
350350

351+
/**
352+
* Basic case when the second header has a different value.
353+
* Both responses should be cached
354+
*/
355+
public function testWriteWithMultipleVaryAndCachedAllResponse()
356+
{
357+
$req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']);
358+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24);
359+
$res1 = new Response($content, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']);
360+
$this->store->write($req1, $res1);
361+
362+
$responseLook = $this->store->lookup($req1);
363+
$this->assertSame($content, $responseLook->getContent());
364+
365+
$req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'foobar']);
366+
$content2 = str_repeat('b', 24).'a'.str_repeat('b', 24);
367+
$res2 = new Response($content2, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']);
368+
$this->store->write($req2, $res2);
369+
370+
$responseLook = $this->store->lookup($req2);
371+
$this->assertSame($content2, $responseLook->getContent());
372+
373+
$responseLook = $this->store->lookup($req1);
374+
$this->assertSame($content, $responseLook->getContent());
375+
}
376+
377+
/**
378+
* Basic case when the second header has the same value on both requests.
379+
* The last response should be cached
380+
*/
381+
public function testWriteWithMultipleVaryAndCachedLastResponse()
382+
{
383+
$req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']);
384+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24);
385+
$res1 = new Response($content, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']);
386+
$this->store->write($req1, $res1);
387+
388+
$responseLook = $this->store->lookup($req1);
389+
$this->assertSame($content, $responseLook->getContent());
390+
391+
$req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']);
392+
$content2 = str_repeat('b', 24).'a'.str_repeat('b', 24);
393+
$res2 = new Response($content2, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']);
394+
$this->store->write($req2, $res2);
395+
396+
$responseLook = $this->store->lookup($req2);
397+
$this->assertSame($content2, $responseLook->getContent());
398+
399+
$responseLook = $this->store->lookup($req1);
400+
$this->assertSame($content2, $responseLook->getContent());
401+
}
402+
403+
/**
404+
* Case when a vary value has been removed.
405+
* Both responses should be cached
406+
*/
407+
public function testWriteWithChangingVary()
408+
{
409+
$req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']);
410+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24);
411+
$res1 = new Response($content, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']);
412+
$this->store->write($req1, $res1);
413+
414+
$req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']);
415+
$content2 = str_repeat('b', 24).'a'.str_repeat('b', 24);
416+
$res2 = new Response($content2, 200, ['vary' => ['Foo', 'foobar'], 'X-Body-Eval' => 'SSI']);
417+
$this->store->write($req2, $res2);
418+
419+
$responseLook = $this->store->lookup($req2);
420+
$this->assertSame($content2, $responseLook->getContent());
421+
422+
$responseLook = $this->store->lookup($req1);
423+
$this->assertSame($content, $responseLook->getContent());
424+
}
425+
426+
/**
427+
* Case when a vary value has been removed and headers of the new vary list are the same.
428+
* The last response should be cached
429+
*/
430+
public function testWriteWithRemoveVaryAndAllHeadersOnTheList()
431+
{
432+
$req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar',]);
433+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24);
434+
$res1 = new Response($content, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']);
435+
$this->store->write($req1, $res1);
436+
437+
$req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']);
438+
$content2 = str_repeat('b', 24).'a'.str_repeat('b', 24);
439+
$res2 = new Response($content2, 200, ['vary' => ['Foo', 'foobar'], 'X-Body-Eval' => 'SSI']);
440+
$this->store->write($req2, $res2);
441+
442+
$responseLook = $this->store->lookup($req2);
443+
$this->assertSame($content2, $responseLook->getContent());
444+
445+
$responseLook = $this->store->lookup($req1);
446+
$this->assertSame($content2, $responseLook->getContent());
447+
}
448+
449+
/**
450+
* Case when a vary value has been added and headers of the new vary list are the same.
451+
* The last response should be cached
452+
*/
453+
public function testWriteWithAddingVaryAndAllHeadersOnTheList()
454+
{
455+
$req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']);
456+
$content = str_repeat('a', 24).'b'.str_repeat('a', 24);
457+
$res1 = new Response($content, 200, ['vary' => ['Foo', 'foobar'], 'X-Body-Eval' => 'SSI']);
458+
$this->store->write($req1, $res1);
459+
460+
$req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'foobar', 'HTTP_FOOBAR' => 'bar']);
461+
$content2 = str_repeat('b', 24).'a'.str_repeat('b', 24);
462+
$res2 = new Response($content2, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']);
463+
$this->store->write($req2, $res2);
464+
465+
$responseLook = $this->store->lookup($req2);
466+
$this->assertSame($content2, $responseLook->getContent());
467+
468+
$responseLook = $this->store->lookup($req1);
469+
$this->assertSame($content, $responseLook->getContent());
470+
}
471+
351472
protected function storeSimpleEntry($path = null, $headers = [])
352473
{
353474
$path ??= '/test';

src/Symfony/Component/JsonPath/JsonCrawler.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -720,12 +720,12 @@ private function evaluateScalar(string $expr, mixed $context): mixed
720720
$bracketContent = substr($path, 1, -1);
721721
$result = $this->evaluateBracket($bracketContent, $context);
722722

723-
return $result ? $result[0] : self::nothing();
723+
return $result ? $result[0] : Nothing::Nothing;
724724
}
725725

726726
$results = $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.$path)), $context);
727727

728-
return $results ? $results[0] : self::nothing();
728+
return $results ? $results[0] : Nothing::Nothing;
729729
}
730730

731731
// function calls
@@ -783,7 +783,7 @@ private function evaluateFunction(string $name, string $args, mixed $context): m
783783
\is_string($value) => mb_strlen($value),
784784
\is_array($value) => \count($value),
785785
$value instanceof \stdClass => \count(get_object_vars($value)),
786-
default => self::nothing(),
786+
default => Nothing::Nothing,
787787
},
788788
'count' => $nodelistSize,
789789
'match' => match (true) {
@@ -794,7 +794,7 @@ private function evaluateFunction(string $name, string $args, mixed $context): m
794794
\is_string($value) && \is_string($argList[1] ?? null) => (bool) @preg_match("/{$this->transformJsonPathRegex($argList[1])}/u", $value),
795795
default => false,
796796
},
797-
'value' => 1 < $nodelistSize ? self::nothing() : (1 === $nodelistSize ? (\is_array($value) ? ($value[0] ?? null) : $value) : $value),
797+
'value' => 1 < $nodelistSize ? Nothing::Nothing : (1 === $nodelistSize ? (\is_array($value) ? ($value[0] ?? null) : $value) : $value),
798798
default => null,
799799
};
800800
}
@@ -831,8 +831,8 @@ private function compare(mixed $left, mixed $right, string $operator): bool
831831

832832
private function compareEquality(mixed $left, mixed $right): bool
833833
{
834-
$leftIsNothing = $left === self::nothing();
835-
$rightIsNothing = $right === self::nothing();
834+
$leftIsNothing = $left === Nothing::Nothing;
835+
$rightIsNothing = $right === Nothing::Nothing;
836836

837837
if (
838838
$leftIsNothing && $rightIsNothing
@@ -1101,11 +1101,6 @@ private function isValidMixedBracketExpression(string $expr): bool
11011101
return $hasFilter && $validMixed && 1 < \count($parts);
11021102
}
11031103

1104-
private static function nothing(): \stdClass
1105-
{
1106-
return self::$nothing ??= new \stdClass();
1107-
}
1108-
11091104
private function getValueIfKeyExists(mixed $value, string $key): array
11101105
{
11111106
return $this->isArrayOrObject($value) && \array_key_exists($key, $arrayValue = (array) $value) ? [$arrayValue[$key]] : [];
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\JsonPath;
13+
14+
enum Nothing
15+
{
16+
case Nothing;
17+
}

src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,16 +396,22 @@ protected function instantiateObject(array &$data, string $class, array &$contex
396396
continue;
397397
}
398398

399-
$constructorParameterType = 'unknown';
399+
$constructorParameterTypes = [];
400400
$reflectionType = $constructorParameter->getType();
401-
if ($reflectionType instanceof \ReflectionNamedType) {
402-
$constructorParameterType = $reflectionType->getName();
401+
if ($reflectionType instanceof \ReflectionUnionType) {
402+
foreach ($reflectionType->getTypes() as $reflectionType) {
403+
$constructorParameterTypes[] = (string) $reflectionType;
404+
}
405+
} elseif ($reflectionType instanceof \ReflectionType) {
406+
$constructorParameterTypes[] = (string) $reflectionType;
407+
} else {
408+
$constructorParameterTypes[] = 'unknown';
403409
}
404410

405411
$exception = NotNormalizableValueException::createForUnexpectedDataType(
406412
\sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
407413
null,
408-
[$constructorParameterType],
414+
$constructorParameterTypes,
409415
$attributeContext['deserialization_path'] ?? null,
410416
true
411417
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
/**
15+
* @author Dmitrii <github.com/d-mitrofanov-v>
16+
*/
17+
class DummyWithUnion
18+
{
19+
public function __construct(
20+
public int|float $value,
21+
public string|int $value2,
22+
) {
23+
}
24+
}

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
7171
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty;
7272
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithObjectOrNull;
73+
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithUnion;
7374
use Symfony\Component\Serializer\Tests\Fixtures\DummyWithVariadicParameter;
7475
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
7576
use Symfony\Component\Serializer\Tests\Fixtures\FooImplementationDummy;
@@ -1426,6 +1427,60 @@ public function testCollectDenormalizationErrorsWithInvalidConstructorTypes()
14261427
$this->assertSame($expected, $exceptionsAsArray);
14271428
}
14281429

1430+
public function testCollectDenormalizationErrorsWithUnionConstructorTypes()
1431+
{
1432+
$json = '{}';
1433+
1434+
$serializer = new Serializer(
1435+
[new ObjectNormalizer()],
1436+
['json' => new JsonEncoder()]
1437+
);
1438+
1439+
try {
1440+
$serializer->deserialize(
1441+
$json,
1442+
DummyWithUnion::class,
1443+
'json',
1444+
[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true]
1445+
);
1446+
1447+
$this->fail();
1448+
} catch (\Throwable $th) {
1449+
$this->assertInstanceOf(PartialDenormalizationException::class, $th);
1450+
}
1451+
1452+
$exceptionsAsArray = array_map(fn (NotNormalizableValueException $e): array => [
1453+
'currentType' => $e->getCurrentType(),
1454+
'expectedTypes' => $e->getExpectedTypes(),
1455+
'path' => $e->getPath(),
1456+
'useMessageForUser' => $e->canUseMessageForUser(),
1457+
'message' => $e->getMessage(),
1458+
], $th->getErrors());
1459+
1460+
$expected = [
1461+
[
1462+
'currentType' => 'null',
1463+
'expectedTypes' => [
1464+
'int', 'float',
1465+
],
1466+
'path' => 'value',
1467+
'useMessageForUser' => true,
1468+
'message' => 'Failed to create object because the class misses the "value" property.',
1469+
],
1470+
[
1471+
'currentType' => 'null',
1472+
'expectedTypes' => [
1473+
'string', 'int',
1474+
],
1475+
'path' => 'value2',
1476+
'useMessageForUser' => true,
1477+
'message' => 'Failed to create object because the class misses the "value2" property.',
1478+
],
1479+
];
1480+
1481+
$this->assertSame($expected, $exceptionsAsArray);
1482+
}
1483+
14291484
public function testCollectDenormalizationErrorsWithEnumConstructor()
14301485
{
14311486
$serializer = new Serializer(

0 commit comments

Comments
 (0)