Skip to content

Commit f89ccf0

Browse files
authored
feat: object parsing (#5)
1 parent 6ada20f commit f89ccf0

13 files changed

+396
-37
lines changed

Internal/Hook/HookExtractor.php renamed to Internal/Hook/MarshalHookExtractor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @internal
1818
*/
19-
final class HookExtractor
19+
final class MarshalHookExtractor
2020
{
2121
/**
2222
* @param array<string, mixed> $context
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 Symfony package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\Component\Marshaller\Internal\Hook;
11+
12+
/**
13+
* @author Mathias Arlaud <[email protected]>
14+
*
15+
* @internal
16+
*/
17+
final class UnmarshalHookExtractor
18+
{
19+
/**
20+
* @param class-string $className
21+
* @param array<string, mixed> $context
22+
*/
23+
public function extractFromKey(string $className, string $key, array $context): ?callable
24+
{
25+
if (null === ($hook = $context['hooks'][$className][$key] ?? null)) {
26+
return null;
27+
}
28+
29+
$reflection = new \ReflectionFunction(\Closure::fromCallable($hook));
30+
31+
if (4 !== \count($reflection->getParameters())) {
32+
throw new \InvalidArgumentException(sprintf('Hook "%s" of "%s" must have exactly 4 arguments.', $key, $className));
33+
}
34+
35+
$classParameterType = $reflection->getParameters()[0]->getType();
36+
if (!$classParameterType instanceof \ReflectionNamedType || \ReflectionClass::class !== $classParameterType->getName()) {
37+
throw new \InvalidArgumentException(sprintf('Hook "%s" of "%s" must have a "%s" for first argument.', $key, $className, \ReflectionClass::class));
38+
}
39+
40+
$objectParameterType = $reflection->getParameters()[1]->getType();
41+
if (!$objectParameterType instanceof \ReflectionNamedType || 'object' !== $objectParameterType->getName()) {
42+
throw new \InvalidArgumentException(sprintf('Hook "%s" of "%s" must have an "object" for second argument.', $key, $className));
43+
}
44+
45+
$valueParameterType = $reflection->getParameters()[2]->getType();
46+
if (!$valueParameterType instanceof \ReflectionNamedType || 'callable' !== $valueParameterType->getName()) {
47+
throw new \InvalidArgumentException(sprintf('Hook "%s" of "%s" must have a "callable" for third argument.', $key, $className));
48+
}
49+
50+
$contextParameterType = $reflection->getParameters()[3]->getType();
51+
if (!$contextParameterType instanceof \ReflectionNamedType || 'array' !== $contextParameterType->getName()) {
52+
throw new \InvalidArgumentException(sprintf('Hook "%s" of "%s" must have an "array" for fourth argument.', $key, $className));
53+
}
54+
55+
return $hook;
56+
}
57+
}

Internal/Parser/Parser.php

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
/*
46
* This file is part of the Symfony package.
57
* (c) Fabien Potencier <[email protected]>
@@ -9,6 +11,7 @@
911

1012
namespace Symfony\Component\Marshaller\Internal\Parser;
1113

14+
use Symfony\Component\Marshaller\Internal\Hook\UnmarshalHookExtractor;
1215
use Symfony\Component\Marshaller\Internal\Type\Type;
1316
use Symfony\Component\Marshaller\Internal\Type\UnionType;
1417
use Symfony\Component\Marshaller\Type\ReflectionTypeExtractor;
@@ -20,15 +23,15 @@
2023
*/
2124
final class Parser
2225
{
23-
private readonly ReflectionTypeExtractor $reflectionTypeExtractor;
26+
private UnmarshalHookExtractor|null $hookExtractor = null;
27+
private ReflectionTypeExtractor|null $reflectionTypeExtractor = null;
2428

2529
public function __construct(
2630
private readonly NullableParserInterface $nullableParser,
2731
private readonly ScalarParserInterface $scalarParser,
2832
private readonly ListParserInterface $listParser,
2933
private readonly DictParserInterface $dictParser,
3034
) {
31-
$this->reflectionTypeExtractor = new ReflectionTypeExtractor();
3235
}
3336

3437
/**
@@ -111,12 +114,15 @@ private function parseDict(\Iterator $tokens, Type $type, array $context): \Iter
111114
*/
112115
private function parseObject(\Iterator $tokens, Type $type, array $context): object
113116
{
117+
$this->hookExtractor = $this->hookExtractor ?? new UnmarshalHookExtractor();
118+
$this->reflectionTypeExtractor = $this->reflectionTypeExtractor ?? new ReflectionTypeExtractor();
119+
114120
$reflection = new \ReflectionClass($type->className());
115-
$object = $reflection->newInstanceWithoutConstructor();
121+
$object = $this->instantiateObject($reflection);
116122

117123
foreach ($this->dictParser->parse($tokens, $context) as $key) {
118-
if (null !== ($hook = $context['hooks'][$reflection->getName()][$key] ?? null)) {
119-
$hook($reflection, $object, $context, fn (string $type, array $context): mixed => $this->parse($tokens, Type::createFromString($type), $context));
124+
if (null !== $hook = $this->hookExtractor->extractFromKey($reflection->getName(), $key, $context)) {
125+
$hook($reflection, $object, fn (string $type, array $context): mixed => $this->parse($tokens, Type::createFromString($type), $context), $context);
120126

121127
continue;
122128
}
@@ -126,4 +132,42 @@ private function parseObject(\Iterator $tokens, Type $type, array $context): obj
126132

127133
return $object;
128134
}
135+
136+
/**
137+
* @template T of object
138+
*
139+
* @param \ReflectionClass<T> $reflection
140+
*
141+
* @return T
142+
*/
143+
private function instantiateObject(\ReflectionClass $reflection): object
144+
{
145+
if (null === $constructor = $reflection->getConstructor()) {
146+
return new ($reflection->getName())();
147+
}
148+
149+
if (!$constructor->isPublic()) {
150+
return $reflection->newInstanceWithoutConstructor();
151+
}
152+
153+
$constructorParameters = [];
154+
155+
foreach ($constructor->getParameters() as $constructorParameter) {
156+
if ($constructorParameter->isDefaultValueAvailable()) {
157+
$constructorParameters[] = $constructorParameter->getDefaultValue();
158+
159+
continue;
160+
}
161+
162+
if ($constructorParameter->hasType() && $constructorParameter->getType()?->allowsNull()) {
163+
$constructorParameters[] = null;
164+
165+
continue;
166+
}
167+
168+
return $reflection->newInstanceWithoutConstructor();
169+
}
170+
171+
return $reflection->newInstanceArgs($constructorParameters);
172+
}
129173
}

Internal/Template/ObjectTemplateGenerator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Symfony\Component\Marshaller\Internal\Ast\Node\RawNode;
1919
use Symfony\Component\Marshaller\Internal\Ast\Node\ScalarNode;
2020
use Symfony\Component\Marshaller\Internal\Ast\Node\VariableNode;
21-
use Symfony\Component\Marshaller\Internal\Hook\HookExtractor;
21+
use Symfony\Component\Marshaller\Internal\Hook\MarshalHookExtractor;
2222
use Symfony\Component\Marshaller\Internal\Type\Type;
2323
use Symfony\Component\Marshaller\Type\ReflectionTypeExtractor;
2424

@@ -31,7 +31,7 @@ final class ObjectTemplateGenerator
3131
{
3232
use VariableNameScoperTrait;
3333

34-
private readonly HookExtractor $hookExtractor;
34+
private readonly MarshalHookExtractor $hookExtractor;
3535
private readonly ReflectionTypeExtractor $reflectionTypeExtractor;
3636

3737
/**
@@ -45,7 +45,7 @@ public function __construct(
4545
private readonly string $afterPropertyName,
4646
private readonly \Closure $propertyNameEscaper,
4747
) {
48-
$this->hookExtractor = new HookExtractor();
48+
$this->hookExtractor = new MarshalHookExtractor();
4949
$this->reflectionTypeExtractor = new ReflectionTypeExtractor();
5050
}
5151

Internal/Template/TemplateGenerator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use Symfony\Component\Marshaller\Internal\Ast\Node\NodeInterface;
1616
use Symfony\Component\Marshaller\Internal\Ast\Node\RawNode;
1717
use Symfony\Component\Marshaller\Internal\Ast\Node\ScalarNode;
18-
use Symfony\Component\Marshaller\Internal\Hook\HookExtractor;
18+
use Symfony\Component\Marshaller\Internal\Hook\MarshalHookExtractor;
1919
use Symfony\Component\Marshaller\Internal\Type\Type;
2020
use Symfony\Component\Marshaller\Internal\Type\UnionType;
2121

@@ -26,7 +26,7 @@
2626
*/
2727
final class TemplateGenerator
2828
{
29-
private readonly HookExtractor $hookExtractor;
29+
private readonly MarshalHookExtractor $hookExtractor;
3030
private readonly UnionTemplateGenerator $unionGenerator;
3131

3232
public function __construct(
@@ -35,7 +35,7 @@ public function __construct(
3535
private readonly ListTemplateGenerator $listGenerator,
3636
private readonly DictTemplateGenerator $dictGenerator,
3737
) {
38-
$this->hookExtractor = new HookExtractor();
38+
$this->hookExtractor = new MarshalHookExtractor();
3939
$this->unionGenerator = new UnionTemplateGenerator($this);
4040
}
4141

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
- Unmarshal name and formatter hook (property hook)
88
- UTF-8 BOM
99
- tests (hooks, context generation, internal unmarshal, unmarshal)
10-
- mark internal classes
11-
- if constructor -> newInstanceWithoutConstructor but set defaults
12-
- else classic new instance
1310
- create dedicated exceptions and wrap native ones
1411

1512
## Questions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\Component\Marshaller\Tests\Fixtures\Dto;
11+
12+
final class DummyWithConstructorWithDefaultValues
13+
{
14+
public function __construct(
15+
public int $id = 1,
16+
) {
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\Component\Marshaller\Tests\Fixtures\Dto;
11+
12+
final class DummyWithConstructorWithNullableValues
13+
{
14+
public function __construct(
15+
public ?int $id,
16+
) {
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\Component\Marshaller\Tests\Fixtures\Dto;
11+
12+
final class DummyWithConstructorWithRequiredValues
13+
{
14+
public int $id = 1;
15+
16+
public function __construct(int $id)
17+
{
18+
$this->id = $id;
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\Component\Marshaller\Tests\Fixtures\Dto;
11+
12+
final class DummyWithPrivateConstructor
13+
{
14+
public int $id = 1;
15+
16+
private function __construct()
17+
{
18+
$this->id = 2;
19+
}
20+
}

Tests/Internal/Hook/HookExtractorTest.php renamed to Tests/Internal/Hook/MarshalHookExtractorTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
namespace Symfony\Component\Marshaller\Tests\Internal\Hook;
1111

1212
use PHPUnit\Framework\TestCase;
13-
use Symfony\Component\Marshaller\Internal\Hook\HookExtractor;
13+
use Symfony\Component\Marshaller\Internal\Hook\MarshalHookExtractor;
1414
use Symfony\Component\Marshaller\Internal\Type\Type;
1515
use Symfony\Component\Marshaller\Tests\Fixtures\Dto\ClassicDummy;
1616

17-
final class HookExtractorTest extends TestCase
17+
final class MarshalHookExtractorTest extends TestCase
1818
{
1919
/**
2020
* @dataProvider extractFromPropertyDataProvider
@@ -30,7 +30,7 @@ public function testExtractFromProperty(?callable $expectedHook, array $hooks):
3030
$property->method('getName')->willReturn('bar');
3131
$property->method('getDeclaringClass')->willReturn($class);
3232

33-
$this->assertSame($expectedHook, (new HookExtractor())->extractFromProperty($property, ['hooks' => $hooks]));
33+
$this->assertSame($expectedHook, (new MarshalHookExtractor())->extractFromProperty($property, ['hooks' => $hooks]));
3434
}
3535

3636
/**
@@ -54,7 +54,7 @@ public function extractFromPropertyDataProvider(): iterable
5454
*/
5555
public function testExtractFromType(?callable $expectedHook, array $hooks, Type $type): void
5656
{
57-
$this->assertSame($expectedHook, (new HookExtractor())->extractFromType($type, ['hooks' => $hooks]));
57+
$this->assertSame($expectedHook, (new MarshalHookExtractor())->extractFromType($type, ['hooks' => $hooks]));
5858
}
5959

6060
/**
@@ -138,7 +138,7 @@ public function testPropertyHookValidation(?string $expectedExceptionMessage, ca
138138
$this->expectExceptionMessage($expectedExceptionMessage);
139139
}
140140

141-
(new HookExtractor())->extractFromProperty($property, ['hooks' => ['property' => $callable]]);
141+
(new MarshalHookExtractor())->extractFromProperty($property, ['hooks' => ['property' => $callable]]);
142142

143143
$this->addToAssertionCount(1);
144144
}
@@ -170,7 +170,7 @@ public function testTypeHookValidation(?string $expectedExceptionMessage, callab
170170
$this->expectExceptionMessage($expectedExceptionMessage);
171171
}
172172

173-
(new HookExtractor())->extractFromType(new Type('int'), ['hooks' => ['type' => $callable]]);
173+
(new MarshalHookExtractor())->extractFromType(new Type('int'), ['hooks' => ['type' => $callable]]);
174174

175175
$this->addToAssertionCount(1);
176176
}

0 commit comments

Comments
 (0)