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

Skip to content

Commit e2a8f22

Browse files
committed
[Serializer] Add a NormalizerGenerator
1 parent 4149eab commit e2a8f22

File tree

3 files changed

+291
-23
lines changed

3 files changed

+291
-23
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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\Dumper;
13+
14+
use Symfony\Component\Serializer\NormalizerInterface;
15+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
16+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
17+
18+
/**
19+
* @author Guilhem Niot <guilhem.niot@gmail.com>
20+
*/
21+
final class NormalizerDumper
22+
{
23+
private $classMetadataFactory;
24+
25+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
26+
{
27+
$this->classMetadataFactory = $classMetadataFactory;
28+
}
29+
30+
public function dump($class, array $context = array())
31+
{
32+
$reflectionClass = new \ReflectionClass($class);
33+
if (!isset($context['class'])) {
34+
$context['class'] = $reflectionClass->getShortName().'Normalizer';
35+
}
36+
37+
$namespaceLine = isset($context['namespace']) ? "\nnamespace {$context['namespace']};\n" : '';
38+
39+
return <<<EOL
40+
<?php
41+
$namespaceLine
42+
use Symfony\Component\Serializer\Exception\CircularReferenceException;
43+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
44+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
45+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
46+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
47+
48+
/**
49+
* This class is generated.
50+
* Please do not update it manually.
51+
*/
52+
class {$context['class']} implements NormalizerInterface, NormalizerAwareInterface
53+
{
54+
use NormalizerAwareTrait;
55+
56+
{$this->generateNormalizeMethod($reflectionClass)}
57+
58+
{$this->generateSupportsNormalizationMethod($reflectionClass)}
59+
}
60+
EOL;
61+
}
62+
63+
/**
64+
* Generates the {@see NormalizerInterface::normalize} method.
65+
*
66+
* @param \ReflectionClass $reflectionClass
67+
*
68+
* @return string
69+
*/
70+
private function generateNormalizeMethod(\ReflectionClass $reflectionClass)
71+
{
72+
return <<<EOL
73+
public function normalize(\$object, \$format = null, array \$context = array())
74+
{
75+
{$this->generateNormalizeMethodInner($reflectionClass)}
76+
}
77+
EOL;
78+
}
79+
80+
private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass)
81+
{
82+
$code = <<<EOL
83+
84+
\$objectHash = spl_object_hash(\$object);
85+
if (isset(\$context[ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT][\$objectHash])) {
86+
throw new CircularReferenceException('A circular reference has been detected (configured limit: 1).');
87+
} else {
88+
\$context[ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT][\$objectHash] = 1;
89+
}
90+
91+
\$groups = isset(\$context[ObjectNormalizer::GROUPS]) && is_array(\$context[ObjectNormalizer::GROUPS]) ? \$context[ObjectNormalizer::GROUPS] : null;
92+
93+
\$output = array();
94+
EOL;
95+
96+
$attributesMetadata = $this->classMetadataFactory->getMetadataFor($reflectionClass->name)->getAttributesMetadata();
97+
$maxDepthCode = '';
98+
foreach ($attributesMetadata as $attributeMetadata) {
99+
if (null === $maxDepth = $attributeMetadata->getMaxDepth()) {
100+
continue;
101+
}
102+
103+
$key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name);
104+
$maxDepthCode .= <<<EOL
105+
106+
if (!isset(\$context['{$key}'])) {
107+
\$context['{$key}'] = 1;
108+
} else {
109+
++\$context['{$key}'];
110+
}
111+
EOL;
112+
}
113+
114+
if ($maxDepthCode) {
115+
$code .= <<<EOL
116+
117+
if (isset(\$context[ObjectNormalizer::ENABLE_MAX_DEPTH])) {{$maxDepthCode}
118+
}
119+
120+
EOL;
121+
}
122+
123+
foreach ($attributesMetadata as $attributeMetadata) {
124+
$code .= <<<EOL
125+
126+
if ((null === \$groups
127+
EOL;
128+
129+
if ($attributeMetadata->groups) {
130+
$code .= sprintf(" || count(array_intersect(\$groups, array('%s')))", implode("', '", $attributeMetadata->groups));
131+
}
132+
$code .= ')';
133+
134+
$code .= " && (!isset(\$context['attributes']) || isset(\$context['attributes']['{$attributeMetadata->name}']) || (is_array(\$context['attributes']) && in_array('{$attributeMetadata->name}', \$context['attributes'], true)))";
135+
136+
if (null !== $maxDepth = $attributeMetadata->getMaxDepth()) {
137+
$key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name);
138+
$code .= " && (!isset(\$context['{$key}']) || {$maxDepth} >= \$context['{$key}'])";
139+
}
140+
141+
$code .= ') {';
142+
143+
$value = $this->generateGetAttributeValueExpression($attributeMetadata->name, $reflectionClass);
144+
$code .= <<<EOL
145+
146+
if (is_scalar({$value})) {
147+
\$output['{$attributeMetadata->name}'] = {$value};
148+
} else {
149+
\$subContext = \$context;
150+
if (isset(\$context['attributes']['{$attributeMetadata->name}'])) {
151+
\$subContext 10000 ['attributes'] = \$context['attributes']['{$attributeMetadata->name}'];
152+
}
153+
154+
\$output['{$attributeMetadata->name}'] = \$this->normalizer->normalize({$value}, \$format, \$subContext);
155+
}
156+
}
157+
EOL;
158+
}
159+
160+
$code .= <<<EOL
161+
162+
163+
return \$output;
164+
EOL;
165+
166+
return $code;
167+
}
168+
169+
/**
170+
* Generates an expression to get the value of an attribute.
171+
*
172+
* @param string $property
173+
* @param \ReflectionClass $reflectionClass
174+
*
175+
* @return string
176+
*/
177+
private function generateGetAttributeValueExpression($property, \ReflectionClass $reflectionClass)
178+
{
179+
$camelProp = $this->camelize($property);
180+
181+
foreach ($methods = array('get'.$camelProp, lcfirst($camelProp), 'is'.$camelProp, 'has'.$camelProp) as $method) {
182+
if ($reflectionClass->hasMethod($method) && $reflectionClass->getMethod($method)) {
183+
return sprintf('$object->%s()', $method);
184+
}
185+
}
186+
187+
if ($reflectionClass->hasProperty($property) && $reflectionClass->getProperty($property)->isPublic()) {
188+
return sprintf('$object->%s', $property);
189+
}
190+
191+
if ($reflectionClass->hasMethod('__get') && $reflectionClass->getMethod('__get')) {
192+
return sprintf('$object->__get(\'%s\')', $property);
193+
}
194+
195+
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), $reflectionClass->name));
196+
}
197+
198+
/**
199+
* Generates the {@see NormalizerInterface::supportsNormalization()} method.
200+
*
201+
* @param \ReflectionClass $reflectionClass
202+
*
203+
* @return string
204+
*/
205+
private function generateSupportsNormalizationMethod(\ReflectionClass $reflectionClass)
206+
{
207+
$instanceof = '\\'.$reflectionClass->name;
208+
209+
return <<<EOL
210+
public function supportsNormalization(\$data, \$format = null, array \$context = array())
211+
{
212+
return \$data instanceof {$instanceof};
213+
}
214+
EOL;
215+
}
216+
217+
private function camelize($string)
218+
{
219+
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
220+
}
221+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\Tests\Dumper;
13+
14+
use Doctrine\Common\Annotations\AnnotationReader;
15+
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
16+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
17+
use Symfony\Component\Serializer\Tests\Normalizer\ObjectNormalizerTest;
18+
use Symfony\Component\Serializer\Dumper\NormalizerDumper;
19+
20+
class NormalizerDumperTest extends ObjectNormalizerTest
21+
{
22+
protected function getNormalizerFor($class)
23+
{
24+
$normalizerName = 'Test'.md5($class).'Normalizer';
25+
26+
if (!class_exists($normalizerName)) {
27+
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
28+
$dumper = new NormalizerDumper($classMetadataFactory);
29+
30+
eval('?>'.$dumper->dump($class, array('class' => $normalizerName)));
31+
}
32+
33+
$normalizer = new $normalizerName();
34+
$normalizer->setNormalizer($this->serializer);
35+
36+
return $normalizer;
37+
}
38+
}

0 commit comments

Comments
 (0)
0