浏览代码

[Serializer] Added initial version of the Serializer component

Jordi Boggiano 14 年之前
父节点
当前提交
005c1d9df8

+ 40 - 0
src/Symfony/Component/Serializer/Encoder/AbstractEncoder.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Symfony\Component\Serializer\Encoder;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Abstract Encoder implementation
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+abstract class AbstractEncoder
+{
+    protected $serializer;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setSerializer(SerializerInterface $serializer)
+    {
+        $this->serializer = $serializer;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSerializer()
+    {
+        return $this->serializer;
+    }
+}

+ 54 - 0
src/Symfony/Component/Serializer/Encoder/EncoderInterface.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace Symfony\Component\Serializer\Encoder;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Defines the interface of encoders
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+interface EncoderInterface
+{
+    /**
+     * Encodes data into a string
+     *
+     * @param mixed $data data to encode
+     * @param string $format format to encode to
+     * @return string
+     */
+    function encode($data, $format);
+
+    /**
+     * Decodes a string into PHP data
+     *
+     * @param string $data data to decode
+     * @param string $format format to decode from
+     * @return mixed
+     */
+    function decode($data, $format);
+
+    /**
+     * Sets the owning Serializer object
+     *
+     * @param SerializerInterface $serializer
+     */
+    function setSerializer(SerializerInterface $serializer);
+
+    /**
+     * Gets the owning Serializer object
+     *
+     * @return SerializerInterface
+     */
+    function getSerializer();
+}

+ 41 - 0
src/Symfony/Component/Serializer/Encoder/JsonEncoder.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Symfony\Component\Serializer\Encoder;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Encodes JSON data
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class JsonEncoder extends AbstractEncoder implements EncoderInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data, $format)
+    {
+        if (!is_scalar($data)) {
+            $data = $this->serializer->normalize($data, $format);
+        }
+        return json_encode($data);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data, $format)
+    {
+        return json_decode($data);
+    }
+}

+ 253 - 0
src/Symfony/Component/Serializer/Encoder/XmlEncoder.php

@@ -0,0 +1,253 @@
+<?php
+
+namespace Symfony\Component\Serializer\Encoder;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Encodes XML data
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author John Wards <jwards@whiteoctober.co.uk>
+ * @author Fabian Vogler <fabian@equivalence.ch>
+ */
+class XmlEncoder extends AbstractEncoder implements EncoderInterface
+{
+    protected $dom;
+    protected $format;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data, $format)
+    {
+        if ($data instanceof \DOMDocument) {
+            return $data->saveXML();
+        }
+
+        $this->dom = new \DOMDocument();
+        $this->format = $format;
+
+        if (is_scalar($data)) {
+            $this->appendNode($this->dom, $data, 'response');
+        } else {
+            $root = $this->dom->createElement('response');
+            $this->dom->appendChild($root);
+            $this->buildXml($root, $data);
+        }
+        return $this->dom->saveXML();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data, $format)
+    {
+        $xml = simplexml_load_string($data);
+        if (!$xml->count()) {
+            return (string) $xml;
+        }
+        return $this->parseXml($xml);
+    }
+
+    /**
+     * Parse the input SimpleXmlElement into an array
+     *
+     * @param SimpleXmlElement $node xml to parse
+     * @return array
+     */
+    protected function parseXml($node)
+    {
+        $data = array();
+        foreach ($node->children() as $key => $subnode) {
+            if ($subnode->count()) {
+                $value = $this->parseXml($subnode);
+            } else {
+                $value = (string) $subnode;
+            }
+            if ($key === 'item') {
+                if (isset($subnode['key'])) {
+                    $data[(string)$subnode['key']] = $value;
+                } elseif (isset($data['item'])) {
+                    $tmp = $data['item'];
+                    unset($data['item']);
+                    $data[] = $tmp;
+                    $data[] = $value;
+                }
+            } else {
+                $data[$key] = $value;
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Parse the data and convert it to DOMElements
+     *
+     * @param DOMNode $parentNode
+     * @param array|object $data data
+     * @return bool
+     */
+    protected function buildXml($parentNode, $data)
+    {
+        $append = true;
+
+        if (is_array($data) || $data instanceof \Traversable) {
+            foreach ($data as $key => $data) {
+                if (is_array($data) && false === is_numeric($key)) {
+                    $append = $this->appendNode($parentNode, $data, $key);
+                } elseif (is_numeric($key) || !$this->isElementNameValid($key)) {
+                    $append = $this->appendNode($parentNode, $data, "item", $key);
+                } else {
+                    $append = $this->appendNode($parentNode, $data, $key);
+                }
+            }
+            return $append;
+        }
+        if (is_object($data)) {
+            $data = $this->serializer->normalizeObject($data, $this->format);
+            if (is_scalar($data)) {
+                // top level data object is normalized into a scalar
+                if (!$parentNode->parentNode->parentNode) {
+                    $root = $parentNode->parentNode;
+                    $root->removeChild($parentNode);
+                    return $this->appendNode($root, $data, 'response');
+                }
+                return $this->appendNode($parentNode, $data, 'data');
+            }
+            return $this->buildXml($parentNode, $data);
+        }
+        throw new \UnexpectedValueException('An unexpected value could not be serialized: '.var_export($data, true));
+    }
+
+    /**
+     * Selects the type of node to create and appends it to the parent.
+     *
+     * @param  $parentNode
+     * @param  $data
+     * @param  $nodename
+     * @return void
+     */
+    protected function appendNode($parentNode, $data, $nodeName, $key = null)
+    {
+        $node = $this->dom->createElement($nodeName);
+        if (null !== $key) {
+            $node->setAttribute('key', $key);
+        }
+        $appendNode = $this->selectNodeType($node, $data);
+        // we may have decided not to append this node, either in error or if its $nodeName is not valid
+        if ($appendNode) {
+            $parentNode->appendChild($node);
+        }
+        return $appendNode;
+    }
+
+    /**
+     * Tests the value being passed and decide what sort of element to create
+     *
+     * @param DOMNode $node
+     * @param mixed $val
+     * @return Boolean
+     */
+    protected function selectNodeType($node, $val)
+    {
+        if (is_array($val)) {
+            return $this->buildXml($node, $val);
+        } elseif (is_object($val)) {
+            return $this->buildXml($node, $this->serializer->normalizeObject($val, $this->format));
+        } elseif ($val instanceof \SimpleXMLElement) {
+            $child = $this->dom->importNode(dom_import_simplexml($val), true);
+            $node->appendChild($child);
+        } elseif ($val instanceof \Traversable) {
+            $this->buildXml($node, $val);
+        } elseif (is_numeric($val)) {
+            return $this->appendText($node, (string) $val);
+        } elseif (is_string($val)) {
+            return $this->appendCData($node, $val);
+        } elseif (is_bool($val)) {
+            return $this->appendText($node, (int) $val);
+        } elseif ($val instanceof \DOMNode){
+            $child = $this->dom->importNode($val, true);
+            $node->appendChild($child);
+        }
+
+        return true;
+    }
+
+    /**
+     * @param DOMNode $node
+     * @param string $val
+     * @return Boolean
+     */
+    protected function appendXMLString($node, $val)
+    {
+        if (strlen($val) > 0) {
+            $frag = $this->dom->createDocumentFragment();
+            $frag->appendXML($val);
+            $node->appendChild($frag);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param DOMNode $node
+     * @param string $val
+     * @return Boolean
+     */
+    protected function appendText($node, $val)
+    {
+        $nodeText = $this->dom->createTextNode($val);
+        $node->appendChild($nodeText);
+
+        return true;
+    }
+
+    /**
+     * @param DOMNode $node
+     * @param string $val
+     * @return Boolean
+     */
+    protected function appendCData($node, $val)
+    {
+        $nodeText = $this->dom->createCDATASection($val);
+        $node->appendChild($nodeText);
+
+        return true;
+    }
+
+    /**
+     * @param DOMNode $node
+     * @param DOMDocumentFragment $fragment
+     * @return Boolean
+     */
+    protected function appendDocumentFragment($node, $fragment)
+    {
+        if ($fragment instanceof DOMDocumentFragment) {
+            $node->appendChild($fragment);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks the name is avalid xml element name
+     * @param string $name
+     * @return Boolean
+     */
+    protected function isElementNameValid($name)
+    {
+        return $name && strpos($name, ' ') === false && preg_match('|^\w+$|', $name);
+    }
+}

+ 40 - 0
src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Symfony\Component\Serializer\Normalizer;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Abstract Normalizer implementation
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+abstract class AbstractNormalizer
+{
+    protected $serializer;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setSerializer(SerializerInterface $serializer)
+    {
+        $this->serializer = $serializer;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSerializer()
+    {
+        return $this->serializer;
+    }
+}

+ 51 - 0
src/Symfony/Component/Serializer/Normalizer/CustomNormalizer.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace Symfony\Component\Serializer\Normalizer;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class CustomNormalizer extends AbstractNormalizer implements NormalizerInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function normalize($object, $format, $properties = null)
+    {
+        return $object->normalize($this, $format, $properties);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function denormalize($data, $class, $format = null)
+    {
+        $object = new $class;
+        $object->denormalize($this, $data, $format);
+        return $object;
+    }
+
+    /**
+     * Checks if the given class implements the NormalizableInterface.
+     *
+     * @param  ReflectionClass $class  A ReflectionClass instance of the class
+     *                                 to serialize into or from.
+     * @param  string $format The format being (de-)serialized from or into.
+     * @return Boolean
+     */
+    public function supports(\ReflectionClass $class, $format = null)
+    {
+        return $class->implementsInterface('Symfony\Component\Serializer\Normalizer\NormalizableInterface');
+    }
+}

+ 145 - 0
src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php

@@ -0,0 +1,145 @@
+<?php
+
+namespace Symfony\Component\Serializer\Normalizer;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Converts between objects with getter and setter methods and arrays.
+ *
+ * The normalization process looks at all public methods and calls the ones
+ * which have a name starting with get and take no parameters. The result is a
+ * map from property names (method name stripped of the get prefix and converted
+ * to lower case) to property values. Property values are normalized through the
+ * serializer.
+ *
+ * The denormalization first looks at the constructor of the given class to see
+ * if any of the parameters have the same name as one of the properties. The
+ * constructor is then called with all parameters or an exception is thrown if
+ * any required parameters were not present as properties. Then the denormalizer
+ * walks through the given map of property names to property values to see if a
+ * setter method exists for any of the properties. If a setter exists it is
+ * called with the property value. No automatic denormalization of the value
+ * takes place.
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class GetSetMethodNormalizer extends AbstractNormalizer implements NormalizerInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function normalize($object, $format, $properties = null)
+    {
+        $propertyMap = (null === $properties) ? null : array_flip(array_map('strtolower', $properties));
+
+        $reflectionObject = new \ReflectionObject($object);
+        $reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
+
+        $attributes = array();
+        foreach ($reflectionMethods as $method) {
+            if ($this->isGetMethod($method)) {
+                $attributeName = strtolower(substr($method->getName(), 3));
+
+                if (null === $propertyMap || isset($propertyMap[$attributeName])) {
+                    $attributeValue = $method->invoke($object);
+                    if (!is_scalar($attributeValue)) {
+                        $attributeValue = $this->serializer->normalize($attributeValue, $format);
+                    }
+
+                    $attributes[$attributeName] = $attributeValue;
+                }
+            }
+        }
+
+        return $attributes;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function denormalize($data, $class, $format = null)
+    {
+        $reflectionClass = new \ReflectionClass($class);
+        $constructor = $reflectionClass->getConstructor();
+
+        if ($constructor) {
+            $constructorParameters = $constructor->getParameters();
+
+            $attributeNames = array_keys($data);
+            $params = array();
+            foreach ($constructorParameters as $constructorParameter) {
+                $paramName = strtolower($constructorParameter->getName());
+
+                if (isset($data[$paramName])) {
+                    $params[] = $data[$paramName];
+                    // don't run set for a parameter passed to the constructor
+                    unset($data[$paramName]);
+                } else if (!$constructorParameter->isOptional()) {
+                    throw new \RuntimeException(
+                        'Cannot create an instance of ' . $class .
+                        ' from serialized data because its constructor requires ' .
+                        'parameter "' . $constructorParameter->getName() .
+                        '" to be present.');
+                }
+            }
+
+            $object = $reflectionClass->newInstanceArgs($params);
+        } else {
+            $object = new $class;
+        }
+
+        foreach ($data as $attribute => $value) {
+            $setter = 'set' . $attribute;
+            if (method_exists($object, $setter)) {
+                $object->$setter($value);
+            }
+        }
+
+        return $object;
+    }
+
+    /**
+     * Checks if the given class has any get{Property} method.
+     *
+     * @param  ReflectionClass $class  A ReflectionClass instance of the class
+     *                                 to serialize into or from.
+     * @param  string $format The format being (de-)serialized from or into.
+     * @return Boolean Whether the class has any getters.
+     */
+    public function supports(\ReflectionClass $class, $format = null)
+    {
+        $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
+        foreach ($methods as $method) {
+            if ($this->isGetMethod($method)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if a method's name is get.* and can be called without parameters.
+     *
+     * @param ReflectionMethod $method the method to check
+     * @return Boolean whether the method is a getter.
+     */
+    protected function isGetMethod(\ReflectionMethod $method)
+    {
+        return (
+            0 === strpos($method->getName(), 'get') &&
+            3 < strlen($method->getName()) &&
+            0 === $method->getNumberOfRequiredParameters()
+        );
+    }
+}

+ 26 - 0
src/Symfony/Component/Serializer/Normalizer/NormalizableInterface.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Symfony\Component\Serializer\Normalizer;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Defines the most basic interface a class must implement to be normalizable
+ *
+ * If a normalizer is registered for the class and it doesn't implement
+ * the Normalizable interfaces, the normalizer will be used instead
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+interface NormalizableInterface
+{
+    function normalize(NormalizerInterface $normalizer, $format, $properties = null);
+    function denormalize(NormalizerInterface $normalizer, $data, $format = null);
+}

+ 65 - 0
src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Symfony\Component\Serializer\Normalizer;
+
+use Symfony\Component\Serializer\SerializerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Defines the interface of serializers
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+interface NormalizerInterface
+{
+    /**
+     * Normalizes an object into a set of arrays/scalars
+     *
+     * @param object $object object to normalize
+     * @param string $format format the normalization result will be encoded as
+     * @param array $properties a list of properties to extract, if null all properties are returned
+     * @return array|scalar
+     */
+    function normalize($object, $format, $properties = null);
+
+    /**
+     * Denormalizes data back into an object of the given class
+     *
+     * @param mixed $data data to restore
+     * @param string $class the expected class to instantiate
+     * @param string $format format the given data was extracted from
+     * @return object
+     */
+    function denormalize($data, $class, $format = null);
+
+    /**
+     * Checks whether the given class is supported by this normalizer
+     *
+     * @param ReflectionClass $class
+     * @param string $format format the given data was extracted from
+     * @return Boolean
+     */
+    function supports(\ReflectionClass $class, $format = null);
+
+    /**
+     * Sets the owning Serializer object
+     *
+     * @param SerializerInterface $serializer
+     */
+    function setSerializer(SerializerInterface $serializer);
+
+    /**
+     * Gets the owning Serializer object
+     *
+     * @return SerializerInterface
+     */
+    function getSerializer();
+}

+ 156 - 0
src/Symfony/Component/Serializer/Serializer.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace Symfony\Component\Serializer;
+
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\Encoder\EncoderInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Serializer serializes and deserializes data
+ *
+ * objects are turned into arrays by normalizers
+ * arrays are turned into various output formats by encoders
+ *
+ * $serializer->serialize($obj, 'xml')
+ * $serializer->decode($data, 'xml')
+ * $serializer->denormalizeObject($data, 'Class', 'xml')
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class Serializer implements SerializerInterface
+{
+    protected $normalizers = array();
+    protected $encoders = array();
+    protected $normalizerCache = array();
+
+    /**
+     * {@inheritdoc}
+     */
+    public function serialize($data, $format)
+    {
+        return $this->encode($data, $format);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function normalizeObject($object, $format, $properties = null)
+    {
+        if (!$this->normalizers) {
+            throw new \LogicException('You must register at least one normalizer to be able to normalize objects.');
+        }
+        $class = get_class($object);
+        if (isset($this->normalizerCache[$class][$format])) {
+            return $this->normalizerCache[$class][$format]->normalize($object, $format, $properties);
+        }
+        $reflClass = new \ReflectionClass($class);
+        foreach ($this->normalizers as $normalizer) {
+            if ($normalizer->supports($reflClass, $format)) {
+                $this->normalizerCache[$class][$format] = $normalizer;
+                return $normalizer->normalize($object, $format, $properties);
+            }
+        }
+        throw new \UnexpectedValueException('Could not normalize object of type '.$class.', no supporting normalizer found.');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function denormalizeObject($data, $class, $format = null)
+    {
+        if (!$this->normalizers) {
+            throw new \LogicException('You must register at least one normalizer to be able to denormalize objects.');
+        }
+        if (isset($this->normalizerCache[$class][$format])) {
+            return $this->normalizerCache[$class][$format]->denormalize($data, $format);
+        }
+        $reflClass = new \ReflectionClass($class);
+        foreach ($this->normalizers as $normalizer) {
+            if ($normalizer->supports($reflClass, $format)) {
+                $this->normalizerCache[$class][$format] = $normalizer;
+                return $normalizer->denormalize($data, $class, $format);
+            }
+        }
+        throw new \UnexpectedValueException('Could not denormalize object of type '.$class.', no supporting normalizer found.');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function normalize($data, $format)
+    {
+        if (is_array($data)) {
+            foreach ($data as $key => $val) {
+                $data[$key] = is_scalar($val) ? $val : $this->normalize($val, $format);
+            }
+            return $data;
+        }
+        if (is_object($data)) {
+            return $this->normalizeObject($data, $format);
+        }
+        throw new \UnexpectedValueException('An unexpected value could not be normalized: '.var_export($data, true));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function encode($data, $format)
+    {
+        if (!isset($this->encoders[$format])) {
+            throw new \UnexpectedValueException('No encoder registered for the '.$format.' format');
+        }
+        return $this->encoders[$format]->encode($data, $format);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function decode($data, $format)
+    {
+        if (!isset($this->encoders[$format])) {
+            throw new \UnexpectedValueException('No encoder registered to decode the '.$format.' format');
+        }
+        return $this->encoders[$format]->decode($data, $format);
+    }
+
+    public function addNormalizer(NormalizerInterface $normalizer)
+    {
+        $this->normalizers[] = $normalizer;
+        $normalizer->setSerializer($this);
+    }
+
+    public function getNormalizers()
+    {
+        return $this->normalizers;
+    }
+
+    public function removeNormalizer(NormalizerInterface $normalizer)
+    {
+        unset($this->normalizers[array_search($normalizer, $this->normalizers, true)]);
+    }
+
+    public function addEncoder($format, EncoderInterface $encoder)
+    {
+        $this->encoders[$format] = $encoder;
+        $encoder->setSerializer($this);
+    }
+
+    public function getEncoders()
+    {
+        return $this->encoders;
+    }
+
+    public function removeEncoder($format)
+    {
+        unset($this->encoders[$format]);
+    }
+}

+ 76 - 0
src/Symfony/Component/Serializer/SerializerInterface.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Symfony\Component\Serializer;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Defines the interface of the Serializer
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+interface SerializerInterface
+{
+    /**
+     * Serializes data in the appropriate format
+     *
+     * @param mixed $data any data
+     * @param string $format format name
+     * @return string
+     */
+    function serialize($data, $format);
+
+    /**
+     * Normalizes any data into a set of arrays/scalars
+     *
+     * @param mixed $data data to normalize
+     * @param string $format format name, present to give the option to normalizers to act differently based on formats
+     * @return array|scalar
+     */
+    function normalize($data, $format);
+
+    /**
+     * Normalizes an object into a set of arrays/scalars
+     *
+     * @param object $object object to normalize
+     * @param string $format format name, present to give the option to normalizers to act differently based on formats
+     * @param array $properties a list of properties to extract, if null all properties are returned
+     * @return array|scalar
+     */
+    function normalizeObject($object, $format, $properties = null);
+
+    /**
+     * Denormalizes data back into an object of the given class
+     *
+     * @param mixed $data data to restore
+     * @param string $class the expected class to instantiate
+     * @param string $format format name, present to give the option to normalizers to act differently based on formats
+     * @return object
+     */
+    function denormalizeObject($data, $class, $format = null);
+
+    /**
+     * Encodes data into the given format
+     *
+     * @param mixed $data data to encode
+     * @param string $format format name
+     * @return array|scalar
+     */
+    function encode($data, $format);
+
+    /**
+     * Decodes a string from the given format back into PHP data
+     *
+     * @param string $data data to decode
+     * @param string $format format name
+     * @return mixed
+     */
+    function decode($data, $format);
+}

+ 88 - 0
tests/Symfony/Tests/Component/Serializer/Encoder/XmlEncoderTest.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Symfony\Tests\Component\Serializer\Encoder;
+
+require_once __DIR__.'/../Fixtures/Dummy.php';
+require_once __DIR__.'/../Fixtures/ScalarDummy.php';
+
+use Symfony\Tests\Component\Serializer\Fixtures\Dummy;
+use Symfony\Tests\Component\Serializer\Fixtures\ScalarDummy;
+use Symfony\Component\Serializer\Encoder\XmlEncoder;
+use Symfony\Component\Serializer\Serializer;
+use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+class XmlEncoderTest extends \PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        $serializer = new Serializer;
+        $this->encoder = new XmlEncoder;
+        $serializer->addEncoder('xml', $this->encoder);
+        $serializer->addNormalizer(new CustomNormalizer);
+    }
+
+    public function testEncodeScalar()
+    {
+        $obj = new ScalarDummy;
+        $obj->xmlFoo = "foo";
+
+        $expected = '<?xml version="1.0"?>'."\n".
+            '<response><![CDATA[foo]]></response>'."\n";
+
+        $this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
+    }
+
+    public function testDecodeScalar()
+    {
+        $source = '<?xml version="1.0"?>'."\n".
+            '<response>foo</response>'."\n";
+
+        $this->assertEquals('foo', $this->encoder->decode($source, 'xml'));
+    }
+
+    public function testEncode()
+    {
+        $source = $this->getXmlSource();
+        $obj = $this->getObject();
+
+        $this->assertEquals($source, $this->encoder->encode($obj, 'xml'));
+    }
+
+    public function testDecode()
+    {
+        $source = $this->getXmlSource();
+        $obj = $this->getObject();
+
+        $this->assertEquals(get_object_vars($obj), $this->encoder->decode($source, 'xml'));
+    }
+
+    protected function getXmlSource()
+    {
+        return '<?xml version="1.0"?>'."\n".
+            '<response>'.
+            '<foo><![CDATA[foo]]></foo>'.
+            '<bar><item key="0"><![CDATA[a]]></item><item key="1"><![CDATA[b]]></item></bar>'.
+            '<baz><key><![CDATA[val]]></key><key2><![CDATA[val]]></key2><item key="A B"><![CDATA[bar]]></item></baz>'.
+            '<qux>1</qux>'.
+            '</response>'."\n";
+    }
+
+    protected function getObject()
+    {
+        $obj = new Dummy;
+        $obj->foo = 'foo';
+        $obj->bar = array('a', 'b');
+        $obj->baz = array('key' => 'val', 'key2' => 'val', 'A B' => 'bar');
+        $obj->qux = "1";
+        return $obj;
+    }
+}

+ 32 - 0
tests/Symfony/Tests/Component/Serializer/Fixtures/Dummy.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace Symfony\Tests\Component\Serializer\Fixtures;
+
+use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+class Dummy implements NormalizableInterface
+{
+    public $foo;
+    public $bar;
+    public $baz;
+    public $qux;
+
+    public function normalize(NormalizerInterface $normalizer, $format, $properties = null)
+    {
+        return array(
+            'foo' => $this->foo,
+            'bar' => $this->bar,
+            'baz' => $this->baz,
+            'qux' => $this->qux,
+        );
+    }
+
+    public function denormalize(NormalizerInterface $normalizer, $data, $format = null)
+    {
+        $this->foo = $data['foo'];
+        $this->bar = $data['bar'];
+        $this->baz = $data['baz'];
+        $this->qux = $data['qux'];
+    }
+}

+ 26 - 0
tests/Symfony/Tests/Component/Serializer/Fixtures/ScalarDummy.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Symfony\Tests\Component\Serializer\Fixtures;
+
+use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+class ScalarDummy implements NormalizableInterface
+{
+    public $foo;
+    public $xmlFoo;
+
+    public function normalize(NormalizerInterface $normalizer, $format, $properties = null)
+    {
+        return $format === 'xml' ? $this->xmlFoo : $this->foo;
+    }
+
+    public function denormalize(NormalizerInterface $normalizer, $data, $format = null)
+    {
+        if ($format === 'xml') {
+            $this->xmlFoo = $data;
+        } else {
+            $this->foo = $data;
+        }
+    }
+}

+ 52 - 0
tests/Symfony/Tests/Component/Serializer/Normalizer/CustomNormalizerTest.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Symfony\Tests\Component\Serializer\Normalizer;
+
+require_once __DIR__.'/../Fixtures/ScalarDummy.php';
+
+use Symfony\Tests\Component\Serializer\Fixtures\ScalarDummy;
+use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+class CustomNormalizerTest extends \PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        $this->normalizer = new CustomNormalizer;
+        $this->normalizer->setSerializer($this->getMock('Symfony\Component\Serializer\Serializer'));
+    }
+
+    public function testSerialize()
+    {
+        $obj = new ScalarDummy;
+        $obj->foo = 'foo';
+        $obj->xmlFoo = 'xml';
+        $this->assertEquals('foo', $this->normalizer->normalize($obj, 'json'));
+        $this->assertEquals('xml', $this->normalizer->normalize($obj, 'xml'));
+    }
+
+    public function testDeserialize()
+    {
+        $obj = $this->normalizer->denormalize('foo', get_class(new ScalarDummy), 'xml');
+        $this->assertEquals('foo', $obj->xmlFoo);
+        $this->assertNull($obj->foo);
+
+        $obj = $this->normalizer->denormalize('foo', get_class(new ScalarDummy), 'json');
+        $this->assertEquals('foo', $obj->foo);
+        $this->assertNull($obj->xmlFoo);
+    }
+
+    public function testSupports()
+    {
+        $this->assertTrue($this->normalizer->supports(new \ReflectionClass(get_class(new ScalarDummy))));
+        $this->assertFalse($this->normalizer->supports(new \ReflectionClass('stdClass')));
+    }
+}

+ 125 - 0
tests/Symfony/Tests/Component/Serializer/Normalizer/GetSetMethodNormalizerTest.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace Symfony\Tests\Component\Serializer\Normalizer;
+
+use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        $this->normalizer = new GetSetMethodNormalizer;
+        $this->normalizer->setSerializer($this->getMock('Symfony\Component\Serializer\Serializer'));
+    }
+
+    public function testNormalize()
+    {
+        $obj = new GetSetDummy;
+        $obj->setFoo('foo');
+        $obj->setBar('bar');
+        $this->assertEquals(
+            array('foo' => 'foo', 'bar' => 'bar', 'foobar' => 'foobar'),
+            $this->normalizer->normalize($obj, 'any'));
+    }
+
+    public function testNormalizeRestricted()
+    {
+        $obj = new GetSetDummy;
+        $obj->setFoo('foo');
+        $obj->setBar('bar');
+        $this->assertEquals(
+            array('foo' => 'foo'),
+            $this->normalizer->normalize($obj, 'any', array('foo')));
+    }
+
+    public function testDenormalize()
+    {
+        $obj = $this->normalizer->denormalize(
+            array('foo' => 'foo', 'bar' => 'bar', 'foobar' => 'foobar'),
+            __NAMESPACE__.'\GetSetDummy', 'any');
+        $this->assertEquals('foo', $obj->getFoo());
+        $this->assertEquals('bar', $obj->getBar());
+    }
+
+    public function testConstructorDenormalize()
+    {
+        $obj = $this->normalizer->denormalize(
+            array('foo' => 'foo', 'bar' => 'bar', 'foobar' => 'foobar'),
+            __NAMESPACE__.'\GetConstructorDummy', 'any');
+        $this->assertEquals('foo', $obj->getFoo());
+        $this->assertEquals('bar', $obj->getBar());
+    }
+}
+
+class GetSetDummy
+{
+    protected $foo;
+    private $bar;
+
+    public function getFoo()
+    {
+        return $this->foo;
+    }
+
+    public function setFoo($foo)
+    {
+        $this->foo = $foo;
+    }
+
+    public function getBar()
+    {
+        return $this->bar;
+    }
+
+    public function setBar($bar)
+    {
+        $this->bar = $bar;
+    }
+
+    public function getFooBar()
+    {
+        return $this->foo . $this->bar;
+    }
+
+    public function otherMethod()
+    {
+        throw new \RuntimeException("Dummy::otherMethod() should not be called");
+    }
+}
+
+class GetConstructorDummy
+{
+    protected $foo;
+    private $bar;
+
+    public function __construct($foo, $bar)
+    {
+        $this->foo = $foo;
+        $this->bar = $bar;
+    }
+
+    public function getFoo()
+    {
+        return $this->foo;
+    }
+
+    public function getBar()
+    {
+        return $this->bar;
+    }
+
+    public function otherMethod()
+    {
+        throw new \RuntimeException("Dummy::otherMethod() should not be called");
+    }
+}

+ 82 - 0
tests/Symfony/Tests/Component/Serializer/SerializerTest.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace Symfony\Tests\Component\Serializer;
+
+use Symfony\Component\Serializer\Serializer;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+class SerializerTest extends \PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        $this->serializer = new Serializer();
+    }
+
+    /**
+     * @expectedException \UnexpectedValueException
+     */
+    public function testNormalizeObjectNoMatch()
+    {
+        $this->serializer->addNormalizer($this->getMock('Symfony\Component\Serializer\Normalizer\CustomNormalizer'));
+        $this->serializer->normalizeObject(new \stdClass, 'xml');
+    }
+
+    /**
+     * @expectedException \UnexpectedValueException
+     */
+    public function testDenormalizeObjectNoMatch()
+    {
+        $this->serializer->addNormalizer($this->getMock('Symfony\Component\Serializer\Normalizer\CustomNormalizer'));
+        $this->serializer->denormalizeObject('foo', 'stdClass');
+    }
+
+    public function testSerializeScalar()
+    {
+        $this->serializer->addEncoder('json', new JsonEncoder());
+        $result = $this->serializer->serialize('foo', 'json');
+        $this->assertEquals('"foo"', $result);
+    }
+
+    public function testSerializeArrayOfScalars()
+    {
+        $this->serializer->addEncoder('json', new JsonEncoder());
+        $data = array('foo', array(5, 3));
+        $result = $this->serializer->serialize($data, 'json');
+        $this->assertEquals(json_encode($data), $result);
+    }
+
+    public function testEncode()
+    {
+        $this->serializer->addEncoder('json', new JsonEncoder());
+        $data = array('foo', array(5, 3));
+        $result = $this->serializer->encode($data, 'json');
+        $this->assertEquals(json_encode($data), $result);
+    }
+
+    public function testDecode()
+    {
+        $this->serializer->addEncoder('json', new JsonEncoder());
+        $data = array('foo', array(5, 3));
+        $result = $this->serializer->decode(json_encode($data), 'json');
+        $this->assertEquals($data, $result);
+    }
+
+    /**
+     * @expectedException \UnexpectedValueException
+     */
+    public function testNormalizeNoMatchObject()
+    {
+        $this->serializer->addNormalizer($this->getMock('Symfony\Component\Serializer\Normalizer\CustomNormalizer'));
+        $this->serializer->normalizeObject(new \stdClass, 'xml');
+    }
+}