Skip to content

Commit 55d3491

Browse files
committed
serialize hooks
1 parent c82c917 commit 55d3491

File tree

17 files changed

+304
-647
lines changed

17 files changed

+304
-647
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/ser_des.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,6 @@
134134
service('ser_des.type_extractor'),
135135
])
136136

137-
->set('ser_des.hook.serialize.property', SerializeHook\PropertyHook::class)
138-
->args([
139-
service('ser_des.type_extractor'),
140-
])
141-
142137
->set('ser_des.hook.deserialize.object', DeserializeHook\ObjectHook::class)
143138
->args([
144139
service('ser_des.type_extractor'),

src/Symfony/Component/SerDes/Context/SerializeContext.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,6 @@ public function withObjectHook(ObjectHookInterface|callable $hook, string $class
6868
return $this->withHook($hookName, $hook);
6969
}
7070

71-
/**
72-
* @param PropertyHookInterface|callable(\ReflectionProperty, string, array<string, mixed>): array{name: string, type: string, accessor: string, context: array<string, mixed>} $hook
73-
* @param class-string|null $className
74-
*/
75-
public function withPropertyHook(PropertyHookInterface|callable $hook, string $className = null, string $propertyName = null): self
76-
{
77-
$hookName = null !== $className && null !== $propertyName ? sprintf('%s::$%s', $className, $propertyName) : 'property';
78-
79-
return $this->withHook($hookName, $hook);
80-
}
81-
8271
private function withHook(string $hookName, callable $hook): self
8372
{
8473
$hooks = $this->options['hooks'] ?? [];

src/Symfony/Component/SerDes/Hook/Serialize/ObjectHook.php

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\SerDes\Hook\Serialize;
1313

1414
use Symfony\Component\SerDes\Exception\InvalidArgumentException;
15+
use Symfony\Component\SerDes\Type\Type;
1516
use Symfony\Component\SerDes\Type\TypeExtractorInterface;
1617
use Symfony\Component\SerDes\Type\TypeGenericsHelper;
18+
use Symfony\Component\SerDes\Type\UnionType;
1719

1820
/**
1921
* @author Mathias Arlaud <[email protected]>
@@ -30,10 +32,24 @@ public function __construct(
3032
$this->typeGenericsHelper = new TypeGenericsHelper();
3133
}
3234

33-
public function __invoke(string $type, string $accessor, array $context): array
35+
public function __invoke(Type $type, string $accessor, array $properties, array $context): array
3436
{
37+
$className = $type->className();
38+
$context = $this->addGenericParameterTypes($type, $context);
39+
40+
$properties = array_filter($properties, fn (string $name): bool => $this->propertyHasMatchingGroup($className, $name, $context), \ARRAY_FILTER_USE_KEY);
41+
42+
array_walk($properties, function (array &$property, string $name) use ($className, $context): void {
43+
$formatter = $this->propertyFormatter($className, $name, $context);
44+
45+
$property['name'] = $this->propertyName($className, $name, $context);
46+
$property['type'] = $this->propertyType($className, $name, $formatter, $context);
47+
$property['accessor'] = $this->propertyAccessor($className, $name, $formatter, $property['accessor'], $context);
48+
});
49+
3550
return [
36-
'context' => $this->addGenericParameterTypes($type, $context),
51+
'properties' => $properties,
52+
'context' => $context,
3753
];
3854
}
3955

@@ -42,27 +58,117 @@ public function __invoke(string $type, string $accessor, array $context): array
4258
*
4359
* @return array<string, mixed>
4460
*/
45-
private function addGenericParameterTypes(string $type, array $context): array
61+
private function addGenericParameterTypes(Type $type, array $context): array
4662
{
47-
$generics = $this->typeGenericsHelper->extractGenerics($type);
63+
$className = $type->className();
64+
$genericParameterTypes = $type->genericParameterTypes();
4865

49-
$genericType = $generics['genericType'];
50-
$genericParameters = $generics['genericParameters'];
66+
$templates = $this->typeExtractor->extractTemplateFromClass(new \ReflectionClass($className));
5167

52-
if (!class_exists($genericType)) {
53-
return $context;
68+
if (\count($templates) !== \count($genericParameterTypes)) {
69+
throw new InvalidArgumentException(sprintf('Given %d generic parameters in "%s", but %d templates are defined in "%s".', \count($genericParameterTypes), (string) $type, \count($templates), $className));
5470
}
5571

56-
$templates = $this->typeExtractor->extractTemplateFromClass(new \ReflectionClass($genericType));
72+
foreach ($genericParameterTypes as $i => $genericParameterType) {
73+
$context['_symfony']['generic_parameter_types'][$className][$templates[$i]] = $genericParameterType;
74+
}
75+
76+
return $context;
77+
}
5778

58-
if (\count($templates) !== \count($genericParameters)) {
59-
throw new InvalidArgumentException(sprintf('Given %d generic parameters in "%s", but %d templates are defined in "%s".', \count($genericParameters), $type, \count($templates), $genericType));
79+
/**
80+
* @param class-string $className
81+
* @param array<string, mixed> $context
82+
*/
83+
private function propertyHasMatchingGroup(string $className, string $name, array $context): bool
84+
{
85+
if (!isset($context['groups'])) {
86+
return true;
6087
}
6188

62-
foreach ($genericParameters as $i => $genericParameter) {
63-
$context['_symfony']['generic_parameter_types'][$genericType][$templates[$i]] = $genericParameter;
89+
foreach ($context['groups'] as $group) {
90+
if (isset($context['_symfony']['serialize']['property_groups'][$className][$name][$group])) {
91+
return true;
92+
}
6493
}
6594

66-
return $context;
95+
return false;
96+
}
97+
98+
/**
99+
* @param class-string $className
100+
* @param array<string, mixed> $context
101+
*/
102+
private function propertyName(string $className, string $name, array $context): string
103+
{
104+
if (isset($context['_symfony']['serialize']['property_name'][$className][$name])) {
105+
return $context['_symfony']['serialize']['property_name'][$className][$name];
106+
}
107+
108+
return $name;
109+
}
110+
111+
/**
112+
* @param class-string $className
113+
* @param array<string, mixed> $context
114+
*/
115+
private function propertyFormatter(string $className, string $name, array $context): ?\ReflectionFunction
116+
{
117+
if (isset($context['_symfony']['serialize']['property_formatter'][$className][$name])) {
118+
return new \ReflectionFunction(\Closure::fromCallable($context['_symfony']['serialize']['property_formatter'][$className][$name]));
119+
}
120+
121+
return null;
122+
}
123+
124+
/**
125+
* @param class-string $className
126+
* @param array<string, mixed> $context
127+
*/
128+
private function propertyType(string $className, string $name, ?\ReflectionFunction $formatter, array $context): Type|UnionType
129+
{
130+
$type = null === $formatter
131+
? $this->typeExtractor->extractFromProperty(new \ReflectionProperty($className, $name))
132+
: $this->typeExtractor->extractFromFunctionReturn($formatter);
133+
134+
// if method doesn't belong to the property class, ignore generic search
135+
if (null !== $formatter && $formatter->getClosureScopeClass()?->getName() !== $className) {
136+
return $type;
137+
}
138+
139+
if ([] !== ($genericTypes = $context['_symfony']['generic_parameter_types'][$className] ?? [])) {
140+
$type = $this->typeGenericsHelper->replaceGenericTypes($type, $genericTypes);
141+
}
142+
143+
return $type;
144+
}
145+
146+
/**
147+
* @param class-string $className
148+
* @param array<string, mixed> $context
149+
*/
150+
private function propertyAccessor(string $className, string $name, ?\ReflectionFunction $formatter, string $accessor, array $context): string
151+
{
152+
if (null === $formatter) {
153+
return $accessor;
154+
}
155+
156+
if (!$formatter->getClosureScopeClass()?->hasMethod($formatter->getName()) || !$formatter->isStatic()) {
157+
throw new InvalidArgumentException(sprintf('Property formatter "%s" must be a static method.', sprintf('%s::$%s', $className, $name)));
158+
}
159+
160+
if (($returnType = $formatter->getReturnType()) instanceof \ReflectionNamedType && ('void' === $returnType->getName() || 'never' === $returnType->getName())) {
161+
throw new InvalidArgumentException(sprintf('Return type of property formatter "%s" must not be "void" nor "never".', sprintf('%s::$%s', $className, $name)));
162+
}
163+
164+
if (null !== ($contextParameter = $formatter->getParameters()[1] ?? null)) {
165+
$contextParameterType = $contextParameter->getType();
166+
167+
if (!$contextParameterType instanceof \ReflectionNamedType || 'array' !== $contextParameterType->getName()) {
168+
throw new InvalidArgumentException(sprintf('Second argument of property formatter "%s" must be an array.', sprintf('%s::$%s', $className, $name)));
169+
}
170+
}
171+
172+
return sprintf('%s::%s(%s, $context)', $formatter->getClosureScopeClass()->getName(), $formatter->getName(), $accessor);
67173
}
68174
}

src/Symfony/Component/SerDes/Hook/Serialize/ObjectHookInterface.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\Component\SerDes\Hook\Serialize;
1313

14+
use Symfony\Component\SerDes\Type\Type;
15+
use Symfony\Component\SerDes\Type\UnionType;
16+
1417
/**
1518
* @author Mathias Arlaud <[email protected]>
1619
*
@@ -19,9 +22,10 @@
1922
interface ObjectHookInterface
2023
{
2124
/**
22-
* @param array<string, mixed> $context
25+
* @param array<string, array{name: string, type: Type|UnionType, accessor: string}> $properties
26+
* @param array<string, mixed> $context
2327
*
24-
* @return array{type?: string, accessor?: string, context?: array<string, mixed>}
28+
* @return array{properties?: array<string, array{name?: string, type?: Type|UnionType, accessor?: string}>, context?: array<string, mixed>}
2529
*/
26-
public function __invoke(string $type, string $accessor, array $context): array;
30+
public function __invoke(Type $type, string $accessor, array $properties, array $context): array;
2731
}

src/Symfony/Component/SerDes/Hook/Serialize/PropertyHook.php

Lines changed: 0 additions & 125 deletions
This file was deleted.

src/Symfony/Component/SerDes/Hook/Serialize/PropertyHookInterface.php

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)