Skip to content

Commit d2758bb

Browse files
authored
chore: link to component (#7)
1 parent 6adc426 commit d2758bb

32 files changed

+809
-202
lines changed

Attribute/Formatter.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
#[\Attribute(\Attribute::TARGET_PROPERTY)]
1616
final class Formatter
1717
{
18-
public readonly \Closure $marshalFormatter;
19-
public readonly \Closure $unmarshalFormatter;
18+
public readonly ?\Closure $marshalFormatter;
19+
public readonly ?\Closure $unmarshalFormatter;
2020

2121
/**
2222
* @param string|array{0: string, 1: string} $marshal
@@ -30,16 +30,15 @@ public function __construct(
3030
if (!\is_callable($marshal)) {
3131
throw new \InvalidArgumentException(sprintf('Parameter "$marshal" of attribute "%s" must be a valid callable.', self::class));
3232
}
33-
34-
$this->marshalFormatter = \Closure::fromCallable($marshal);
3533
}
3634

3735
if (null !== $unmarshal) {
3836
if (!\is_callable($unmarshal)) {
3937
throw new \InvalidArgumentException(sprintf('Parameter "$unmarshal" of attribute "%s" must be a valid callable.', self::class));
4038
}
41-
42-
$this->unmarshalFormatter = \Closure::fromCallable($unmarshal);
4339
}
40+
41+
$this->marshalFormatter = null !== $marshal ? \Closure::fromCallable($marshal) : null;
42+
$this->unmarshalFormatter = null !== $unmarshal ? \Closure::fromCallable($unmarshal) : null;
4443
}
4544
}

Context/Generation/FormatterAttributeContextBuilder.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public function build(string $type, Context $context, array $rawContext): array
3232

3333
/** @var Formatter $attributeInstance */
3434
$attributeInstance = $attribute->newInstance();
35+
if (null === $attributeInstance->marshalFormatter) {
36+
break;
37+
}
3538

3639
$propertyIdentifier = sprintf('%s::$%s', $property->getDeclaringClass()->getName(), $property->getName());
3740
$rawContext['symfony']['marshal']['property_formatter'][$propertyIdentifier] = $attributeInstance->marshalFormatter;

Context/Unmarshal/FormatterAttributeContextBuilder.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public function build(string $type, Context $context, array $rawContext): array
3232

3333
/** @var Formatter $attributeInstance */
3434
$attributeInstance = $attribute->newInstance();
35+
if (null === $attributeInstance->unmarshalFormatter) {
36+
break;
37+
}
3538

3639
$propertyIdentifier = sprintf('%s::$%s', $property->getDeclaringClass()->getName(), $property->getName());
3740
$rawContext['symfony']['unmarshal']['property_formatter'][$propertyIdentifier] = $attributeInstance->unmarshalFormatter;

Context/Unmarshal/NameAttributeContextBuilder.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ public function build(string $type, Context $context, array $rawContext): array
3232

3333
/** @var Name $attributeInstance */
3434
$attributeInstance = $attribute->newInstance();
35-
36-
$propertyIdentifier = sprintf('%s::$%s', $property->getDeclaringClass()->getName(), $property->getName());
37-
$rawContext['symfony']['unmarshal']['property_name'][$propertyIdentifier] = $attributeInstance->name;
35+
$rawContext['symfony']['unmarshal']['property_name'][$property->getDeclaringClass()->getName()][$attributeInstance->name] = $property->getName();
3836

3937
break;
4038
}

DependencyInjection/MarshallerExtension.php

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
1717
use Symfony\Component\Marshaller\Cache\TemplateCacheWarmer;
1818
use Symfony\Component\Marshaller\Cache\WarmableResolver;
19-
use Symfony\Component\Marshaller\Context\Generation\FormatterAttributeContextBuilder;
20-
use Symfony\Component\Marshaller\Context\Generation\HookContextBuilder;
21-
use Symfony\Component\Marshaller\Context\Generation\NameAttributeContextBuilder;
22-
use Symfony\Component\Marshaller\Context\Generation\TypeFormatterContextBuilder;
23-
use Symfony\Component\Marshaller\Context\Marshal\JsonEncodeFlagsContextBuilder;
24-
use Symfony\Component\Marshaller\Context\Marshal\TypeContextBuilder;
19+
use Symfony\Component\Marshaller\Context\Generation;
20+
use Symfony\Component\Marshaller\Context\Marshal;
21+
use Symfony\Component\Marshaller\Context\Unmarshal;
2522
use Symfony\Component\Marshaller\Marshaller;
2623
use Symfony\Component\Marshaller\MarshallerInterface;
2724
use Symfony\Component\Marshaller\Type\PhpstanTypeExtractor;
@@ -72,32 +69,34 @@ public function load(array $configs, ContainerBuilder $container): void
7269
//
7370
// Generation context builders
7471
//
75-
$container->register('.marshaller.builder.generation.hook', HookContextBuilder::class)
72+
$container->register('.marshaller.builder.generation.hook', Generation\HookContextBuilder::class)
7673
->addTag('marshaller.context.builder.generation', ['priority' => -128]);
7774

78-
$container->register('.marshaller.builder.generation.type_formatter', TypeFormatterContextBuilder::class)
75+
$container->register('.marshaller.builder.generation.type_formatter', Generation\TypeFormatterContextBuilder::class)
7976
->addTag('marshaller.context.builder.generation', ['priority' => -128]);
8077

81-
$container->register('.marshaller.builder.generation.name_attribute', NameAttributeContextBuilder::class)
78+
$container->register('.marshaller.builder.generation.name_attribute', Generation\NameAttributeContextBuilder::class)
8279
->addTag('marshaller.context.builder.generation', ['priority' => -128]);
8380

84-
$container->register('.marshaller.builder.generation.formatter_attribute', FormatterAttributeContextBuilder::class)
81+
$container->register('.marshaller.builder.generation.formatter_attribute', Generation\FormatterAttributeContextBuilder::class)
8582
->addTag('marshaller.context.builder.generation', ['priority' => -128]);
8683

8784
//
8885
// Marshal context builders
8986
//
90-
$container->register('.marshaller.builder.marshal.type', TypeContextBuilder::class)
87+
$container->register('.marshaller.builder.marshal.type', Marshal\TypeContextBuilder::class)
9188
->addTag('marshaller.context.builder.marshal', ['priority' => -128]);
9289

93-
$container->register('.marshaller.builder.marshal.json_encode_flags', JsonEncodeFlagsContextBuilder::class)
90+
$container->register('.marshaller.builder.marshal.json_encode_flags', Marshal\JsonEncodeFlagsContextBuilder::class)
9491
->addTag('marshaller.context.builder.marshal', ['priority' => -128]);
9592

9693
//
9794
// Unmarshal context builders
9895
//
99-
$container->register('.marshaller.builder.unmarshal.formatter_attribute', FormatterAttributeContextBuilder::class)
100-
->addTag('marshaller.context.builder.unmarshal', ['priority' => -128]);
96+
$container->register('.marshaller.builder.unmarshal.name_attribute', Unmarshal\NameAttributeContextBuilder::class)
97+
->addTag('marshaller.context.builder.unmarshal', ['priority' => -256]);
98+
$container->register('.marshaller.builder.unmarshal.formatter_attribute', Unmarshal\FormatterAttributeContextBuilder::class)
99+
->addTag('marshaller.context.builder.unmarshal', ['priority' => -128]); // must be triggered after ".marshaller.builder.unmarshal.name_attribute"
101100

102101
//
103102
// Cache

Hook/ObjectHook.php renamed to Hook/Marshal/ObjectHook.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* file that was distributed with this source code.
88
*/
99

10-
namespace Symfony\Component\Marshaller\Hook;
10+
namespace Symfony\Component\Marshaller\Hook\Marshal;
1111

1212
use Symfony\Component\Marshaller\Type\TypeExtractorInterface;
1313

Hook/PropertyHook.php renamed to Hook/Marshal/PropertyHook.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* file that was distributed with this source code.
88
*/
99

10-
namespace Symfony\Component\Marshaller\Hook;
10+
namespace Symfony\Component\Marshaller\Hook\Marshal;
1111

1212
use Symfony\Component\Marshaller\Type\TypeExtractorInterface;
1313

Hook/TypeHook.php renamed to Hook/Marshal/TypeHook.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* file that was distributed with this source code.
88
*/
99

10-
namespace Symfony\Component\Marshaller\Hook;
10+
namespace Symfony\Component\Marshaller\Hook\Marshal;
1111

1212
use Symfony\Component\Marshaller\Type\TypeExtractorInterface;
1313

Hook/Unmarshal/PropertyHook.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Hook\Unmarshal;
11+
12+
use Symfony\Component\Marshaller\Type\TypeExtractorInterface;
13+
14+
/**
15+
* @author Mathias Arlaud <[email protected]>
16+
*
17+
* @internal
18+
*/
19+
final class PropertyHook
20+
{
21+
public function __construct(
22+
private readonly TypeExtractorInterface $typeExtractor,
23+
) {
24+
}
25+
26+
/**
27+
* @param \ReflectionClass<object> $class
28+
* @param callable(string, array<string, mixed>): mixed $value
29+
* @param array<string, mixed> $context
30+
*/
31+
public function __invoke(\ReflectionClass $class, object $object, string $key, callable $value, array $context): void
32+
{
33+
$propertyName = $context['symfony']['unmarshal']['property_name'][$class->getName()][$key] ?? $key;
34+
$propertyIdentifier = sprintf('%s::$%s', $class->getName(), $propertyName);
35+
$propertyFormatter = $context['symfony']['unmarshal']['property_formatter'][$propertyIdentifier] ?? null;
36+
37+
if (null !== $propertyFormatter) {
38+
$propertyFormatterReflection = new \ReflectionFunction($propertyFormatter);
39+
$this->validateFormatter($propertyFormatterReflection, $propertyIdentifier);
40+
41+
$valueType = $this->typeExtractor->extractFromParameter($propertyFormatterReflection->getParameters()[0]);
42+
}
43+
44+
$valueType ??= $this->typeExtractor->extractFromProperty(new \ReflectionProperty($object, $propertyName));
45+
46+
$propertyValue = $value($valueType, $context);
47+
if (null !== $propertyFormatter) {
48+
$propertyValue = $propertyFormatter($propertyValue, $context);
49+
}
50+
51+
$object->{$propertyName} = $propertyValue;
52+
}
53+
54+
private function validateFormatter(\ReflectionFunction $reflection, string $propertyIdentifier): void
55+
{
56+
if (!$reflection->getClosureScopeClass()?->hasMethod($reflection->getName()) || !$reflection->isStatic()) {
57+
throw new \InvalidArgumentException(sprintf('Property formatter "%s" must be a static method.', $propertyIdentifier));
58+
}
59+
60+
if (($returnType = $reflection->getReturnType()) instanceof \ReflectionNamedType && ('void' === $returnType->getName() || 'never' === $returnType->getName())) {
61+
throw new \InvalidArgumentException(sprintf('Return type of property formatter "%s" must not be "void" nor "never".', $propertyIdentifier));
62+
}
63+
64+
if (\count($reflection->getParameters()) < 1) {
65+
throw new \InvalidArgumentException(sprintf('Property formatter "%s" must have at least one argument.', $propertyIdentifier));
66+
}
67+
68+
if (null !== ($contextParameter = $reflection->getParameters()[1] ?? null)) {
69+
$contextParameterType = $contextParameter->getType();
70+
71+
if (!$contextParameterType instanceof \ReflectionNamedType || 'array' !== $contextParameterType->getName()) {
72+
throw new \InvalidArgumentException(sprintf('Second argument of property formatter "%s" must be an array.', $propertyIdentifier));
73+
}
74+
}
75+
}
76+
}

Internal/Hook/UnmarshalHookExtractor.php

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,62 @@ final class UnmarshalHookExtractor
2222
*/
2323
public function extractFromKey(string $className, string $key, array $context): ?callable
2424
{
25-
if (null === ($hook = $context['hooks'][$className][$key] ?? null)) {
25+
if (null === $findHookResult = $this->findHook($className, $key, $context)) {
2626
return null;
2727
}
2828

29+
[$hookName, $hook] = $findHookResult;
30+
2931
$reflection = new \ReflectionFunction(\Closure::fromCallable($hook));
3032

31-
if (4 !== \count($reflection->getParameters())) {
32-
throw new \InvalidArgumentException(sprintf('Hook "%s" of "%s" must have exactly 4 arguments.', $key, $className));
33+
if (5 !== \count($reflection->getParameters())) {
34+
throw new \InvalidArgumentException(sprintf('Hook "%s" must have exactly 5 arguments.', $hookName));
3335
}
3436

3537
$classParameterType = $reflection->getParameters()[0]->getType();
3638
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));
39+
throw new \InvalidArgumentException(sprintf('Hook "%s" must have a "%s" for first argument.', $hookName, \ReflectionClass::class));
3840
}
3941

4042
$objectParameterType = $reflection->getParameters()[1]->getType();
4143
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));
44+
throw new \InvalidArgumentException(sprintf('Hook "%s" must have an "object" for second argument.', $hookName));
45+
}
46+
47+
$nameParameterType = $reflection->getParameters()[2]->getType();
48+
if (!$nameParameterType instanceof \ReflectionNamedType || 'string' !== $nameParameterType->getName()) {
49+
throw new \InvalidArgumentException(sprintf('Hook "%s" must have a "string" for third argument.', $hookName));
4350
}
4451

45-
$valueParameterType = $reflection->getParameters()[2]->getType();
52+
$valueParameterType = $reflection->getParameters()[3]->getType();
4653
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));
54+
throw new \InvalidArgumentException(sprintf('Hook "%s" must have a "callable" for fourth argument.', $hookName));
4855
}
4956

50-
$contextParameterType = $reflection->getParameters()[3]->getType();
57+
$contextParameterType = $reflection->getParameters()[4]->getType();
5158
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));
59+
throw new \InvalidArgumentException(sprintf('Hook "%s" must have an "array" for fifth argument.', $hookName));
5360
}
5461

5562
return $hook;
5663
}
64+
65+
/**
66+
* @param class-string $className
67+
* @param array<string, mixed> $context
68+
*
69+
* @return array{0: string, 1: callable}|null
70+
*/
71+
private function findHook(string $className, string $key, array $context): ?array
72+
{
73+
if (null !== ($hook = $context['hooks'][$className][$key] ?? null)) {
74+
return [sprintf('%s: %s', $className, $key), $hook];
75+
}
76+
77+
if (null !== ($hook = $context['hooks']['property'] ?? null)) {
78+
return ['property', $hook];
79+
}
80+
81+
return null;
82+
}
5783
}

Internal/Lexer/JsonLexer.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public function tokens(mixed $resource, array $context): \Iterator
4545
}
4646

4747
if (!($type & $expectedType)) {
48-
// TODO better message
4948
throw new InvalidJsonException();
5049
}
5150

Internal/Parser/Parser.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,13 @@ private function parseObject(\Iterator $tokens, Type $type, array $context): obj
128128

129129
foreach ($this->dictParser->parse($tokens, $context) as $key) {
130130
if (null !== $hook = $this->hookExtractor->extractFromKey($reflection->getName(), $key, $context)) {
131-
$hook($reflection, $object, fn (string $type, array $context): mixed => $this->parse($tokens, Type::createFromString($type), $context), $context);
131+
$hook(
132+
$reflection,
133+
$object,
134+
$key,
135+
fn (string $type, array $context): mixed => $this->parse($tokens, Type::createFromString($type), $context),
136+
$context,
137+
);
132138

133139
continue;
134140
}

Marshaller.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
use Symfony\Component\Marshaller\Context\GenerationContextBuilderInterface;
1414
use Symfony\Component\Marshaller\Context\MarshalContextBuilderInterface;
1515
use Symfony\Component\Marshaller\Context\UnmarshalContextBuilderInterface;
16-
use Symfony\Component\Marshaller\Hook\ObjectHook;
17-
use Symfony\Component\Marshaller\Hook\PropertyHook;
18-
use Symfony\Component\Marshaller\Hook\TypeHook;
16+
use Symfony\Component\Marshaller\Hook\Marshal\ObjectHook as MarshalObjectHook;
17+
use Symfony\Component\Marshaller\Hook\Marshal\PropertyHook as MarshalPropertyHook;
18+
use Symfony\Component\Marshaller\Hook\Marshal\TypeHook as MarshalTypeHook;
19+
use Symfony\Component\Marshaller\Hook\Unmarshal\PropertyHook as UnmarshalPropertyHook;
1920
use Symfony\Component\Marshaller\Stream\StreamInterface;
2021
use Symfony\Component\Marshaller\Type\TypeExtractorInterface;
2122

@@ -87,9 +88,9 @@ private function buildGenerationContext(string $type, ?Context $context, array $
8788

8889
$rawContext += [
8990
'hooks' => [
90-
'object' => (new ObjectHook($this->typeExtractor))(...),
91-
'property' => (new PropertyHook($this->typeExtractor))(...),
92-
'type' => (new TypeHook($this->typeExtractor))(...),
91+
'object' => (new MarshalObjectHook($this->typeExtractor))(...),
92+
'property' => (new MarshalPropertyHook($this->typeExtractor))(...),
93+
'type' => (new MarshalTypeHook($this->typeExtractor))(...),
9394
],
9495
];
9596

@@ -108,12 +109,11 @@ private function buildUnmarshalContext(string $type, ?Context $context): array
108109
$context = $context ?? new Context();
109110
$rawContext = [];
110111

111-
// TODO
112-
// $rawContext += [
113-
// 'hooks' => [
114-
// 'property' => (new PropertyHook($this->typeExtractor))(...),
115-
// ],
116-
// ];
112+
$rawContext += [
113+
'hooks' => [
114+
'property' => (new UnmarshalPropertyHook($this->typeExtractor))(...),
115+
],
116+
];
117117

118118
foreach ($this->unmarshalContextBuilders as $builder) {
119119
$rawContext = $builder->build($type, $context, $rawContext);

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
# TODO
22

3+
- determine which services are internal
4+
- determine which classes are internal
5+
36
## Marshal
47
- create dedicated exceptions and wrap native ones
58

69
## Unmarshal
7-
- Unmarshal name and formatter hook (property hook)
810
- UTF-8 BOM
9-
- tests (hooks, context generation, unmarshal)
11+
- Union selector attribute?
12+
- tests (unmarshal from component)
1013
- create dedicated exceptions and wrap native ones
14+
- try catch property assignment (to not have TypeError)
15+
- collect errors mode (optional) -> throw at the end with errors and decoded
16+
object
17+
- not internal exceptions (move it to public folder)
18+
- bench in CI
1119

1220
## Questions
1321
- do we really phpstan? (we might implement it, reduced)

0 commit comments

Comments
 (0)