diff --git a/src/Symfony/Component/Serializer/Annotation/Groups.php b/src/Symfony/Component/Serializer/Annotation/Groups.php new file mode 100644 index 0000000000..e88ffa71d7 --- /dev/null +++ b/src/Symfony/Component/Serializer/Annotation/Groups.php @@ -0,0 +1,63 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Exception/MappingException.php b/src/Symfony/Component/Serializer/Exception/MappingException.php new file mode 100644 index 0000000000..8b63dd0a6d --- /dev/null +++ b/src/Symfony/Component/Serializer/Exception/MappingException.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +class MappingException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php new file mode 100644 index 0000000000..ff24c3cabc --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php @@ -0,0 +1,134 @@ + + * + * 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 + */ +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', + ); + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php new file mode 100644 index 0000000000..13164dc351 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php @@ -0,0 +1,137 @@ + + * + * 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 + */ +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, '\\'); + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php new file mode 100644 index 0000000000..3b25948924 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -0,0 +1,81 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/FileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/FileLoader.php new file mode 100644 index 0000000000..a583757ba6 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/FileLoader.php @@ -0,0 +1,40 @@ + + * + * 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; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/LoaderChain.php b/src/Symfony/Component/Serializer/Mapping/Loader/LoaderChain.php new file mode 100644 index 0000000000..572d78aeca --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/LoaderChain.php @@ -0,0 +1,62 @@ + + * + * 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 all of these loaders, regardless of whether any of them was + * successful or not. + * + * @author Bernhard Schussek + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/LoaderInterface.php b/src/Symfony/Component/Serializer/Mapping/Loader/LoaderInterface.php new file mode 100644 index 0000000000..d81f11e017 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/LoaderInterface.php @@ -0,0 +1,31 @@ + + * + * 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 + */ +interface LoaderInterface +{ + /** + * Load class metadata. + * + * @param ClassMetadata $metadata A metadata + * + * @return bool + */ + public function loadClassMetadata(ClassMetadata $metadata); +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php new file mode 100644 index 0000000000..6e47e99a63 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -0,0 +1,80 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php new file mode 100644 index 0000000000..f72aa5a1d1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -0,0 +1,80 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd new file mode 100644 index 0000000000..cd5a9a9f0d --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php new file mode 100644 index 0000000000..5d6a18fb9c --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -0,0 +1,130 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index b15eefc6e4..24cd7f0e9f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -36,13 +36,10 @@ use Symfony\Component\Serializer\Exception\RuntimeException; * @author Nils Adermann * @author Kévin Dunglas */ -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} */ diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index c8e1572224..609680763c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -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 + * @author Kévin Dunglas */ -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. * diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/GroupsTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/GroupsTest.php new file mode 100644 index 0000000000..b427325f08 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Annotation/GroupsTest.php @@ -0,0 +1,52 @@ + + * + * 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 + */ +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()); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummy.php new file mode 100644 index 0000000000..90d45f5b91 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummy.php @@ -0,0 +1,74 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummyInterface.php b/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummyInterface.php new file mode 100644 index 0000000000..6ff54b0450 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummyInterface.php @@ -0,0 +1,25 @@ + + * + * 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 + */ +interface GroupDummyInterface +{ + /** + * @Groups({"a"}) + */ + public function getSymfony(); +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummyParent.php b/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummyParent.php new file mode 100644 index 0000000000..dd24233993 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/GroupDummyParent.php @@ -0,0 +1,49 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/empty-mapping.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/empty-mapping.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/invalid-mapping.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/invalid-mapping.yml new file mode 100644 index 0000000000..1910281566 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/invalid-mapping.yml @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serializer.xml b/src/Symfony/Component/Serializer/Tests/Fixtures/serializer.xml new file mode 100644 index 0000000000..c8c1e887be --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serializer.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serializer.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serializer.yml new file mode 100644 index 0000000000..e855ea472b --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serializer.yml @@ -0,0 +1,6 @@ +Symfony\Component\Serializer\Tests\Fixtures\GroupDummy: + attributes: + foo: + groups: ['group1', 'group2'] + bar: + groups: ['group2'] diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php new file mode 100644 index 0000000000..0573f2889a --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php @@ -0,0 +1,73 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php new file mode 100644 index 0000000000..8a34108cbd --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -0,0 +1,57 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php new file mode 100644 index 0000000000..f45a2f0eeb --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -0,0 +1,43 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php new file mode 100644 index 0000000000..6eec425563 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -0,0 +1,58 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/TestClassMetadataFactory.php b/src/Symfony/Component/Serializer/Tests/Mapping/TestClassMetadataFactory.php new file mode 100644 index 0000000000..c253e72eaa --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/TestClassMetadataFactory.php @@ -0,0 +1,56 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index fa59ca904e..f9c5049028 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -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 */ diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index a954775a4a..1465946b43 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -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( diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 026be84e1c..8338fe6af1 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -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\\": "" } },