feature #12092 [Serializer] Serialization groups support (dunglas)

This PR was submitted for the master branch but it was merged into the 2.7 branch instead (closes #12092).

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
-------

57a191b [Serializer] Serialization groups support
This commit is contained in:
Fabien Potencier 2014-12-21 23:11:53 +01:00
commit 64d6ddb870
30 changed files with 1611 additions and 146 deletions

View File

@ -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;
}
}

View File

@ -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
{
}

View File

@ -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',
);
}
}

View File

@ -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, '\\');
}
}

View File

@ -0,0 +1,81 @@
<?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\Loader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* Annotation loader.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AnnotationLoader implements LoaderInterface
{
/**
* @var Reader
*/
private $reader;
/**
* @param Reader $reader
*/
public function __construct(Reader $reader)
{
$this->reader = $reader;
}
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata)
{
$reflClass = $metadata->getReflectionClass();
$className = $reflClass->name;
$loaded = false;
foreach ($reflClass->getProperties() as $property) {
if ($property->getDeclaringClass()->name == $className) {
foreach ($this->reader->getPropertyAnnotations($property) as $groups) {
if ($groups instanceof Groups) {
foreach ($groups->getGroups() as $group) {
$metadata->addAttributeGroup($property->name, $group);
}
}
$loaded = true;
}
}
}
foreach ($reflClass->getMethods() as $method) {
if ($method->getDeclaringClass()->name == $className) {
foreach ($this->reader->getMethodAnnotations($method) as $groups) {
if ($groups instanceof Groups) {
if (preg_match('/^(get|is)(.+)$/i', $method->name, $matches)) {
foreach ($groups->getGroups() as $group) {
$metadata->addAttributeGroup(lcfirst($matches[2]), $group);
}
} else {
throw new \BadMethodCallException(sprintf('Groups on "%s::%s" cannot be added. Groups can only be added on methods beginning with "get" or "is".', $className, $method->name));
}
}
$loaded = true;
}
}
}
return $loaded;
}
}

View File

@ -0,0 +1,40 @@
<?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\Loader;
use Symfony\Component\Serializer\Exception\MappingException;
abstract class FileLoader implements LoaderInterface
{
protected $file;
/**
* Constructor.
*
* @param string $file The mapping file to load
*
* @throws MappingException if the mapping file does not exist
* @throws MappingException if the mapping file is not readable
*/
public function __construct($file)
{
if (!is_file($file)) {
throw new MappingException(sprintf('The mapping file %s does not exist', $file));
}
if (!is_readable($file)) {
throw new MappingException(sprintf('The mapping file %s is not readable', $file));
}
$this->file = $file;
}
}

View File

@ -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\Loader;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* Calls multiple LoaderInterface instances in a chain
*
* This class accepts multiple instances of LoaderInterface to be passed to the
* constructor. When loadClassMetadata() is called, the same method is called
* in <em>all</em> of these loaders, regardless of whether any of them was
* successful or not.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class LoaderChain implements LoaderInterface
{
protected $loaders;
/**
* Accepts a list of LoaderInterface instances
*
* @param LoaderInterface[] $loaders An array of LoaderInterface instances
*
* @throws MappingException If any of the loaders does not implement LoaderInterface
*/
public function __construct(array $loaders)
{
foreach ($loaders as $loader) {
if (!$loader instanceof LoaderInterface) {
throw new MappingException(sprintf('Class %s is expected to implement LoaderInterface', get_class($loader)));
}
}
$this->loaders = $loaders;
}
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata)
{
$success = false;
foreach ($this->loaders as $loader) {
$success = $loader->loadClassMetadata($metadata) || $success;
}
return $success;
}
}

View File

@ -0,0 +1,31 @@
<?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\Loader;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* Loads class metadata.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface LoaderInterface
{
/**
* Load class metadata.
*
* @param ClassMetadata $metadata A metadata
*
* @return bool
*/
public function loadClassMetadata(ClassMetadata $metadata);
}

View File

@ -0,0 +1,80 @@
<?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\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* Loads XML mapping files.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class XmlFileLoader extends FileLoader
{
/**
* An array of SimpleXMLElement instances.
*
* @var \SimpleXMLElement[]|null
*/
private $classes;
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata)
{
if (null === $this->classes) {
$this->classes = array();
$xml = $this->parseFile($this->file);
foreach ($xml->class as $class) {
$this->classes[(string) $class['name']] = $class;
}
}
if (isset($this->classes[$metadata->getClassName()])) {
$xml = $this->classes[$metadata->getClassName()];
foreach ($xml->attribute as $attribute) {
foreach ($attribute->group as $group) {
$metadata->addAttributeGroup((string) $attribute['name'], (string) $group);
}
}
return true;
}
return false;
}
/**
* Parse a XML File.
*
* @param string $file Path of file
*
* @return \SimpleXMLElement
*
* @throws MappingException
*/
private function parseFile($file)
{
try {
$dom = XmlUtils::loadFile($file, __DIR__.'/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd');
} catch (\Exception $e) {
throw new MappingException($e->getMessage(), $e->getCode(), $e);
}
return simplexml_import_dom($dom);
}
}

View File

@ -0,0 +1,80 @@
<?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\Loader;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Yaml\Parser;
/**
* YAML File Loader
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class YamlFileLoader extends FileLoader
{
private $yamlParser;
/**
* An array of YAML class descriptions
*
* @var array
*/
private $classes = null;
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata)
{
if (null === $this->classes) {
if (!stream_is_local($this->file)) {
throw new MappingException(sprintf('This is not a local file "%s".', $this->file));
}
if (null === $this->yamlParser) {
$this->yamlParser = new Parser();
}
$classes = $this->yamlParser->parse(file_get_contents($this->file));
if (empty($classes)) {
return false;
}
// not an array
if (!is_array($classes)) {
throw new MappingException(sprintf('The file "%s" must contain a YAML array.', $this->file));
}
$this->classes = $classes;
}
if (isset($this->classes[$metadata->getClassName()])) {
$yaml = $this->classes[$metadata->getClassName()];
if (isset($yaml['attributes']) && is_array($yaml['attributes'])) {
foreach ($yaml['attributes'] as $attribute => $data) {
if (isset($data['groups'])) {
foreach ($data['groups'] as $group) {
$metadata->addAttributeGroup($attribute, $group);
}
}
}
}
return true;
}
return false;
}
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" ?>
<xsd:schema xmlns="http://symfony.com/schema/dic/serializer-mapping"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema/dic/serializer-mapping"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation><![CDATA[
Symfony Serializer Mapping Schema, version 1.0
Authors: Kévin Dunglas
A serializer mapping connects attributes with serialization groups.
]]></xsd:documentation>
</xsd:annotation>
<xsd:element name="serializer" type="serializer" />
<xsd:complexType name="serializer">
<xsd:annotation>
<xsd:documentation><![CDATA[
The root element of the serializer mapping definition.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="class" type="class" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="class">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains serialization groups for a single class.
Nested elements may be class property and/or getter definitions.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="attribute" type="attribute" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="attribute">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains serialization groups for a attributes. The name of the attribute should be given in the "name" option.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="group" type="xsd:string" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,130 @@
<?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\Normalizer;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
/**
* Normalizer implementation.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
{
protected $classMetadataFactory;
protected $callbacks = array();
protected $ignoredAttributes = array();
protected $camelizedAttributes = array();
/**
* Sets the {@link ClassMetadataFactory} to use.
*
* @param ClassMetadataFactory $classMetadataFactory
*/
public function __construct(ClassMetadataFactory $classMetadataFactory = null)
{
$this->classMetadataFactory = $classMetadataFactory;
}
/**
* Set normalization callbacks.
*
* @param array $callbacks help normalize the result
*
* @return self
*
* @throws InvalidArgumentException if a non-callable callback is set
*/
public function setCallbacks(array $callbacks)
{
foreach ($callbacks as $attribute => $callback) {
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
'The given callback for attribute "%s" is not callable.',
$attribute
));
}
}
$this->callbacks = $callbacks;
return $this;
}
/**
* Set ignored attributes for normalization and denormalization.
*
* @param array $ignoredAttributes
*
* @return self
*/
public function setIgnoredAttributes(array $ignoredAttributes)
{
$this->ignoredAttributes = $ignoredAttributes;
return $this;
}
/**
* Set attributes to be camelized on denormalize.
*
* @param array $camelizedAttributes
*
* @return self
*/
public function setCamelizedAttributes(array $camelizedAttributes)
{
$this->camelizedAttributes = $camelizedAttributes;
return $this;
}
/**
* Format an attribute name, for example to convert a snake_case name to camelCase.
*
* @param string $attributeName
* @return string
*/
protected function formatAttribute($attributeName)
{
if (in_array($attributeName, $this->camelizedAttributes)) {
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
}, $attributeName);
}
return $attributeName;
}
/**
* Gets attributes to normalize using groups.
*
* @param string|object $classOrObject
* @param array $context
* @return array|bool
*/
protected function getAllowedAttributes($classOrObject, array $context)
{
if (!$this->classMetadataFactory || !isset($context['groups']) || !is_array($context['groups'])) {
return false;
}
$allowedAttributes = array();
foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesGroups() as $group => $attributes) {
if (in_array($group, $context['groups'])) {
$allowedAttributes = array_merge($allowedAttributes, $attributes);
}
}
return array_unique($allowedAttributes);
}
}

View File

@ -36,13 +36,10 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
* @author Nils Adermann <naderman@naderman.de>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GetSetMethodNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
class GetSetMethodNormalizer extends AbstractNormalizer
{
protected $circularReferenceLimit = 1;
protected $circularReferenceHandler;
protected $callbacks = array();
protected $ignoredAttributes = array();
protected $camelizedAttributes = array();
/**
* Set circular reference limit.
@ -78,55 +75,6 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal
return $this;
}
/**
* Set normalization callbacks.
*
* @param callable[] $callbacks help normalize the result
*
* @throws InvalidArgumentException if a non-callable callback is set
*
* @return self
*/
public function setCallbacks(array $callbacks)
{
foreach ($callbacks as $attribute => $callback) {
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute));
}
}
$this->callbacks = $callbacks;
return $this;
}
/**
* Set ignored attributes for normalization.
*
* @param array $ignoredAttributes
*
* @return self
*/
public function setIgnoredAttributes(array $ignoredAttributes)
{
$this->ignoredAttributes = $ignoredAttributes;
return $this;
}
/**
* Set attributes to be camelized on denormalize.
*
* @param array $camelizedAttributes
*
* @return self
*/
public function setCamelizedAttributes(array $camelizedAttributes)
{
$this->camelizedAttributes = $camelizedAttributes;
return $this;
}
/**
* {@inheritdoc}
*/
@ -152,6 +100,7 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal
$reflectionObject = new \ReflectionObject($object);
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
$allowedAttributes = $this->getAllowedAttributes($object, $context);
$attributes = array();
foreach ($reflectionMethods as $method) {
@ -162,6 +111,10 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal
continue;
}
if (false !== $allowedAttributes && !in_array($attributeName, $allowedAttributes)) {
continue;
}
$attributeValue = $method->invoke($object);
if (array_key_exists($attributeName, $this->callbacks)) {
$attributeValue = call_user_func($this->callbacks[$attributeName], $attributeValue);
@ -186,6 +139,8 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
$allowedAttributes = $this->getAllowedAttributes($class, $context);
if (is_array($data) || is_object($data) && $data instanceof \ArrayAccess) {
$normalizedData = $data;
} elseif (is_object($data)) {
@ -208,7 +163,9 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal
foreach ($constructorParameters as $constructorParameter) {
$paramName = lcfirst($this->formatAttribute($constructorParameter->name));
if (isset($normalizedData[$paramName])) {
$allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
$ignored = in_array($paramName, $this->ignoredAttributes);
if ($allowed && !$ignored && isset($normalizedData[$paramName])) {
$params[] = $normalizedData[$paramName];
// don't run set for a parameter passed to the constructor
unset($normalizedData[$paramName]);
@ -229,38 +186,21 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal
}
foreach ($normalizedData as $attribute => $value) {
$setter = 'set'.$this->formatAttribute($attribute);
$allowed = $allowedAttributes === false || in_array($attribute, $allowedAttributes);
$ignored = in_array($attribute, $this->ignoredAttributes);
if (method_exists($object, $setter)) {
$object->$setter($value);
if ($allowed && !$ignored) {
$setter = 'set'.$this->formatAttribute($attribute);
if (method_exists($object, $setter)) {
$object->$setter($value);
}
}
}
return $object;
}
/**
* Format attribute name to access parameters or methods
* As option, if attribute name is found on camelizedAttributes array
* returns attribute name in camelcase format.
*
* @param string $attributeName
*
* @return string
*/
protected function formatAttribute($attributeName)
{
if (in_array($attributeName, $this->camelizedAttributes)) {
return preg_replace_callback(
'/(^|_|\.)+(.)/', function ($match) {
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
}, $attributeName
);
}
return $attributeName;
}
/**
* {@inheritdoc}
*/

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\RuntimeException;
/**
@ -29,53 +28,10 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
* property with the corresponding name exists. If found, the property gets the value.
*
* @author Matthieu Napoli <matthieu@mnapoli.fr>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
class PropertyNormalizer extends AbstractNormalizer
{
private $callbacks = array();
private $ignoredAttributes = array();
private $camelizedAttributes = array();
/**
* Set normalization callbacks
*
* @param array $callbacks help normalize the result
*
* @throws InvalidArgumentException if a non-callable callback is set
*/
public function setCallbacks(array $callbacks)
{
foreach ($callbacks as $attribute => $callback) {
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
'The given callback for attribute "%s" is not callable.',
$attribute
));
}
}
$this->callbacks = $callbacks;
}
/**
* Set ignored attributes for normalization.
*
* @param array $ignoredAttributes
*/
public function setIgnoredAttributes(array $ignoredAttributes)
{
$this->ignoredAttributes = $ignoredAttributes;
}
/**
* Set attributes to be camelized on denormalize
*
* @param array $camelizedAttributes
*/
public function setCamelizedAttributes(array $camelizedAttributes)
{
$this->camelizedAttributes = $camelizedAttributes;
}
/**
* {@inheritdoc}
*/
@ -83,12 +39,17 @@ class PropertyNormalizer extends SerializerAwareNormalizer implements Normalizer
{
$reflectionObject = new \ReflectionObject($object);
$attributes = array();
$allowedAttributes = $this->getAllowedAttributes($object, $context);
foreach ($reflectionObject->getProperties() as $property) {
if (in_array($property->name, $this->ignoredAttributes)) {
continue;
}
if (false !== $allowedAttributes && !in_array($property->name, $allowedAttributes)) {
continue;
}
// Override visibility
if (! $property->isPublic()) {
$property->setAccessible(true);
@ -114,6 +75,8 @@ class PropertyNormalizer extends SerializerAwareNormalizer implements Normalizer
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
$allowedAttributes = $this->getAllowedAttributes($class, $context);
$reflectionClass = new \ReflectionClass($class);
$constructor = $reflectionClass->getConstructor();
@ -124,7 +87,9 @@ class PropertyNormalizer extends SerializerAwareNormalizer implements Normalizer
foreach ($constructorParameters as $constructorParameter) {
$paramName = lcfirst($this->formatAttribute($constructorParameter->name));
if (isset($data[$paramName])) {
$allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
$ignored = in_array($paramName, $this->ignoredAttributes);
if ($allowed && !$ignored && isset($data[$paramName])) {
$params[] = $data[$paramName];
// don't run set for a parameter passed to the constructor
unset($data[$paramName]);
@ -146,7 +111,9 @@ class PropertyNormalizer extends SerializerAwareNormalizer implements Normalizer
foreach ($data as $propertyName => $value) {
$propertyName = lcfirst($this->formatAttribute($propertyName));
if ($reflectionClass->hasProperty($propertyName)) {
$allowed = $allowedAttributes === false || in_array($propertyName, $allowedAttributes);
$ignored = in_array($propertyName, $this->ignoredAttributes);
if ($allowed && !$ignored && $reflectionClass->hasProperty($propertyName)) {
$property = $reflectionClass->getProperty($propertyName);
// Override visibility
@ -177,24 +144,6 @@ class PropertyNormalizer extends SerializerAwareNormalizer implements Normalizer
return $this->supports($type);
}
/**
* Format an attribute name, for example to convert a snake_case name to camelCase.
*
* @param string $attributeName
*
* @return string
*/
protected function formatAttribute($attributeName)
{
if (in_array($attributeName, $this->camelizedAttributes)) {
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
}, $attributeName);
}
return $attributeName;
}
/**
* Checks if the given class has any non-static property.
*

View File

@ -0,0 +1,52 @@
<?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\Tests\Annotation;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GroupsTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \InvalidArgumentException
*/
public function testEmptyGroupsParameter()
{
new Groups(array('value' => array()));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testNotAnArrayGroupsParameter()
{
new Groups(array('value' => 'coopTilleuls'));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testInvalidGroupsParameter()
{
new Groups(array('value' => array('a', 1, new \stdClass())));
}
public function testGroupsParameters()
{
$validData = array('a', 'b');
$groups = new Groups(array('value' => $validData));
$this->assertEquals($validData, $groups->getGroups());
}
}

View File

@ -0,0 +1,74 @@
<?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\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GroupDummy extends GroupDummyParent implements GroupDummyInterface
{
/**
* @Groups({"a"})
*/
private $foo;
/**
* @Groups({"b", "c"})
*/
protected $bar;
private $fooBar;
private $symfony;
public function setBar($bar)
{
$this->bar = $bar;
}
public function getBar()
{
return $this->bar;
}
public function setFoo($foo)
{
$this->foo = $foo;
}
public function getFoo()
{
return $this->foo;
}
public function setFooBar($fooBar)
{
$this->fooBar = $fooBar;
}
/**
* @Groups({"a", "b"})
*/
public function getFooBar()
{
return $this->fooBar;
}
public function setSymfony($symfony)
{
$this->symfony = $symfony;
}
public function getSymfony()
{
return $this->symfony;
}
}

View File

@ -0,0 +1,25 @@
<?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\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface GroupDummyInterface
{
/**
* @Groups({"a"})
*/
public function getSymfony();
}

View File

@ -0,0 +1,49 @@
<?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\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GroupDummyParent
{
/**
* @Groups({"a"})
*/
private $kevin;
private $coopTilleuls;
public function setKevin($kevin)
{
$this->kevin = $kevin;
}
public function getKevin()
{
return $this->kevin;
}
public function setCoopTilleuls($coopTilleuls)
{
$this->coopTilleuls = $coopTilleuls;
}
/**
* @Groups({"a", "b"})
*/
public function getCoopTilleuls()
{
return $this->coopTilleuls;
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" ?>
<serializer xmlns="http://symfony.com/schema/dic/serializer"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/serializer http://symfony.com/schema/dic/constraint-mapping/serializer-1.0.xsd">
<class name="Symfony\Component\Serializer\Tests\Fixtures\GroupDummy">
<attribute name="foo">
<group name="group1" />
<group name="group2" />
</attribute>
<attribute name="bar">
<group name="group2" />
</attribute>
</class>
</serializer>

View File

@ -0,0 +1,6 @@
Symfony\Component\Serializer\Tests\Fixtures\GroupDummy:
attributes:
foo:
groups: ['group1', 'group2']
bar:
groups: ['group2']

View File

@ -0,0 +1,73 @@
<?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\Tests\Mapping\Factory;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
require_once __DIR__.'/../../../Annotation/Groups.php';
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ClassMetadataFactoryTest extends \PHPUnit_Framework_TestCase
{
public function testGetMetadataFor()
{
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$metadata = $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true, true), $metadata);
}
public function testHasMetadataFor()
{
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'));
$this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummyParent'));
$this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummyInterface'));
$this->assertFalse($factory->hasMetadataFor('Dunglas\Entity'));
}
public function testCacheExists()
{
$cache = $this->getMock('Doctrine\Common\Cache\Cache');
$cache
->expects($this->once())
->method('fetch')
->will($this->returnValue('foo'))
;
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()), $cache);
$this->assertEquals('foo', $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'));
}
public function testCacheNotExists()
{
$cache = $this->getMock('Doctrine\Common\Cache\Cache');
$cache
->method('fetch')
->will($this->returnValue(false))
;
$cache
->method('save')
;
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()), $cache);
$metadata = $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true, true), $metadata);
}
}

View File

@ -0,0 +1,57 @@
<?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\Tests\Mapping\Loader;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
require_once __DIR__.'/../../../Annotation/Groups.php';
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AnnotationLoaderTest extends \PHPUnit_Framework_TestCase
{
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$loader = new AnnotationLoader(new AnnotationReader());
$metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->assertTrue($loader->loadClassMetadata($metadata));
}
public function testLoadClassMetadata()
{
$loader = new AnnotationLoader(new AnnotationReader());
$metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$loader->loadClassMetadata($metadata);
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(), $metadata);
}
public function testLoadClassMetadataAndMerge()
{
$loader = new AnnotationLoader(new AnnotationReader());
$metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$parentMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummyParent');
$loader->loadClassMetadata($parentMetadata);
$metadata->mergeAttributesGroups($parentMetadata);
$loader->loadClassMetadata($metadata);
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true), $metadata);
}
}

View File

@ -0,0 +1,43 @@
<?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\Tests\Mapping\Loader;
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
{
private $loader;
private $metadata;
public function setUp()
{
$this->loader = new XmlFileLoader(__DIR__.'/../../Fixtures/serializer.xml');
$this->metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
}
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$this->assertTrue($this->loader->loadClassMetadata($this->metadata));
}
public function testLoadClassMetadata()
{
$this->loader->loadClassMetadata($this->metadata);
$this->assertEquals(TestClassMetadataFactory::createXmlCLassMetadata(), $this->metadata);
}
}

View File

@ -0,0 +1,58 @@
<?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\Tests\Mapping\Loader;
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
{
private $loader;
private $metadata;
public function setUp()
{
$this->loader = new YamlFileLoader(__DIR__.'/../../Fixtures/serializer.yml');
$this->metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
}
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$this->assertTrue($this->loader->loadClassMetadata($this->metadata));
}
public function testLoadClassMetadataReturnsFalseWhenEmpty()
{
$loader = new YamlFileLoader(__DIR__.'/../../Fixtures/empty-mapping.yml');
$this->assertFalse($loader->loadClassMetadata($this->metadata));
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\MappingException
*/
public function testLoadClassMetadataReturnsThrowsInvalidMapping()
{
$loader = new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-mapping.yml');
$loader->loadClassMetadata($this->metadata);
}
public function testLoadClassMetadata()
{
$this->loader->loadClassMetadata($this->metadata);
$this->assertEquals(TestClassMetadataFactory::createXmlCLassMetadata(), $this->metadata);
}
}

View File

@ -0,0 +1,56 @@
<?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\Tests\Mapping;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class TestClassMetadataFactory
{
public static function createClassMetadata($withParent = false, $withInterface = false)
{
$expected = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
if ($withParent) {
$expected->addAttributeGroup('kevin', 'a');
$expected->addAttributeGroup('coopTilleuls', 'a');
$expected->addAttributeGroup('coopTilleuls', 'b');
}
if ($withInterface) {
$expected->addAttributeGroup('symfony', 'a');
}
$expected->addAttributeGroup('foo', 'a');
$expected->addAttributeGroup('bar', 'b');
$expected->addAttributeGroup('bar', 'c');
$expected->addAttributeGroup('fooBar', 'a');
$expected->addAttributeGroup('fooBar', 'b');
// load reflection class so that the comparison passes
$expected->getReflectionClass();
return $expected;
}
public static function createXmlCLassMetadata()
{
$expected = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$expected->addAttributeGroup('foo', 'group1');
$expected->addAttributeGroup('foo', 'group2');
$expected->addAttributeGroup('bar', 'group2');
return $expected;
}
}

View File

@ -11,12 +11,18 @@
namespace Symfony\Component\Serializer\Tests\Normalizer;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
require_once __DIR__.'/../../Annotation/Groups.php';
class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
{
@ -24,6 +30,10 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
* @var GetSetMethodNormalizer
*/
private $normalizer;
/**
* @var SerializerInterface
*/
private $serializer;
protected function setUp()
{
@ -155,6 +165,64 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('bar', $obj->getBar());
}
public function testGroupsNormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals(array(
'bar' => 'bar',
), $this->normalizer->normalize($obj, null, array('groups' => array('c'))));
$this->assertEquals(array(
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
'kevin' => 'kevin',
'coopTilleuls' => 'coopTilleuls',
), $this->normalizer->normalize($obj, null, array('groups' => array('a', 'c'))));
}
public function testGroupsDenormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$toNormalize = array('foo' => 'foo', 'bar' => 'bar');
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
array('groups' => array('a'))
);
$this->assertEquals($obj, $normalized);
$obj->setBar('bar');
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
array('groups' => array('a', 'b'))
);
$this->assertEquals($obj, $normalized);
}
/**
* @dataProvider provideCallbacks
*/

View File

@ -11,7 +11,14 @@
namespace Symfony\Component\Serializer\Tests\Normalizer;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
require_once __DIR__.'/../../Annotation/Groups.php';
class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
{
@ -19,11 +26,16 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
* @var PropertyNormalizer
*/
private $normalizer;
/**
* @var SerializerInterface
*/
private $serializer;
protected function setUp()
{
$this->serializer = $this->getMock('Symfony\Component\Serializer\SerializerInterface');
$this->normalizer = new PropertyNormalizer();
$this->normalizer->setSerializer($this->getMock('Symfony\Component\Serializer\Serializer'));
$this->normalizer->setSerializer($this->serializer);
}
public function testNormalize()
@ -135,6 +147,63 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
);
}
public function testGroupsNormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$obj->setBar('bar');
$obj->setFooBar('fooBar');
$obj->setSymfony('symfony');
$obj->setKevin('kevin');
$obj->setCoopTilleuls('coopTilleuls');
$this->assertEquals(array(
'bar' => 'bar',
), $this->normalizer->normalize($obj, null, array('groups' => array('c'))));
// The PropertyNormalizer is not able to hydrate properties from parent classes
$this->assertEquals(array(
'symfony' => 'symfony',
'foo' => 'foo',
'fooBar' => 'fooBar',
'bar' => 'bar',
), $this->normalizer->normalize($obj, null, array('groups' => array('a', 'c'))));
}
public function testGroupsDenormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new PropertyNormalizer($classMetadataFactory);
$this->normalizer->setSerializer($this->serializer);
$obj = new GroupDummy();
$obj->setFoo('foo');
$toNormalize = array('foo' => 'foo', 'bar' => 'bar');
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
array('groups' => array('a'))
);
$this->assertEquals($obj, $normalized);
$obj->setBar('bar');
$normalized = $this->normalizer->denormalize(
$toNormalize,
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
null,
array('groups' => array('a', 'b'))
);
$this->assertEquals($obj, $normalized);
}
public function provideCallbacks()
{
return array(

View File

@ -18,6 +18,18 @@
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"symfony/yaml": "~2.0",
"symfony/config": "~2.2",
"doctrine/annotations": "~1.0",
"doctrine/cache": "~1.0"
},
"suggest": {
"doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.",
"doctrine/cache": "For using the default cached annotation reader and metadata cache.",
"symfony/yaml": "For using the default YAML mapping loader.",
"symfony/config": "For using the XML mapping loader."
},
"autoload": {
"psr-0": { "Symfony\\Component\\Serializer\\": "" }
},