Skip to content

[Serializer] Add a NormalizerDumper #22051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode)
->info('serializer configuration')
->{!class_exists(FullStack::class) && class_exists(Serializer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->booleanNode('enable_normalizer_generation')->defaultFalse()->end()
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end()
->scalarNode('name_converter')->end()
->scalarNode('circular_reference_handler')->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,12 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder

if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) {
$container->getDefinition('serializer.normalizer.object')->addMethodCall('setCircularReferenceHandler', array(new Reference($config['circular_reference_handler'])));
$container->getDefinition('serializer.normalizer.object.generated')->addMethodCall('setCircularReferenceHandler', array(new Reference($config['circular_reference_handler'])));
}

if (!$config['enable_normalizer_generation']) {
$container->removeDefinition('serializer.normalizer.object.generated');
$container->removeDefinition('serializer.normalizer.object.dumper');
}

if ($config['max_depth_handler'] ?? false) {
Expand Down
13 changes: 13 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@
</service>
<service id="Symfony\Component\Serializer\Normalizer\ObjectNormalizer" alias="serializer.normalizer.object" />

<service id="serializer.normalizer.object.generated" class="Symfony\Component\Serializer\Normalizer\GeneratedObjectNormalizer">
<argument type="service" id="serializer.normalizer.object.dumper" />
<argument>%kernel.cache_dir%</argument>
<argument>%kernel.debug%</argument>

<!-- Run after all custom normalizers but before the object normalizer -->
<tag name="serializer.normalizer" priority="-995" />
</service>

<service id="serializer.normalizer.object.dumper" class="Symfony\Component\Serializer\Dumper\NormalizerDumper">
<argument type="service" id="serializer.mapping.class_metadata_factory" />
</service>

<service id="serializer.denormalizer.array" class="Symfony\Component\Serializer\Normalizer\ArrayDenormalizer">
<!-- Run before the object normalizer -->
<tag name="serializer.normalizer" priority="-990" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ protected static function getBundleDefaultConfig()
),
'serializer' => array(
'enabled' => !class_exists(FullStack::class),
'enable_normalizer_generation' => false,
'enable_annotations' => !class_exists(FullStack::class),
'mapping' => array('paths' => array()),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ public function testDeserializeArrayOfObject()

$this->assertEquals($expected, $result);
}

/**
* @dataProvider caseProvider
*/
public function testSerializeArrayOfObject($testCase)
{
static::bootKernel(array('test_case' => $testCase));
$container = static::$kernel->getContainer();

$bar1 = new Bar();
$bar1->id = 1;
$bar2 = new Bar();
$bar2->id = 2;

$foo = new Foo();
$foo->bars = array($bar1, $bar2);

$result = $container->get('serializer')->normalize($foo);

$this->assertEquals(array('bars' => array(array('id' => 1), array('id' => 2))), $result);
}

public function caseProvider()
{
return array(array('Serializer'), array('GeneratedSerializer'));
}
}

class Foo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\Bundle\FrameworkBundle\FrameworkBundle;

return array(
new FrameworkBundle(),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
imports:
- { resource: ../config/default.yml }

framework:
serializer:
enable_normalizer_generation: true
enable_annotations: true # required to detect properties
211 changes: 211 additions & 0 deletions src/Symfony/Component/Serializer/Dumper/NormalizerDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Serializer\Dumper;

use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\NormalizerInterface;

/**
* @author Guilhem Niot <guilhem.niot@gmail.com>
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
*
* @experimental
*/
final class NormalizerDumper
{
private $classMetadataFactory;

public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
{
$this->classMetadataFactory = $classMetadataFactory;
}

public function dump(string $class, array $context = array())
{
$reflectionClass = new \ReflectionClass($class);
if (!isset($context['class'])) {
$context['class'] = $reflectionClass->getShortName().'Normalizer';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think we should introduce a ClassNameConverter here, I made one on my fork that's just doing str_replace('\\', '_', $reflectionClass->getName()) to avoid collisions.

Copy link
Contributor Author

@GuilhemN GuilhemN Mar 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the namespace option is enough (maybe we should use it by default though).

Copy link
Contributor

@soyuka soyuka Mar 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yes in fact I saw this option while working with it but I didn't try it. I'll, and yes I think we should make it default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rethinking about it, I think we should make the class option mandatory: in any case, the code using the NormalizerDumper will also have to generate the normalizer name to be able to call it.
I also don't expect the dumper to automatically put my normalizers in the same vendor than the model.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's correct. If it then works within a CompilerPass, it would need this same class name to create a Definition.

}

$namespaceLine = isset($context['namespace']) ? "\nnamespace {$context['namespace']};\n" : '';

return <<<EOL
<?php
$namespaceLine
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Normalizer\CircularReferenceTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

/**
* This class is generated.
* Please do not update it manually.
*/
class {$context['class']} implements NormalizerInterface, NormalizerAwareInterface
{
protected \$defaultContext = array(
ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT => 1,
);

use CircularReferenceTrait, NormalizerAwareTrait;

public function __construct(array \$defaultContext = array())
{
\$this->defaultContext = array_merge(\$this->defaultContext, \$defaultContext);
}

{$this->generateNormalizeMethod($reflectionClass)}

{$this->generateSupportsNormalizationMethod($reflectionClass)}
}
EOL;
}

/**
* Generates the {@see NormalizerInterface::normalize} method.
*/
private function generateNormalizeMethod(\ReflectionClass $reflectionClass): string
{
return <<<EOL
public function normalize(\$object, \$format = null, array \$context = array())
{
{$this->generateNormalizeMethodInner($reflectionClass)}
}
EOL;
}

private function generateNormalizeMethodInner(\ReflectionClass $reflectionClass): string
{
$code = <<<EOL

if (\$this->isCircularReference(\$object, \$context)) {
return \$this->handleCircularReference(\$object, \$format, \$context);
}

\$groups = isset(\$context[ObjectNormalizer::GROUPS]) && is_array(\$context[ObjectNormalizer::GROUPS]) ? \$context[ObjectNormalizer::GROUPS] : null;

\$output = array();
EOL;

$attributesMetadata = $this->classMetadataFactory->getMetadataFor($reflectionClass->name)->getAttributesMetadata();
$maxDepthCode = '';
foreach ($attributesMetadata as $attributeMetadata) {
if (null === $maxDepth = $attributeMetadata->getMaxDepth()) {
continue;
}

$key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name);
$maxDepthCode .= <<<EOL
isset(\$context['{$key}']) ? ++\$context['{$key}'] : \$context['{$key}'] = 1;
EOL;
}

if ($maxDepthCode) {
$code .= <<<EOL

if (\$context[ObjectNormalizer::ENABLE_MAX_DEPTH] ?? \$this->defaultContext[ObjectNormalizer::ENABLE_MAX_DEPTH]) {{$maxDepthCode}
}

EOL;
}

foreach ($attributesMetadata as $attributeMetadata) {
$code .= <<<EOL

\$attributes = \$context[ObjectNormalizer::ATTRIBUTES] ?? \$this->defaultContext[ObjectNormalizer::ATTRIBUTES] ?? null;
if ((null === \$groups
EOL;

if ($attributeMetadata->groups) {
$code .= sprintf(" || array_intersect(\$groups, array('%s'))", implode("', '", $attributeMetadata->groups));
}
$code .= ')';

$code .= " && (null === \$attributes || isset(\$attributes['{$attributeMetadata->name}']) || (is_array(\$attributes) && in_array('{$attributeMetadata->name}', \$attributes, true)))";

if (null !== $maxDepth = $attributeMetadata->getMaxDepth()) {
$key = sprintf(ObjectNormalizer::DEPTH_KEY_PATTERN, $reflectionClass->name, $attributeMetadata->name);
$code .= " && (!isset(\$context['{$key}']) || {$maxDepth} >= \$context['{$key}'])";
}

$code .= ') {';

$value = $this->generateGetAttributeValueExpression($attributeMetadata->name, $reflectionClass);
$code .= <<<EOL

\$value = {$value};
if (is_scalar(\$value)) {
\$output['{$attributeMetadata->name}'] = \$value;
} else {
\$subContext = \$context;
if (isset(\$attributes['{$attributeMetadata->name}'])) {
\$subContext[ObjectNormalizer::ATTRIBUTES] = \$attributes['{$attributeMetadata->name}'];
} else {
unset(\$subContext[ObjectNormalizer::ATTRIBUTES]);
}

\$output['{$attributeMetadata->name}'] = \$this->normalizer->normalize(\$value, \$format, \$subContext);
}
}
EOL;
}

$code .= <<<EOL


return \$output;
EOL;

return $code;
}

private function generateGetAttributeValueExpression(string $property, \ReflectionClass $reflectionClass): string
{
$camelProp = $this->camelize($property);

foreach ($methods = array("get$camelProp", lcfirst($camelProp), "is$camelProp", "has$camelProp)") as $method) {
if ($reflectionClass->hasMethod($method) && $reflectionClass->getMethod($method)) {
return sprintf('$object->%s()', $method);
}
}

if ($reflectionClass->hasProperty($property) && $reflectionClass->getProperty($property)->isPublic()) {
return sprintf('$object->%s', $property);
}

if ($reflectionClass->hasMethod('__get') && $reflectionClass->getMethod('__get')) {
return sprintf('$object->__get(\'%s\')', $property);
}

throw new \DomainException(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));
}

private function generateSupportsNormalizationMethod(\ReflectionClass $reflectionClass): string
{
$instanceof = '\\'.$reflectionClass->name;

return <<<EOL
public function supportsNormalization(\$data, \$format = null, array \$context = array())
{
return \$data instanceof {$instanceof};
}
EOL;
}

private function camelize(string $string): string
{
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
}
}
Loading