8000 [Serializer] Add a NormalizerGenerator · symfony/symfony@258cd70 · GitHub
[go: up one dir, main page]

Skip to content

Commit 258cd70

Browse files
committed
[Serializer] Add a NormalizerGenerator
1 parent c12727d commit 258cd70

File tree

3 files changed

+271
-2
lines changed

3 files changed

+271
-2
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@
9595
"symfony/polyfill-apcu": "~1.1",
9696
"symfony/security-acl": "~2.8|~3.0",
9797
"phpdocumentor/reflection-docblock": "^3.0",
98-
"sensio/framework-extra-bundle": "^3.0.2"
98+
"sensio/framework-extra-bundle": "^3.0.2",
99+
"nikic/php-parser": "~2.0"
99100
},
100101
"conflict": {
101102
"phpdocumentor/reflection-docblock": "<3.0",
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\AstGenerator;
13+
14+
use PhpParser\Comment;
15+
use PhpParser\Node\Arg;
16+
use PhpParser\Node\Name;
17+
use PhpParser\Node\Param;
18+
use PhpParser\Node\Stmt;
19+
use PhpParser\Node\Expr;
20+
use PhpParser\Node\Scalar;
21+
use Symfony\Component\Serializer\Exception\CircularReferenceException;
22+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
23+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
24+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
25+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
26+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
27+
28+
final class NormalizerGenerator
29+
{
30+
private $classMetadataFactory;
31+
32+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
33+
{
34+
$this->classMetadataFactory = $classMetadataFactory;
35+
}
36+
37+
public function generate($class, array $context = array())
38+
{
39+
$class = new \ReflectionClass($class);
40+
if (!isset($context['name'])) {
41+
$context['name'] = $class->getShortName().'Normalizer';
42+
}
43+
44+
return array(new Stmt\Class_(
45+
new Name($context['name']),
46+
array(
47+
'type' => Stmt\Class_::MODIFIER_FINAL,
48+
'stmts' => array(
49+
new Stmt\Use_(array(
50+
new Name(NormalizerAwareTrait::class),
51+
)),
52+
$this->createNormalizeMethod($class),
53+
$this->createSupportsNormalizationMethod($class),
54+
),
55+
'implements' => array(
56+
new Name(NormalizerInterface::class),
57+
new Name(NormalizerAwareInterface::class),
58+
),
59+
),
60+
array(
61+
'comments' => array(new Comment("/**\n * This class is generated.\n * Please do not update it manually.\n */")),
62+
)
63+
));
64+
}
65+
66+
/**
67+
* Create the normalization method.
68+
*
69+
* @param string $class Class to create normalization from
70+
* @param array $context Context of generation
71+
*
72+
* @return Stmt\ClassMethod
73+
*/
74+
private function createNormalizeMethod(\ReflectionClass $class)
75+
{
76+
return new Stmt\ClassMethod('normalize', array(
77+
'type' => Stmt\Class_::MODIFIER_PUBLIC,
78+
'params' => array(
79+
new Param('object'),
80+
new Param('format', new Expr\ConstFetch(new Name('null'))),
81+
new Param('context', new Expr\Array_(), 'array'),
82+
),
83+
'stmts' => array_merge(
84+
array(new Expr\Assign(new Expr\Variable('output'), new Expr\Array_())),
85+
$this->generateNormalizeMethodInner($class),
86+
array(new Stmt\Return_(new Expr\Variable('output')))
87+
),
88+
));
89+
}
90+
91+
private function generateNormalizeMethodInner(\ReflectionClass $class)
92+
{
93+
$metadata = $this->classMetadataFactory->getMetadataFor($class->name);
94+
95+
$groupsContext = new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(ObjectNormalizer::GROUPS));
96+
$stmts = array();
97+
98+
$stmts[] = new Expr\Assign(new Expr\Variable('objectHash'), new Expr\FuncCall(new Name('spl_object_hash'), array(new Expr\Variable('object'))));
99+
$stmts[] = new Stmt\If_(
100+
new Expr\Isset_(array($circularReferenceContext = new Expr\ArrayDimFetch(new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT)), new Expr\Variable('objectHash')))),
101+
array(
102+
'stmts' => array(
103+
new Stmt\Throw_(new Expr\New_(new Name(CircularReferenceException::class), array(new Arg(new Scalar\String_('A circular reference has been detected (configured limit: 1).'))))),
104+
),
105+
'else' => new Stmt\Else_(array(
106+
new Expr\Assign($circularReferenceContext, new Scalar\LNumber(1)),
107+
)),
108+
)
109+
);
110+
111+
$stmts[] = new Expr\Assign(
112+
new Expr\Variable('groups'),
113+
new Expr\Ternary(
114+
new Expr\BinaryOp\BooleanAnd(
115+
new Expr\Isset_(array($groupsContext)),
116+
new Expr\FuncCall(new Name('is_array'), array(new Arg($groupsContext)))
117+
),
118+
$groupsContext,
119+
new Expr\ConstFetch(new Name('null'))
120+
)
121+
);
122+
123+
$maxDepthStmts = array();
124+
foreach ($metadata->attributesMetadata as $attribute) {
125+
if (null === $maxDepth = $attribute->getMaxDepth()) {
126+
continue;
127+
}
128+
129+
$attributeMaxDepthContext = new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $class->name, $attribute->name)));
130+
$maxDepthStmts[] = new Stmt\If_(
131+
new Expr\BooleanNot(new Expr\Isset_(array($attributeMaxDepthContext))),
132+
array(
133+
'stmts' => array(new Expr\Assign($attributeMaxDepthContext, new Scalar\LNumber(1))),
134+
'elseifs' => array(new Stmt\ElseIf_(
135+
new Expr\BinaryOp\NotIdentical(new Scalar\LNumber($maxDepth), $attributeMaxDepthContext),
136+
array(new Expr\PreInc($attributeMaxDepthContext))
137+
)),
138+
)
139+
);
140+
}
141+
142+
if (0 !== count($maxDepthStmts)) {
143+
$stmts[] = new Stmt\If_(
144+
new Expr\Isset_(array(new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(ObjectNormalizer::ENABLE_MAX_DEPTH)))),
145+
array('stmts' => $maxDepthStmts)
146+
);
147+
}
148+
149+
foreach ($metadata->attributesMetadata as $attribute) {
150+
$attributesContext = new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_('attributes'));
151+
$condition = new Expr\BinaryOp\BooleanOr(
152+
new Expr\BooleanNot(new Expr\Isset_(array(new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_('attributes'))))),
153+
new Expr\BinaryOp\BooleanOr(
154+
new Expr\Isset_(array(new Expr\ArrayDimFetch($attributesContext, new Scalar\String_($attribute->name)))),
155+
new Expr\BinaryOp\BooleanAnd(
156+
new Expr\FuncCall(new Name('is_array'), array(new Arg($attributesContext))),
157+
new Expr\FuncCall(new Name('in_array'), array(new Arg(new Scalar\String_($attribute->name)), new Arg($attributesContext), new Arg(new Expr\ConstFetch(new Name('true')))))
158+
)
159+
)
160+
);
161+
162+
$groupsCondition = new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), new Expr\Variable('groups'));
163+
if ($attribute->groups) {
164+
$groupsCondition = new Expr\BinaryOp\BooleanOr(
165+
$groupsCondition,
166+
new Expr\FuncCall(new Name('count'), array(new Arg(
167+
new Expr\FuncCall(new Name('array_intersect'), array(
168+
new Arg(new Expr\Array_(
169+
array_map(function ($group) {
170+
return new Expr\ArrayItem(new Scalar\String_($group));
171+
}, $attribute->groups)
172+
)),
173+
new Arg(new Expr\Variable('groups')),
174+
))
175+
)))
176+
);
177+
}
178+
$condition = new Expr\BinaryOp\BooleanAnd($groupsCondition, $condition);
179+
180+
if (null !== $maxDepth = $attribute->getMaxDepth()) {
181+
$attributeMaxDepthContext = new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $class->name, $attribute->name)));
182+
183+
$condition = new Expr\BinaryOp\BooleanAnd(
184+
$condition,
185+
new Expr\BinaryOp\BooleanOr(
186+
new Expr\BooleanNot(new Expr\Isset_(array($attributeMaxDepthContext))),
187+
new Expr\BinaryOp\NotIdentical(new Scalar\LNumber($maxDepth), $attributeMaxDepthContext)
188+
)
189+
);
190+
}
191+
192+
$value = $this->getAttributeValue($attribute->name, $class);
193+
$output = new Expr\ArrayDimFetch(new Expr\Variable('output'), new Scalar\String_($attribute->name));
194+
$ifUnknownType = new Stmt\If_(
195+
new Expr\FuncCall(new Name('is_scalar'), array(new Arg($value))),
196+
array(
197+
'stmts' => array(new Expr\Assign($output, $value)),
198+
'else' => new Stmt\Else_(array(
199+
new Expr\Assign(new Expr\Variable('subContext'), new Expr\Variable('context')),
200+
new Stmt\If_(
201+
new Expr\Isset_(array($subAttributes = new Expr\ArrayDimFetch($attributesContext, new Scalar\String_($attribute->name)))),
202+
array('stmts' => array(
203+
new Expr\Assign(
204+
new Expr\ArrayDimFetch(new Expr\Variable('subContext'), new Scalar\String_('attributes')),
205+
$subAttributes
206+
),
207+
))
208+
),
209+
new Expr\Assign($output, new Expr\MethodCall(new Expr\PropertyFetch(new Expr\Variable('this'), 'normalizer'), 'normalize', array(new Arg($value), new Expr\Variable('format'), new Expr\Variable('subContext'))))
210+
)),
211+
)
212+
);
213+
214+
$stmts[] = new Stmt\If_($condition, array(
215+
'stmts' => array($ifUnknownType),
216+
));
217+
}
218+
219+
return $stmts;
220+
}
221+
222+
private function getAttributeValue($property, \ReflectionClass $class)
223+
{
224+
$camelProp = $this->camelize($property);
225+
226+
foreach ($methods = array('get'.$camelProp, lcfirst($camelProp), 'is'.$camelProp, 'has'.$camelProp) as $method) {
227+
if ($class->hasMethod($method) && $class->getMethod($method)) {
228+
return new Expr\MethodCall(new Expr\Variable('object'), $method);
229+
}
230+
}
231+
232+
if ($class->hasProperty($property) && $class->getProperty($property)->isPublic()) {
233+
return new Expr\PropertyFetch(new Expr\Variable('object'), $property);
234+
}
235+
236+
if ($class->hasMethod('__get') && $class->getMethod('__get')) {
237+
return new Expr\MethodCall(new Expr\Variable('object'), '__get');
238+
}
239+
240+
throw new \LogicException(sprintf('Neither the property "%s" nor one of the methods "%s()", "__get()" exist and have public access in class "%s".', $property, implode('()", "', $methods), $class->name));
241+
}
242+
243+
/**
244+
* Create method to check if normalization is supported.
245+
*
246+
* @return Stmt\ClassMethod
247+
*/
248+
private function createSupportsNormalizationMethod($class)
249+
{
250+
return new Stmt\ClassMethod('supportsNormalization', array(
251+
'type' => Stmt\Class_::MODIFIER_PUBLIC,
252+
'params' => array(
253+
new Param('data'),
254+
new Param('format', new Expr\ConstFetch(new Name('null'))),
255+
new Param('context', new Expr\Array_(), 'array'),
256+
),
257+
'stmts' => array(
258+
new Stmt\Return_(new Expr\Instanceof_(new Expr\Variable('data'), new Name('\\'.$class->name))),
259+
),
260+
));
261+
}
262+
263+
private function camelize($string)
264+
{
265+
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
266+
}
267+
}

src/Symfony/Component/Serializer/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"doctrine/annotations": "~1.0",
2929
"symfony/dependency-injection": "~3.2",
3030
"doctrine/cache": "~1.0",
31-
"phpdocumentor/reflection-docblock": "~3.0"
31+
"phpdocumentor/reflection-docblock": "~3.0",
32+
"nikic/php-parser": "~2.0"
3233
},
3334
"conflict": {
3435
"symfony/dependency-injection": "<3.2",

0 commit comments

Comments
 (0)
0