-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature #12092 [Serializer] Serialization groups support (dunglas)
This PR was merged into the 3.0-dev branch. Discussion ---------- [Serializer] Serialization groups support | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | License | MIT | Doc PR | symfony/symfony-docs#4675 This PR is a first attempt adding serialization groups to the `Serializer` component. Btw, it also add supports of ignored attributes for denormalization (only normalization is currently supported). Groups support is totally optional and is not enabled by default (in that case, the `Serializer` will have the current behavior). No BC spotted for now. To use it: ```php use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; class MyObj { /** * @groups({"group1", "group2"}) */ public $foo; /** * @groups({"group3"}) */ public $bar; } $obj = new MyObj(); $obj->foo = 'foo'; $obj->bar = 'bar'; $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); $normalizer = new PropertyNormalizer($classMetadataFactory); $serializer = new Serializer([$normalizer]); $data = $serializer->normalize($obj, null, ['groups' => ['group1']]); // $data = ['foo' => 'foo']; $obj2 = $serializer->denormalize(['foo' => 'foo', 'bar' => 'bar'], 'MyObj', null, ['groups' => ['group1', 'group3']); // $obj2 = MyObj(foo: 'foo', bar: 'bar') ``` Some work still need to be done: - [x] Add XML mapping - [x] Add YAML mapping - [x] Refactor tests The `ClassMetadata` code is largely inspired from the code of the `Validator` component. Duplicated code in `PropertyNormalizer` and `GetSetMethodNormalizer` has been moved in a new `AbstractNormalizer` class. This PR also make the interface of `PropertyNormalizer` fluent (like the current behavior of `GetSetMethodNormalizer`. Commits ------- dcf1d97 [Serializer] Serialization groups support
- Loading branch information
Showing
30 changed files
with
1,611 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?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 @Groups(). | ||
* | ||
* @Annotation | ||
* @Target({"PROPERTY", "METHOD"}) | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class Groups | ||
{ | ||
/** | ||
* @var array | ||
*/ | ||
private $groups; | ||
|
||
/** | ||
* @param array $data | ||
* @throws \InvalidArgumentException | ||
*/ | ||
public function __construct(array $data) | ||
{ | ||
if (!isset($data['value']) || !$data['value']) { | ||
throw new InvalidArgumentException(sprintf("Parameter of annotation '%s' cannot be empty.", get_class($this))); | ||
} | ||
|
||
if (!is_array($data['value'])) { | ||
throw new InvalidArgumentException(sprintf("Parameter of annotation '%s' must be an array of strings.", get_class($this))); | ||
} | ||
|
||
foreach ($data['value'] as $group) { | ||
if (!is_string($group)) { | ||
throw new InvalidArgumentException(sprintf("Parameter of annotation '%s' must be an array of strings.", get_class($this))); | ||
} | ||
} | ||
|
||
$this->groups = $data['value']; | ||
} | ||
|
||
/** | ||
* Gets groups | ||
* | ||
* @return array | ||
*/ | ||
public function getGroups() | ||
{ | ||
return $this->groups; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/Symfony/Component/Serializer/Exception/MappingException.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?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\Exception; | ||
|
||
/** | ||
* MappingException | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class MappingException extends RuntimeException | ||
{ | ||
} |
134 changes: 134 additions & 0 deletions
134
src/Symfony/Component/Serializer/Mapping/ClassMetadata.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
<?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; | ||
|
||
/** | ||
* Stores all metadata needed for serializing objects of specific class. | ||
* | ||
* Primarily, the metadata stores serialization groups. | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class ClassMetadata | ||
{ | ||
/** | ||
* @var string | ||
* | ||
* @internal This property is public in order to reduce the size of the | ||
* class' serialized representation. Do not access it. Use | ||
* {@link getClassName()} instead. | ||
*/ | ||
public $name; | ||
|
||
/** | ||
* @var array | ||
* | ||
* @internal This property is public in order to reduce the size of the | ||
* class' serialized representation. Do not access it. Use | ||
* {@link getGroups()} instead. | ||
*/ | ||
public $attributesGroups = array(); | ||
|
||
/** | ||
* @var \ReflectionClass | ||
*/ | ||
private $reflClass; | ||
|
||
/** | ||
* Constructs a metadata for the given class. | ||
* | ||
* @param string $class | ||
*/ | ||
public function __construct($class) | ||
{ | ||
$this->name = $class; | ||
} | ||
|
||
/** | ||
* Returns the name of the backing PHP class. | ||
* | ||
* @return string The name of the backing class. | ||
*/ | ||
public function getClassName() | ||
{ | ||
return $this->name; | ||
} | ||
|
||
/** | ||
* Gets serialization groups. | ||
* | ||
* @return array | ||
*/ | ||
public function getAttributesGroups() | ||
{ | ||
return $this->attributesGroups; | ||
} | ||
|
||
/** | ||
* Adds an attribute to a serialization group | ||
* | ||
* @param string $attribute | ||
* @param string $group | ||
* @throws \InvalidArgumentException | ||
*/ | ||
public function addAttributeGroup($attribute, $group) | ||
{ | ||
if (!is_string($attribute) || !is_string($group)) { | ||
throw new \InvalidArgumentException('Arguments must be strings.'); | ||
} | ||
|
||
if (!isset($this->groups[$group]) || !in_array($attribute, $this->attributesGroups[$group])) { | ||
$this->attributesGroups[$group][] = $attribute; | ||
} | ||
} | ||
|
||
/** | ||
* Merges attributes' groups. | ||
* | ||
* @param ClassMetadata $classMetadata | ||
*/ | ||
public function mergeAttributesGroups(ClassMetadata $classMetadata) | ||
{ | ||
foreach ($classMetadata->getAttributesGroups() as $group => $attributes) { | ||
foreach ($attributes as $attribute) { | ||
$this->addAttributeGroup($attribute, $group); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Returns a ReflectionClass instance for this class. | ||
* | ||
* @return \ReflectionClass | ||
*/ | ||
public function getReflectionClass() | ||
{ | ||
if (!$this->reflClass) { | ||
$this->reflClass = new \ReflectionClass($this->getClassName()); | ||
} | ||
|
||
return $this->reflClass; | ||
} | ||
|
||
/** | ||
* Returns the names of the properties that should be serialized. | ||
* | ||
* @return string[] | ||
*/ | ||
public function __sleep() | ||
{ | ||
return array( | ||
'name', | ||
'attributesGroups', | ||
); | ||
} | ||
} |
137 changes: 137 additions & 0 deletions
137
src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
<?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\Factory; | ||
|
||
use Doctrine\Common\Cache\Cache; | ||
use Symfony\Component\Serializer\Mapping\ClassMetadata; | ||
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; | ||
|
||
/** | ||
* Returns a {@link ClassMetadata}. | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class ClassMetadataFactory | ||
{ | ||
/** | ||
* @var LoaderInterface | ||
*/ | ||
private $loader; | ||
/** | ||
* @var Cache | ||
*/ | ||
private $cache; | ||
/** | ||
* @var array | ||
*/ | ||
private $loadedClasses; | ||
|
||
/** | ||
* @param LoaderInterface $loader | ||
* @param Cache|null $cache | ||
*/ | ||
public function __construct(LoaderInterface $loader, Cache $cache = null) | ||
{ | ||
$this->loader = $loader; | ||
$this->cache = $cache; | ||
} | ||
|
||
/** | ||
* If the method was called with the same class name (or an object of that | ||
* class) before, the same metadata instance is returned. | ||
* | ||
* If the factory was configured with a cache, this method will first look | ||
* for an existing metadata instance in the cache. If an existing instance | ||
* is found, it will be returned without further ado. | ||
* | ||
* Otherwise, a new metadata instance is created. If the factory was | ||
* configured with a loader, the metadata is passed to the | ||
* {@link LoaderInterface::loadClassMetadata()} method for further | ||
* configuration. At last, the new object is returned. | ||
* | ||
* @param string|object $value | ||
* @return ClassMetadata | ||
* @throws \InvalidArgumentException | ||
*/ | ||
public function getMetadataFor($value) | ||
{ | ||
$class = $this->getClass($value); | ||
if (!$class) { | ||
throw new \InvalidArgumentException(sprintf('Cannot create metadata for non-objects. Got: %s', gettype($value))); | ||
} | ||
|
||
if (isset($this->loadedClasses[$class])) { | ||
return $this->loadedClasses[$class]; | ||
} | ||
|
||
if ($this->cache && ($this->loadedClasses[$class] = $this->cache->fetch($class))) { | ||
return $this->loadedClasses[$class]; | ||
} | ||
|
||
if (!class_exists($class) && !interface_exists($class)) { | ||
throw new \InvalidArgumentException(sprintf('The class or interface "%s" does not exist.', $class)); | ||
} | ||
|
||
$metadata = new ClassMetadata($class); | ||
|
||
$reflClass = $metadata->getReflectionClass(); | ||
|
||
// Include constraints from the parent class | ||
if ($parent = $reflClass->getParentClass()) { | ||
$metadata->mergeAttributesGroups($this->getMetadataFor($parent->name)); | ||
} | ||
|
||
// Include constraints from all implemented interfaces | ||
foreach ($reflClass->getInterfaces() as $interface) { | ||
$metadata->mergeAttributesGroups($this->getMetadataFor($interface->name)); | ||
} | ||
|
||
if ($this->loader) { | ||
$this->loader->loadClassMetadata($metadata); | ||
} | ||
|
||
if ($this->cache) { | ||
$this->cache->save($class, $metadata); | ||
} | ||
|
||
return $this->loadedClasses[$class] = $metadata; | ||
} | ||
|
||
/** | ||
* Checks if class has metadata. | ||
* | ||
* @param mixed $value | ||
* @return bool | ||
*/ | ||
public function hasMetadataFor($value) | ||
{ | ||
$class = $this->getClass($value); | ||
|
||
return class_exists($class) || interface_exists($class); | ||
} | ||
|
||
/** | ||
* Gets a class name for a given class or instance. | ||
* | ||
* @param $value | ||
* @return string|bool | ||
*/ | ||
private function getClass($value) | ||
{ | ||
if (!is_object($value) && !is_string($value)) { | ||
return false; | ||
} | ||
|
||
return ltrim(is_object($value) ? get_class($value) : $value, '\\'); | ||
} | ||
} |
Oops, something went wrong.