Skip to content

[Serializer] Serialize and deserialize from abstract classes #24375

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
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 @@ -62,6 +62,7 @@
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
Expand Down Expand Up @@ -1153,6 +1154,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
$container->removeDefinition('serializer.normalizer.dateinterval');
}

if (!class_exists(ClassDiscriminatorFromClassMetadata::class)) {
$container->removeAlias('Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface');
$container->removeDefinition('serializer.mapping.class_discriminator_resolver');
}

$chainLoader = $container->getDefinition('serializer.mapping.chain_loader');

if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@

<service id="serializer.property_accessor" alias="property_accessor" />

<!-- Discriminator Map -->
<service id="serializer.mapping.class_discriminator_resolver" class="Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata">
Copy link
Member

Choose a reason for hiding this comment

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

This service must be private.
Can you alias serializer.mapping.class_discriminator_resolver to ClassDiscriminatorResolverInterface. It will allow to use autowiring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is private, as it's in the defaults. Added the alias.

<argument type="service" id="serializer.mapping.class_metadata_factory" />
</service>
<service id="Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface" alias="serializer.mapping.class_discriminator_resolver" />

<!-- Normalizer -->
<service id="serializer.normalizer.dateinterval" class="Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer">
<!-- Run before serializer.normalizer.object -->
Expand All @@ -50,6 +56,7 @@
<argument>null</argument> <!-- name converter -->
<argument type="service" id="serializer.property_accessor" />
<argument type="service" id="property_info" on-invalid="ignore" />
<argument type="service" id="serializer.mapping.class_discriminator_resolver" on-invalid="ignore" />

<!-- Run after all custom normalizers -->
<tag name="serializer.normalizer" priority="-1000" />
Expand Down
64 changes: 64 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/DiscriminatorMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?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\Annotation;

use Symfony\Component\Serializer\Exception\InvalidArgumentException;

/**
* Annotation class for @DiscriminatorMap().
*
* @Annotation
* @Target({"CLASS"})
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class DiscriminatorMap
{
/**
* @var string
*/
private $typeProperty;

/**
* @var array
*/
private $mapping;

/**
* @param array $data
*
* @throws InvalidArgumentException
*/
public function __construct(array $data)
{
if (empty($data['typeProperty'])) {
throw new InvalidArgumentException(sprintf('Parameter "typeProperty" of annotation "%s" cannot be empty.', get_class($this)));
}

if (empty($data['mapping'])) {
throw new InvalidArgumentException(sprintf('Parameter "mapping" of annotation "%s" cannot be empty.', get_class($this)));
}

$this->typeProperty = $data['typeProperty'];
$this->mapping = $data['mapping'];
}

public function getTypeProperty(): string
{
return $this->typeProperty;
}

public function getMapping(): array
{
return $this->mapping;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?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\Mapping;

use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;

/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class ClassDiscriminatorFromClassMetadata implements ClassDiscriminatorResolverInterface
{
/**
* @var ClassMetadataFactoryInterface
*/
private $classMetadataFactory;
private $mappingForMappedObjectCache = array();

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

/**
* {@inheritdoc}
*/
public function getMappingForClass(string $class): ?ClassDiscriminatorMapping
{
if ($this->classMetadataFactory->hasMetadataFor($class)) {
return $this->classMetadataFactory->getMetadataFor($class)->getClassDiscriminatorMapping();
}

return null;
}

/**
* {@inheritdoc}
*/
public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping
{
if ($this->classMetadataFactory->hasMetadataFor($object)) {
$metadata = $this->classMetadataFactory->getMetadataFor($object);

if (null !== $metadata->getClassDiscriminatorMapping()) {
return $metadata->getClassDiscriminatorMapping();
Copy link
Member

Choose a reason for hiding this comment

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

Storing the result in a local cache will improve performance (especially in the case where reflection is involved).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, added local cache bellow 👍 (the class metadata factory is already cached)

}
}

$cacheKey = is_object($object) ? get_class($object) : $object;
if (!array_key_exists($cacheKey, $this->mappingForMappedObjectCache)) {
$this->mappingForMappedObjectCache[$cacheKey] = $this->resolveMappingForMappedObject($object);
}

return $this->mappingForMappedObjectCache[$cacheKey];
}

/**
* {@inheritdoc}
*/
public function getTypeForMappedObject($object): ?string
{
if (null === $mapping = $this->getMappingForMappedObject($object)) {
return null;
}

return $mapping->getMappedObjectType($object);
}

private function resolveMappingForMappedObject($object)
{
$reflectionClass = new \ReflectionClass($object);
if ($parentClass = $reflectionClass->getParentClass()) {
return $this->getMappingForMappedObject($parentClass->getName());
}

foreach ($reflectionClass->getInterfaceNames() as $interfaceName) {
if (null !== ($interfaceMapping = $this->getMappingForMappedObject($interfaceName))) {
return $interfaceMapping;
}
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?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\Mapping;

/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class ClassDiscriminatorMapping
{
private $typeProperty;
private $typesMapping;

public function __construct(string $typeProperty, array $typesMapping = array())
Copy link
Member

Choose a reason for hiding this comment

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

Should be compatible with PHP 5.4 to target 3.4.

{
$this->typeProperty = $typeProperty;
$this->typesMapping = $typesMapping;
}

public function getTypeProperty(): string
{
return $this->typeProperty;
}

public function getClassForType(string $type): ?string
{
if (isset($this->typesMapping[$type])) {
return $this->typesMapping[$type];
}

return null;
}

/**
* @param object|string $object
*
* @return string|null
*/
public function getMappedObjectType($object): ?string
{
foreach ($this->typesMapping as $type => $typeClass) {
if (is_a($object, $typeClass)) {
return $type;
}
}

return null;
}

public function getTypesMapping(): array
{
return $this->typesMapping;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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\Mapping;

/**
* Knows how to get the class discriminator mapping for classes and objects.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface ClassDiscriminatorResolverInterface
{
/**
* @param string $class
*
* @return ClassDiscriminatorMapping|null
*/
public function getMappingForClass(string $class): ?ClassDiscriminatorMapping;

/**
* @param object|string $object
*
* @return ClassDiscriminatorMapping|null
*/
public function getMappingForMappedObject($object): ?ClassDiscriminatorMapping;

/**
* @param object|string $object
*
* @return string|null
*/
public function getTypeForMappedObject($object): ?string;
}
35 changes: 34 additions & 1 deletion src/Symfony/Component/Serializer/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,25 @@ class ClassMetadata implements ClassMetadataInterface
*/
private $reflClass;

public function __construct(string $class)
/**
* @var ClassDiscriminatorMapping|null
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getClassDiscriminatorMapping()} instead.
*/
public $classDiscriminatorMapping;

/**
* Constructs a metadata for the given class.
*
* @param string $class
* @param ClassDiscriminatorMapping|null $classDiscriminatorMapping
*/
public function __construct(string $class, ClassDiscriminatorMapping $classDiscriminatorMapping = null)
{
$this->name = $class;
$this->classDiscriminatorMapping = $classDiscriminatorMapping;
}

/**
Expand Down Expand Up @@ -94,6 +110,22 @@ public function getReflectionClass()
return $this->reflClass;
}

/**
* {@inheritdoc}
*/
public function getClassDiscriminatorMapping()
{
return $this->classDiscriminatorMapping;
}

/**
* {@inheritdoc}
*/
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null)
{
$this->classDiscriminatorMapping = $mapping;
}

/**
* Returns the names of the properties that should be serialized.
*
Expand All @@ -104,6 +136,7 @@ public function __sleep()
return array(
'name',
'attributesMetadata',
'classDiscriminatorMapping',
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ public function merge(ClassMetadataInterface $classMetadata);
* @return \ReflectionClass
*/
public function getReflectionClass();

/**
* @return ClassDiscriminatorMapping|null
*/
public function getClassDiscriminatorMapping();

/**
* @param ClassDiscriminatorMapping|null $mapping
*/
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null);
}
Loading