Skip to content

Commit 755284f

Browse files
authored
improve and test parsers (#3)
1 parent cf61f1a commit 755284f

26 files changed

+747
-280
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\Component\Marshaller\Context\Unmarshal;
6+
7+
use Symfony\Component\Marshaller\Attribute\Name;
8+
use Symfony\Component\Marshaller\Context\Context;
9+
use Symfony\Component\Marshaller\Context\UnmarshalContextBuilderInterface;
10+
11+
final class NameAttributeContextBuilder implements UnmarshalContextBuilderInterface
12+
{
13+
public function build(string $type, Context $context, array $rawContext): array
14+
{
15+
if (!class_exists($type)) {
16+
return $rawContext;
17+
}
18+
19+
foreach ((new \ReflectionClass($type))->getProperties() as $property) {
20+
foreach ($property->getAttributes() as $attribute) {
21+
if (Name::class !== $attribute->getName()) {
22+
continue;
23+
}
24+
25+
/** @var Name $attributeInstance */
26+
$attributeInstance = $attribute->newInstance();
27+
28+
$propertyIdentifier = sprintf('%s::$%s', $property->getDeclaringClass()->getName(), $property->getName());
29+
$rawContext['symfony']['unmarshal']['property_name'][$propertyIdentifier] = $attributeInstance->name;
30+
31+
break;
32+
}
33+
}
34+
35+
return $rawContext;
36+
}
37+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\Component\Marshaller\Internal\Exception;
6+
7+
final class InvalidJsonException extends InvalidResourceException
8+
{
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\Component\Marshaller\Internal\Exception;
6+
7+
abstract class InvalidResourceException extends \InvalidArgumentException
8+
{
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\Component\Marshaller\Internal\Exception;
6+
7+
final class InvalidTokenException extends \InvalidArgumentException
8+
{
9+
public function __construct(string $expected, string $actual)
10+
{
11+
parent::__construct(sprintf('Expected "%s" token, got "%s".', $expected, $actual));
12+
}
13+
}

Internal/Lexer/JsonLexer.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Symfony\Component\Marshaller\Internal\Lexer;
66

7+
use Symfony\Component\Marshaller\Internal\Exception\InvalidJsonException;
8+
79
final class JsonLexer implements LexerInterface
810
{
911
private const DICT_START = 1;
@@ -22,7 +24,7 @@ final class JsonLexer implements LexerInterface
2224

2325
private const VALUE = self::DICT_START | self::LIST_START | self::SCALAR;
2426

25-
public function tokens(mixed $resource, array $context): \Generator
27+
public function tokens(mixed $resource, array $context): \Iterator
2628
{
2729
$expectedType = self::VALUE;
2830
$structureStack = new \SplStack();
@@ -33,19 +35,21 @@ public function tokens(mixed $resource, array $context): \Generator
3335
}
3436

3537
if (!($type & $expectedType)) {
36-
throw new \RuntimeException('Invalid JSON.');
38+
// TODO better message
39+
throw new InvalidJsonException();
3740
}
3841

3942
if (self::SCALAR === $type) {
43+
// TODO maybe yield the result for performances?
4044
json_decode($token, flags: $context['json_decode_flags'] ?? 0);
4145

4246
if (JSON_ERROR_NONE !== json_last_error()) {
43-
throw new \RuntimeException('Invalid JSON.');
47+
throw new InvalidJsonException();
4448
}
4549
}
4650

4751
if (self::KEY === $type && !(str_starts_with($token, '"') && str_ends_with($token, '"'))) {
48-
throw new \RuntimeException('Invalid JSON.');
52+
throw new InvalidJsonException();
4953
}
5054

5155
yield $token;
@@ -74,12 +78,12 @@ public function tokens(mixed $resource, array $context): \Generator
7478
0 !== ($type & (self::DICT_END | self::LIST_END | self::SCALAR)) && 'list' === $currentStructure => self::COMMA | self::LIST_END,
7579
0 !== ($type & (self::DICT_END | self::LIST_END | self::SCALAR)) => self::END,
7680

77-
default => throw new \RuntimeException('Invalid JSON.'),
81+
default => throw new InvalidJsonException(),
7882
};
7983
}
8084

8185
if (self::END !== $expectedType) {
82-
throw new \RuntimeException('Invalid JSON.');
86+
throw new InvalidJsonException();
8387
}
8488
}
8589

Internal/Lexer/LexerInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface LexerInterface
1010
* @param resource $resource
1111
* @param array<string, mixed> $context
1212
*
13-
* @return \Generator<string>
13+
* @return \Iterator<string>
1414
*/
15-
public function tokens(mixed $resource, array $context): \Generator;
15+
public function tokens(mixed $resource, array $context): \Iterator;
1616
}

Internal/Parser/DictParserInterface.php

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,13 @@
44

55
namespace Symfony\Component\Marshaller\Internal\Parser;
66

7-
use Symfony\Component\Marshaller\Internal\Type\Type;
8-
use Symfony\Component\Marshaller\Internal\Type\UnionType;
9-
107
interface DictParserInterface
118
{
129
/**
1310
* @param \Iterator<string> $tokens
1411
* @param array<string, mixed> $context
1512
*
16-
* @return array<string, mixed>
17-
*/
18-
public function parse(\Iterator $tokens, Type|UnionType $valueType, array $context, Parser $parser): array;
19-
20-
/**
21-
* @param \Iterator<string> $tokens
22-
* @param array<string, mixed> $context
23-
*
24-
* @return iterable<string, mixed>
13+
* @return \Iterator<string>
2514
*/
26-
public function parseIterable(\Iterator $tokens, Type|UnionType $valueType, array $context, Parser $parser): iterable;
15+
public function parse(\Iterator $tokens, array $context): \Iterator;
2716
}

Internal/Parser/Json/JsonDictParser.php

Lines changed: 9 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,76 +4,18 @@
44

55
namespace Symfony\Component\Marshaller\Internal\Parser\Json;
66

7+
use Symfony\Component\Marshaller\Internal\Exception\InvalidTokenException;
78
use Symfony\Component\Marshaller\Internal\Parser\DictParserInterface;
8-
use Symfony\Component\Marshaller\Internal\Parser\Parser;
9-
use Symfony\Component\Marshaller\Internal\Type\Type;
10-
use Symfony\Component\Marshaller\Internal\Type\UnionType;
119

10+
/**
11+
* @internal
12+
*/
1213
final class JsonDictParser implements DictParserInterface
1314
{
14-
public function parse(\Iterator $tokens, Type|UnionType $valueType, array $context, Parser $parser): array
15+
public function parse(\Iterator $tokens, array $context): \Iterator
1516
{
1617
if ('{' !== $tokens->current()) {
17-
throw new \InvalidArgumentException('Invalid JSON.');
18-
}
19-
20-
$buffer = [];
21-
$result = [];
22-
$level = 0;
23-
$key = null;
24-
25-
$tokens->next();
26-
27-
while ($tokens->valid()) {
28-
$token = $tokens->current();
29-
$tokens->next();
30-
31-
// TODO check null key and null value
32-
if (0 === $level && '}' === $token) {
33-
if (null !== $key) {
34-
$result[$key] = $parser->parse(new \ArrayIterator($buffer), $valueType, $context);
35-
}
36-
37-
return $result;
38-
}
39-
40-
if (null === $key) {
41-
// TODO flags
42-
43-
/** @var string $key */
44-
$key = \json_decode($token);
45-
46-
continue;
47-
}
48-
49-
if (':' === $token) {
50-
continue;
51-
}
52-
53-
if (0 === $level && ',' === $token) {
54-
$result[$key] = $parser->parse(new \ArrayIterator($buffer), $valueType, $context);
55-
$key = null;
56-
$buffer = [];
57-
58-
continue;
59-
}
60-
61-
$buffer[] = $token;
62-
63-
if ('{' === $token) {
64-
++$level;
65-
} elseif ('}' === $token) {
66-
--$level;
67-
}
68-
}
69-
70-
return $result;
71-
}
72-
73-
public function parseIterable(\Iterator $tokens, Type|UnionType $valueType, array $context, Parser $parser): iterable
74-
{
75-
if ('{' !== $tokens->current()) {
76-
throw new \InvalidArgumentException('Invalid JSON.');
18+
throw new InvalidTokenException('{', $tokens->current());
7719
}
7820

7921
$tokens->next();
@@ -89,20 +31,19 @@ public function parseIterable(\Iterator $tokens, Type|UnionType $valueType, arra
8931
}
9032

9133
if (null === $key) {
92-
// TODO flags
93-
$key = \json_decode($token);
34+
$key = \json_decode($token, flags: $context['json_decode_flags'] ?? 0);
9435
$tokens->next();
9536

9637
continue;
9738
}
9839

99-
if (\in_array($token, [',', ':'], true)) {
40+
if (',' === $token || ':' === $token) {
10041
$tokens->next();
10142

10243
continue;
10344
}
10445

105-
yield $key => $parser->parse($tokens, $valueType, $context);
46+
yield $key;
10647

10748
$key = null;
10849
}

Internal/Parser/Json/JsonListParser.php

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,15 @@
44

55
namespace Symfony\Component\Marshaller\Internal\Parser\Json;
66

7+
use Symfony\Component\Marshaller\Internal\Exception\InvalidTokenException;
78
use Symfony\Component\Marshaller\Internal\Parser\ListParserInterface;
8-
use Symfony\Component\Marshaller\Internal\Parser\Parser;
9-
use Symfony\Component\Marshaller\Internal\Type\Type;
10-
use Symfony\Component\Marshaller\Internal\Type\UnionType;
119

1210
final class JsonListParser implements ListParserInterface
1311
{
14-
public function parse(\Iterator $tokens, Type|UnionType $valueType, array $context, Parser $parser): array
12+
public function parse(\Iterator $tokens, array $context): \Iterator
1513
{
1614
if ('[' !== $tokens->current()) {
17-
throw new \InvalidArgumentException('Invalid JSON.');
18-
}
19-
20-
$buffer = [];
21-
$result = [];
22-
$level = 0;
23-
24-
$tokens->next();
25-
26-
while ($tokens->valid()) {
27-
$token = $tokens->current();
28-
$tokens->next();
29-
30-
// TODO check null value
31-
if (0 === $level && ',' === $token) {
32-
$result[] = $parser->parse(new \ArrayIterator($buffer), $valueType, $context);
33-
34-
return $result;
35-
}
36-
37-
if (0 === $level && ']' === $token) {
38-
$result[] = $parser->parse(new \ArrayIterator($buffer), $valueType, $context);
39-
$buffer = [];
40-
41-
continue;
42-
}
43-
44-
$buffer[] = $token;
45-
46-
if ('[' === $token) {
47-
++$level;
48-
49-
continue;
50-
}
51-
52-
if (']' === $token) {
53-
--$level;
54-
55-
continue;
56-
}
57-
}
58-
59-
return $result;
60-
}
61-
62-
public function parseIterable(\Iterator $tokens, Type|UnionType $valueType, array $context, Parser $parser): iterable
63-
{
64-
if ('[' !== $tokens->current()) {
65-
throw new \InvalidArgumentException('Invalid JSON.');
15+
throw new InvalidTokenException('[', $tokens->current());
6616
}
6717

6818
$tokens->next();
@@ -82,7 +32,7 @@ public function parseIterable(\Iterator $tokens, Type|UnionType $valueType, arra
8232
continue;
8333
}
8434

85-
yield $parser->parse($tokens, $valueType, $context);
35+
yield;
8636
}
8737
}
8838
}

Internal/Parser/Json/JsonNullableParser.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
namespace Symfony\Component\Marshaller\Internal\Parser\Json;
66

77
use Symfony\Component\Marshaller\Internal\Parser\NullableParserInterface;
8-
use Symfony\Component\Marshaller\Internal\Parser\Parser;
9-
use Symfony\Component\Marshaller\Internal\Type\Type;
108

119
final class JsonNullableParser implements NullableParserInterface
1210
{
13-
public function parse(\Iterator $tokens, Type $type, array $context, Parser $parser): mixed
11+
public function parse(\Iterator $tokens, callable $handle, array $context): mixed
1412
{
1513
$token = $tokens->current();
1614

@@ -20,8 +18,6 @@ public function parse(\Iterator $tokens, Type $type, array $context, Parser $par
2018
return null;
2119
}
2220

23-
$type = Type::createFromString(substr((string) $type, 1));
24-
25-
return $parser->parse($tokens, $type, $context);
21+
return $handle($tokens);
2622
}
2723
}

0 commit comments

Comments
 (0)