Browse Source

adds the ability to change the access type on a class/property basis (wip)

Johannes Schmitt 13 years ago
parent
commit
b1497bff83

+ 37 - 0
Annotation/AccessType.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace JMS\SerializerBundle\Annotation;
+
+/**
+ * @Annotation
+ * @Target({"CLASS", "PROPERTY"})
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class AccessType
+{
+    /**
+     * @Required
+     * @var string
+     */
+    public $type;
+
+    public function __construct()
+    {
+        if (0 === func_num_args()) {
+            return;
+        }
+        $values = func_get_arg(0);
+
+        if (isset($values['value'])) {
+            $values['type'] = $values['value'];
+        }
+        if (!isset($values['type'])) {
+            throw new \InvalidArgumentException(sprintf('@AccessType requires the AccessType.'));
+        }
+        if (!is_string($values['type'])) {
+            throw new \InvalidArgumentException(sprintf('@AccessType expects a string type, but got %s.', json_encode($values['type'])));
+        }
+        $this->type = $values['type'];
+    }
+}

+ 47 - 0
Annotation/Accessor.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace JMS\SerializerBundle\Annotation;
+
+/**
+ * @Annotation
+ * @Target("PROPERTY")
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class Accessor
+{
+    /**
+     * @var string
+     */
+    public $getter;
+
+    /**
+     * @var string
+     */
+    public $setter;
+
+    public function __construct()
+    {
+        if (0 === func_num_args()) {
+            return;
+        }
+        $values = func_get_arg(0);
+
+        if (isset($values['value'])) {
+            $values['getter'] = $values['value'];
+        }
+        if (isset($values['getter'])) {
+            if (!is_string($values['getter'])) {
+                throw new \InvalidArgumentException(sprintf('"getter" attribute of annotation @Accessor must be a string, but got %s.', json_encode($values['getter'])));
+            }
+            $this->getter = $values['getter'];
+        }
+
+        if (isset($values['setter'])) {
+            if (!is_string($values['setter'])) {
+                throw new \InvalidArgumentException(sprintf('"setter" attribute of annotation @Accessor must be a string, but got %s.', json_encode($values['setter'])));
+            }
+            $this->setter = $values['setter'];
+        }
+    }
+}

+ 15 - 0
Metadata/Driver/AnnotationDriver.php

@@ -18,6 +18,10 @@
 
 namespace JMS\SerializerBundle\Metadata\Driver;
 
+use JMS\SerializerBundle\Annotation\Accessor;
+
+use JMS\SerializerBundle\Annotation\AccessType;
+
 use JMS\SerializerBundle\Annotation\XmlMap;
 use JMS\SerializerBundle\Annotation\XmlRoot;
 use JMS\SerializerBundle\Annotation\XmlAttribute;
@@ -55,6 +59,7 @@ class AnnotationDriver implements DriverInterface
 
         $exclusionPolicy = 'NONE';
         $excludeAll = false;
+        $classAccessType = PropertyMetadata::ACCESS_TYPE_PROPERTY;
         foreach ($this->reader->getClassAnnotations($class) as $annot) {
             if ($annot instanceof ExclusionPolicy) {
                 $exclusionPolicy = $annot->policy;
@@ -62,6 +67,8 @@ class AnnotationDriver implements DriverInterface
                 $classMetadata->xmlRootName = $annot->name;
             } else if ($annot instanceof Exclude) {
                 $excludeAll = true;
+            } else if ($annot instanceof AccessType) {
+                $classAccessType = $annot->type;
             }
         }
 
@@ -73,6 +80,8 @@ class AnnotationDriver implements DriverInterface
 
                 $propertyMetadata = new PropertyMetadata($name, $property->getName());
                 $isExclude = $isExpose = false;
+                $AccessType = $classAccessType;
+                $accessor = array(null, null);
                 foreach ($this->reader->getPropertyAnnotations($property) as $annot) {
                     if ($annot instanceof Since) {
                         $propertyMetadata->sinceVersion = $annot->version;
@@ -99,9 +108,15 @@ class AnnotationDriver implements DriverInterface
                         $propertyMetadata->xmlAttribute = true;
                     } else if ($annot instanceof XmlValue) {
                         $propertyMetadata->xmlValue = true;
+                    } else if ($annot instanceof AccessType) {
+                        $AccessType = $annot->type;
+                    } else if ($annot instanceof Accessor) {
+                        $accessor = array($annot->getter, $annot->setter);
                     }
                 }
 
+                $propertyMetadata->setAccessor($AccessType, $accessor[0], $accessor[1]);
+
                 if ((ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude)
                     || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)) {
                     $classMetadata->addPropertyMetadata($propertyMetadata);

+ 8 - 1
Metadata/Driver/XmlDriver.php

@@ -48,6 +48,7 @@ class XmlDriver extends AbstractFileDriver
         $metadata->fileResources[] = $class->getFileName();
         $exclusionPolicy = $elem->attributes()->{'exclusion-policy'} ?: 'NONE';
         $excludeAll = null !== ($exclude = $elem->attributes()->exclude) ? 'true' === (string) $exclude : false;
+        $classAccessType = (string) ($elem->attributes()->{'access-type'} ?: PropertyMetadata::ACCESS_TYPE_PROPERTY);
 
         if (null !== $xmlRootName = $elem->attributes()->{'xml-root-name'}) {
             $metadata->xmlRootName = (string) $xmlRootName;
@@ -126,11 +127,17 @@ class XmlDriver extends AbstractFileDriver
                     if (isset($pElem->attributes()->{'xml-attribute'})) {
                         $pMetadata->xmlAttribute = 'true' === (string) $pElem->attributes()->{'xml-attribute'};
                     }
-                    
+
                     if (isset($pElem->attributes()->{'xml-value'})) {
                         $pMetadata->xmlValue = 'true' === (string) $pElem->attributes()->{'xml-value'};
                     }
 
+                    $accessor = $pElem->attributes()->{'accessor'};
+                    $pMetadata->setAccessor(
+                        (string) ($pElem->attributes()->{'access-type'} ?: $classAccessType),
+                        $accessor ? (string) $accessor : null
+                    );
+
                     if ((ExclusionPolicy::NONE === (string)$exclusionPolicy && !$isExclude)
                         || (ExclusionPolicy::ALL === (string)$exclusionPolicy && $isExpose)) {
 

+ 7 - 1
Metadata/Driver/YamlDriver.php

@@ -42,6 +42,7 @@ class YamlDriver extends AbstractFileDriver
         $metadata->fileResources[] = $class->getFileName();
         $exclusionPolicy = isset($config['exclusion_policy']) ? $config['exclusion_policy'] : 'NONE';
         $excludeAll = isset($config['exclude']) ? (Boolean) $config['exclude'] : false;
+        $classAccessType = isset($config['access_type']) ? $config['access_type'] : PropertyMetadata::ACCESS_TYPE_PROPERTY;
 
         if (isset($config['xml_root_name'])) {
             $metadata->xmlRootName = (string) $config['xml_root_name'];
@@ -115,11 +116,16 @@ class YamlDriver extends AbstractFileDriver
                     if (isset($pConfig['xml_attribute'])) {
                         $pMetadata->xmlAttribute = (Boolean) $pConfig['xml_attribute'];
                     }
-                    
+
                     if (isset($pConfig['xml_value'])) {
                         $pMetadata->xmlValue = (Boolean) $pConfig['xml_value'];
                     }
 
+                    $pMetadata->setAccessor(
+                        isset($pConfig['access_type']) ? $pConfig['access_type'] : $classAccessType,
+                        isset($pConfig['accessor']) ? $pConfig['accessor'] : null
+                    );
+
                     if ((ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude)
                     || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)) {
                         $metadata->addPropertyMetadata($pMetadata);

+ 37 - 0
Metadata/PropertyMetadata.php

@@ -22,6 +22,9 @@ use Metadata\PropertyMetadata as BasePropertyMetadata;
 
 class PropertyMetadata extends BasePropertyMetadata
 {
+    const ACCESS_TYPE_PROPERTY        = 'property';
+    const ACCESS_TYPE_PUBLIC_METHOD   = 'public_method';
+
     public $sinceVersion;
     public $untilVersion;
     public $serializedName;
@@ -32,6 +35,36 @@ class PropertyMetadata extends BasePropertyMetadata
     public $xmlKeyAttribute;
     public $xmlAttribute = false;
     public $xmlValue = false;
+    public $getter;
+    public $setter;
+
+    public function setAccessor($type, $getter = null, $setter = null)
+    {
+        if (self::ACCESS_TYPE_PUBLIC_METHOD === $type) {
+            $class = $this->reflection->getDeclaringClass();
+
+            if (empty($getter)) {
+                if ($class->hasMethod('get'.$this->name) && $class->getMethod('get'.$this->name)->isPublic()) {
+                    $getter = 'get'.$this->name;
+                } else if ($class->hasMethod('is'.$this->name) && $class->getMethod('is'.$this->name)->isPublic()) {
+                    $getter = 'is'.$this->name;
+                } else {
+                    throw new \RuntimeException(sprintf('There is neither a public %s method, nor a public %s method in class %s. Please specify which public method should be used for retrieving the value of the property %s.', 'get'.ucfirst($this->name), 'is'.ucfirst($this->name), $this->class, $this->name));
+                }
+            }
+
+            if (empty($setter)) {
+                if ($class->hasMethod('set'.$this->name) && $class->getMethod('set'.$this->name)->isPublic()) {
+                    $setter = 'set'.$this->name;
+                } else {
+                    throw new \RuntimeException(sprintf('There is no public %s method in class %s. Please specify which public method should be used for setting the value of the property %s.', 'set'.ucfirst($this->name), $this->class, $this->name));
+                }
+            }
+        }
+
+        $this->getter = $getter;
+        $this->setter = $setter;
+    }
 
     public function serialize()
     {
@@ -46,6 +79,8 @@ class PropertyMetadata extends BasePropertyMetadata
             $this->xmlKeyAttribute,
             $this->xmlAttribute,
             $this->xmlValue,
+            $this->getter,
+            $this->setter,
             parent::serialize(),
         ));
     }
@@ -63,6 +98,8 @@ class PropertyMetadata extends BasePropertyMetadata
             $this->xmlKeyAttribute,
             $this->xmlAttribute,
             $this->xmlValue,
+            $this->getter,
+            $this->setter,
             $parentStr
         ) = unserialize($str);
 

+ 48 - 0
Resources/doc/index.rst

@@ -232,6 +232,52 @@ property was available. If a later version is serialized, then this property is
 excluded automatically. The version must be in a format that is understood by
 PHP's ``version_compare`` function.
 
+@AccessType
+~~~~~~~~~~~
+This annotation can be defined on a property, or a class to specify in which way
+the properties should be accessed. By default, the serializer will retrieve, or
+set the value via reflection, but you may change this to use a public method instead::
+
+    /** @AccessType("public_method") */
+    class User
+    {
+        private $name;
+        
+        public function getName()
+        {
+            return $this->name;
+        }
+        
+        public function setName($name)
+        {
+            $this->name = trim($name);
+        }
+    }
+
+@Accessor
+~~~~~~~~~
+This annotation can be defined on a property to specify which public method should
+be called to retrieve, or set the value of the given property::
+
+    class User
+    {
+        private $id;
+        
+        /** @Accessor(getter="getTrimmedName") */
+        private $name;
+        
+        // ...
+        public function getTrimmedName()
+        {
+            return trim($this->name);
+        }
+        
+        public function setName($name)
+        {
+            $this->name = $name;
+        }
+    }
+
 @PreSerialize
 ~~~~~~~~~~~~~
 This annotation can be defined on a method which is supposed to be called before
@@ -490,10 +536,12 @@ YAML Reference
         exclusion_policy: ALL
         xml_root_name: foobar
         exclude: true
+        access_type: public_method # defaults to property
         properties:
             some-property:
                 exclude: true
                 expose: true
+                access_type: public_method # defaults to property
                 type: string
                 serialized_name: foo
                 since_version: 1.0

+ 7 - 1
Serializer/GenericDeserializationVisitor.php

@@ -182,7 +182,13 @@ abstract class GenericDeserializationVisitor extends AbstractDeserializationVisi
             return;
         }
 
-        $metadata->reflection->setValue($this->currentObject, $v);
+        if (null === $metadata->setter) {
+            $metadata->reflection->setValue($this->currentObject, $v);
+
+            return;
+        }
+
+        $this->currentObject->{$metadata->setter}($v);
     }
 
     public function endVisitingObject(ClassMetadata $metadata, $data, $type)

+ 4 - 1
Serializer/GenericSerializationVisitor.php

@@ -129,7 +129,10 @@ abstract class GenericSerializationVisitor extends AbstractSerializationVisitor
 
     public function visitProperty(PropertyMetadata $metadata, $data)
     {
-        $v = $this->navigator->accept($metadata->reflection->getValue($data), null, $this);
+        $v = (null === $metadata->getter ? $metadata->reflection->getValue($data)
+                : $data->{$metadata->getter}());
+
+        $v = $this->navigator->accept($v, null, $this);
         if (null === $v) {
             return;
         }

+ 9 - 2
Serializer/XmlDeserializationVisitor.php

@@ -205,7 +205,7 @@ class XmlDeserializationVisitor extends AbstractDeserializationVisitor
 
             return;
         }
-        
+
         if ($metadata->xmlValue) {
             $v = $this->navigator->accept($data, $metadata->type, $this);
             $metadata->reflection->setValue($this->currentObject, $v);
@@ -232,7 +232,14 @@ class XmlDeserializationVisitor extends AbstractDeserializationVisitor
         }
 
         $v = $this->navigator->accept($data->$name, $metadata->type, $this);
-        $metadata->reflection->setValue($this->currentObject, $v);
+
+        if (null === $metadata->setter) {
+            $metadata->reflection->setValue($this->currentObject, $v);
+
+            return;
+        }
+
+        $this->currentObject->{$metadata->setter}($v);
     }
 
     public function endVisitingObject(ClassMetadata $metadata, $data, $type)

+ 4 - 1
Serializer/XmlSerializationVisitor.php

@@ -147,7 +147,10 @@ class XmlSerializationVisitor extends AbstractSerializationVisitor
 
     public function visitProperty(PropertyMetadata $metadata, $object)
     {
-        if (null === $v = $metadata->reflection->getValue($object)) {
+        $v = (null === $metadata->getter ? $metadata->reflection->getValue($object)
+            : $object->{$metadata->getter}());
+
+        if (null === $v) {
             return;
         }
 

+ 4 - 1
Serializer/YamlSerializationVisitor.php

@@ -147,7 +147,10 @@ class YamlSerializationVisitor extends AbstractSerializationVisitor
 
     public function visitProperty(PropertyMetadata $metadata, $data)
     {
-        if (null === $v = $metadata->reflection->getValue($data)) {
+        $v = (null === $metadata->getter ? $metadata->reflection->getValue($data)
+            : $data->{$metadata->getter}());
+
+        if (null === $v) {
             return;
         }
 

+ 5 - 0
Tests/Fixtures/Author.php

@@ -33,4 +33,9 @@ class Author
     {
         $this->name = $name;
     }
+
+    public function getName()
+    {
+        return $this->name;
+    }
 }

+ 5 - 0
Tests/Fixtures/Comment.php

@@ -37,4 +37,9 @@ class Comment
         $this->author = $author;
         $this->text = $text;
     }
+
+    public function getAuthor()
+    {
+        return $this->author;
+    }
 }

+ 31 - 0
Tests/Fixtures/GetSetObject.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace JMS\SerializerBundle\Tests\Fixtures;
+
+use JMS\SerializerBundle\Annotation\AccessType;
+use JMS\SerializerBundle\Annotation\Type;
+
+/** @AccessType("public_method") */
+class GetSetObject
+{
+    /** @AccessType("property") @Type("integer") */
+    private $id = 1;
+
+    /** @Type("string") */
+    private $name = 'Foo';
+
+    public function getId()
+    {
+        throw new \RuntimeException('This should not be called.');
+    }
+
+    public function getName()
+    {
+        return 'Johannes';
+    }
+
+    public function setName($name)
+    {
+        $this->name = $name;
+    }
+}

+ 57 - 0
Tests/Fixtures/IndexedCommentsBlogPost.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace JMS\SerializerBundle\Tests\Fixtures;
+
+use JMS\SerializerBundle\Annotation\Accessor;
+use JMS\SerializerBundle\Annotation\XmlMap;
+use JMS\SerializerBundle\Annotation\XmlRoot;
+use JMS\SerializerBundle\Annotation\XmlList;
+use JMS\SerializerBundle\Annotation\XmlAttribute;
+
+/** @XmlRoot("post") */
+class IndexedCommentsBlogPost
+{
+    /**
+     * @XmlMap(keyAttribute="author-name", inline=true, entry="comments")
+     * @Accessor(getter="getCommentsIndexedByAuthor")
+     */
+    private $comments = array();
+
+    public function __construct()
+    {
+        $author = new Author('Foo');
+        $this->comments[] = new Comment($author, 'foo');
+        $this->comments[] = new Comment($author, 'bar');
+    }
+
+    public function getCommentsIndexedByAuthor()
+    {
+        $indexedComments = array();
+        foreach ($this->comments as $comment) {
+            $authorName = $comment->getAuthor()->getName();
+
+            if (!isset($indexedComments[$authorName])) {
+                $indexedComments[$authorName] = new IndexedCommentsList();
+            }
+
+            $indexedComments[$authorName]->addComment($comment);
+        }
+
+        return $indexedComments;
+    }
+}
+
+class IndexedCommentsList
+{
+    /** @XmlList(inline=true, entry="comment") */
+    private $comments = array();
+
+    /** @XmlAttribute */
+    private $count = 0;
+
+    public function addComment(Comment $comment)
+    {
+        $this->comments[] = $comment;
+        $this->count += 1;
+    }
+}

+ 1 - 0
Tests/Fixtures/InvalidUsageOfXmlValue.php

@@ -4,6 +4,7 @@ namespace JMS\SerializerBundle\Tests\Fixtures;
 
 use JMS\SerializerBundle\Annotation\XmlValue;
 
+/** Dummy */
 class InvalidUsageOfXmlValue
 {
     /** @XmlValue */

+ 24 - 0
Tests/Serializer/BaseSerializationTest.php

@@ -18,6 +18,10 @@
 
 namespace JMS\SerializerBundle\Tests\Serializer;
 
+use JMS\SerializerBundle\Tests\Fixtures\GetSetObject;
+
+use JMS\SerializerBundle\Tests\Fixtures\IndexedCommentsBlogPost;
+
 use JMS\SerializerBundle\Tests\Fixtures\CurrencyAwareOrder;
 use JMS\SerializerBundle\Tests\Fixtures\CurrencyAwarePrice;
 use JMS\SerializerBundle\Tests\Fixtures\Order;
@@ -356,6 +360,26 @@ abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals($this->getContent('orm_proxy'), $this->serialize($object));
     }
 
+    public function testCustomAccessor()
+    {
+        $post = new IndexedCommentsBlogPost();
+
+        $this->assertEquals($this->getContent('custom_accessor'), $this->serialize($post));
+    }
+
+    public function testMixedAccessTypes()
+    {
+        $object = new GetSetObject();
+
+        $this->assertEquals($this->getContent('mixed_access_types'), $this->serialize($object));
+
+        if ($this->hasDeserializer()) {
+            $object = $this->deserialize($this->getContent('mixed_access_types'), 'JMS\SerializerBundle\Tests\Fixtures\GetSetObject');
+            $this->assertAttributeEquals(1, 'id', $object);
+            $this->assertAttributeEquals('Johannes', 'name', $object);
+        }
+    }
+
     abstract protected function getContent($key);
     abstract protected function getFormat();
 

+ 2 - 0
Tests/Serializer/JsonSerializationTest.php

@@ -55,6 +55,8 @@ class JsonSerializationTest extends BaseSerializationTest
             $outputs['constraint_violation_list'] = '[{"property_path":"foo","message":"Message of violation"},{"property_path":"bar","message":"Message of another violation"}]';
             $outputs['article'] = '{"custom":"serialized"}';
             $outputs['orm_proxy'] = '{"foo":"foo","moo":"bar","camel_case":"proxy-boo"}';
+            $outputs['custom_accessor'] = '{"comments":{"Foo":{"comments":[{"author":{"full_name":"Foo"},"text":"foo"},{"author":{"full_name":"Foo"},"text":"bar"}],"count":2}}}';
+            $outputs['mixed_access_types'] = '{"id":1,"name":"Johannes"}';
         }
 
         if (!isset($outputs[$key])) {

+ 17 - 0
Tests/Serializer/xml/custom_accessor.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<post>
+  <comments author-name="Foo" count="2">
+    <comment>
+      <author>
+        <full_name><![CDATA[Foo]]></full_name>
+      </author>
+      <text><![CDATA[foo]]></text>
+    </comment>
+    <comment>
+      <author>
+        <full_name><![CDATA[Foo]]></full_name>
+      </author>
+      <text><![CDATA[bar]]></text>
+    </comment>
+  </comments>
+</post>

+ 5 - 0
Tests/Serializer/xml/mixed_access_types.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<result>
+  <id>1</id>
+  <name><![CDATA[Johannes]]></name>
+</result>

+ 12 - 0
Tests/Serializer/yml/custom_accessor.yml

@@ -0,0 +1,12 @@
+comments:
+    Foo:
+        comments:
+            -
+                author:
+                    full_name: Foo
+                text: foo
+            -
+                author:
+                    full_name: Foo
+                text: bar
+        count: 2

+ 13 - 0
Tests/Serializer/yml/custom_accessor.yml~

@@ -0,0 +1,13 @@
+comments:
+    Foo:
+        comments:
+            -
+                author:
+                    full_name: Foo
+                text: foo
+            -
+                author:
+                    full_name: Foo
+                text: bar
+        count: 2
+

+ 2 - 0
Tests/Serializer/yml/mixed_access_types.yml

@@ -0,0 +1,2 @@
+id: 1
+name: Johannes

+ 0 - 0
Tests/Serializer/yml/mixed_access_types.yml~