Skip to content

Commit

Permalink
feature #12092 [Serializer] Serialization groups support (dunglas)
Browse files Browse the repository at this point in the history
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
fabpot committed Dec 21, 2014
2 parents e840ec7 + dcf1d97 commit b1ec7e5
Show file tree
Hide file tree
Showing 30 changed files with 1,611 additions and 146 deletions.
63 changes: 63 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/Groups.php
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 src/Symfony/Component/Serializer/Exception/MappingException.php
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 src/Symfony/Component/Serializer/Mapping/ClassMetadata.php
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',
);
}
}
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, '\\');
}
}
Loading

0 comments on commit b1ec7e5

Please sign in to comment.