Przeglądaj źródła

adds support for controlling the order of properties

Johannes M. Schmitt 13 lat temu
rodzic
commit
ef054c946d

+ 24 - 0
Annotation/AccessorOrder.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace JMS\SerializerBundle\Annotation;
+
+/**
+ * Controls the order of properties in a class.
+ *
+ * @Annotation
+ * @Target("CLASS")
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+final class AccessorOrder
+{
+	/**
+	 * @Required
+	 * @var string
+	 */
+	public $order;
+
+	/**
+	 * @var array<string>
+	 */
+	public $custom = array();
+}

+ 84 - 0
Metadata/ClassMetadata.php

@@ -23,12 +23,52 @@ use Metadata\MergeableInterface;
 use Metadata\MethodMetadata;
 use Metadata\MergeableClassMetadata;
 
+/**
+ * Class Metadata used to customize the serialization process.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
 class ClassMetadata extends MergeableClassMetadata
 {
+	const ACCESSOR_ORDER_UNDEFINED = 'undefined';
+	const ACCESSOR_ORDER_ALPHABETICAL = 'alphabetical';
+	const ACCESSOR_ORDER_CUSTOM = 'custom';
+
     public $preSerializeMethods = array();
     public $postSerializeMethods = array();
     public $postDeserializeMethods = array();
     public $xmlRootName;
+    public $accessorOrder;
+    public $customOrder;
+
+    /**
+     * Sets the order of properties in the class.
+     *
+     * @param string $order
+     * @param array $customOrder
+     */
+    public function setAccessorOrder($order, array $customOrder = array())
+    {
+    	if (!in_array($order, array(self::ACCESSOR_ORDER_UNDEFINED, self::ACCESSOR_ORDER_ALPHABETICAL, self::ACCESSOR_ORDER_CUSTOM), true)) {
+    		throw new \InvalidArgumentException(sprintf('The accessor order "%s" is invalid.', $order));
+    	}
+
+    	foreach ($customOrder as $name) {
+    		if (!is_string($name)) {
+    			throw new \InvalidArgumentException(sprintf('$customOrder is expected to be a list of strings, but got element of value %s.', json_encode($name)));
+    		}
+    	}
+
+    	$this->accessorOrder = $order;
+    	$this->customOrder = array_flip($customOrder);
+    	$this->sortProperties();
+    }
+
+    public function addPropertyMetadata(PropertyMetadata $metadata)
+    {
+    	parent::addPropertyMetadata($metadata);
+    	$this->sortProperties();
+    }
 
     public function addPreSerializeMethod(MethodMetadata $method)
     {
@@ -56,15 +96,26 @@ class ClassMetadata extends MergeableClassMetadata
         $this->postSerializeMethods = array_merge($this->postSerializeMethods, $object->postSerializeMethods);
         $this->postDeserializeMethods = array_merge($this->postDeserializeMethods, $object->postDeserializeMethods);
         $this->xmlRootName = $object->xmlRootName;
+
+        if ($object->accessorOrder) {
+        	$this->accessorOrder = $object->accessorOrder;
+        	$this->customOrder = $object->customOrder;
+        }
+
+        $this->sortProperties();
     }
 
     public function serialize()
     {
+    	$this->sortProperties();
+
         return serialize(array(
             $this->preSerializeMethods,
             $this->postSerializeMethods,
             $this->postDeserializeMethods,
             $this->xmlRootName,
+        	$this->accessorOrder,
+        	$this->customOrder,
             parent::serialize(),
         ));
     }
@@ -76,9 +127,42 @@ class ClassMetadata extends MergeableClassMetadata
             $this->postSerializeMethods,
             $this->postDeserializeMethods,
             $this->xmlRootName,
+        	$this->accessorOrder,
+        	$this->customOrder,
             $parentStr
         ) = unserialize($str);
 
         parent::unserialize($parentStr);
     }
+
+    private function sortProperties()
+    {
+    	switch ($this->accessorOrder) {
+    		case self::ACCESSOR_ORDER_ALPHABETICAL:
+    			ksort($this->propertyMetadata);
+    			break;
+
+    		case self::ACCESSOR_ORDER_CUSTOM:
+    			$order = $this->customOrder;
+    			uksort($this->propertyMetadata, function($a, $b) use ($order) {
+    				$existsA = isset($order[$a]);
+    				$existsB = isset($order[$b]);
+
+    				if (!$existsA && !$existsB) {
+    					return 0;
+    				}
+
+    				if (!$existsA) {
+    					return 1;
+    				}
+
+    				if (!$existsB) {
+    					return -1;
+    				}
+
+    				return $order[$a] < $order[$b] ? -1 : 1;
+    			});
+    			break;
+    	}
+    }
 }

+ 8 - 0
Metadata/Driver/AnnotationDriver.php

@@ -18,6 +18,10 @@
 
 namespace JMS\SerializerBundle\Metadata\Driver;
 
+use JMS\SerializerBundle\Annotation\AccessorOrder;
+
+use JMS\SerializerBundle\Annotation\Groups;
+
 use JMS\SerializerBundle\Annotation\Accessor;
 
 use JMS\SerializerBundle\Annotation\AccessType;
@@ -69,6 +73,8 @@ class AnnotationDriver implements DriverInterface
                 $excludeAll = true;
             } else if ($annot instanceof AccessType) {
                 $classAccessType = $annot->type;
+            } else if ($annot instanceof AccessorOrder) {
+            	$classMetadata->setAccessorOrder($annot->order, $annot->custom);
             }
         }
 
@@ -112,6 +118,8 @@ class AnnotationDriver implements DriverInterface
                         $AccessType = $annot->type;
                     } else if ($annot instanceof Accessor) {
                         $accessor = array($annot->getter, $annot->setter);
+                    } else if ($annot instanceof Groups) {
+                    	$propertyMetadata->setGroups($annot->names);
                     }
                 }
 

+ 4 - 0
Metadata/Driver/XmlDriver.php

@@ -50,6 +50,10 @@ class XmlDriver extends AbstractFileDriver
         $excludeAll = null !== ($exclude = $elem->attributes()->exclude) ? 'true' === (string) $exclude : false;
         $classAccessType = (string) ($elem->attributes()->{'access-type'} ?: PropertyMetadata::ACCESS_TYPE_PROPERTY);
 
+        if (null !== $accessorOrder = $elem->attributes()->{'accessor-order'}) {
+        	$metadata->setAccessorOrder((string) $accessorOrder, preg_split('/\s*,\s*/', (string) $elem->attributes()->{'custom-accessor-order'}));
+        }
+
         if (null !== $xmlRootName = $elem->attributes()->{'xml-root-name'}) {
             $metadata->xmlRootName = (string) $xmlRootName;
         }

+ 4 - 0
Metadata/Driver/YamlDriver.php

@@ -44,6 +44,10 @@ class YamlDriver extends AbstractFileDriver
         $excludeAll = isset($config['exclude']) ? (Boolean) $config['exclude'] : false;
         $classAccessType = isset($config['access_type']) ? $config['access_type'] : PropertyMetadata::ACCESS_TYPE_PROPERTY;
 
+        if (isset($config['accessor_order'])) {
+        	$metadata->setAccessorOrder($config['accessor_order'], isset($config['custom_accessor_order']) ? $config['custom_accessor_order'] : array());
+        }
+
         if (isset($config['xml_root_name'])) {
             $metadata->xmlRootName = (string) $config['xml_root_name'];
         }

+ 32 - 1
Resources/doc/index.rst

@@ -284,6 +284,34 @@ be called to retrieve, or set the value of the given property::
         }
     }
 
+@AccessorOrder
+~~~~~~~~~~~~~~
+This annotation can be defined on a class to control the order of properties. By 
+default the order is undefined, but you may change it to either "alphabetical", or
+"custom".
+
+    /** 
+     * @AccessorOrder("alphabetical") 
+     * 
+     * Resulting Property Order: id, name
+     */
+    class User
+    {
+        private $id;
+        private $name;
+    }
+    
+    /**
+     * @AccessorOrder("custom", custom = {"name", "id"})
+     *
+     * Resulting Property Order: name, id
+     */
+    class User
+    {
+        private $id;
+        private $name;
+    }
+
 @PreSerialize
 ~~~~~~~~~~~~~
 This annotation can be defined on a method which is supposed to be called before
@@ -511,7 +539,8 @@ XML Reference
     <!-- MyBundle\Resources\config\serializer\ClassName.xml -->
     <?xml version="1.0" encoding="UTF-8">
     <serializer>
-        <class name="Fully\Qualified\ClassName" exclusion-policy="ALL" xml-root-name="foo-bar" exclude="true">
+        <class name="Fully\Qualified\ClassName" exclusion-policy="ALL" xml-root-name="foo-bar" exclude="true"
+            accessor-order="custom" custom-accessor-order="propertyName1,propertyName2,...,propertyNameN">
             <property name="some-property"
                       exclude="true"
                       expose="true"
@@ -543,6 +572,8 @@ YAML Reference
         xml_root_name: foobar
         exclude: true
         access_type: public_method # defaults to property
+        accessor_order: custom
+        custom_accessor_order: [propertyName1, propertyName2, ..., propertyNameN]
         properties:
             some-property:
                 exclude: true

+ 11 - 0
Tests/Fixtures/AccessorOrderChild.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace JMS\SerializerBundle\Tests\Fixtures;
+
+use JMS\SerializerBundle\Annotation as Serializer;
+
+/** @Serializer\AccessorOrder("custom", custom = {"c", "d", "a", "b"}) */
+class AccessorOrderChild extends AccessorOrderParent
+{
+	private $c = 'c', $d = 'd';
+}

+ 11 - 0
Tests/Fixtures/AccessorOrderParent.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace JMS\SerializerBundle\Tests\Fixtures;
+
+use JMS\SerializerBundle\Annotation as Serializer;
+
+/** @Serializer\AccessorOrder("alphabetical") */
+class AccessorOrderParent
+{
+	private $b = 'b', $a = 'a';
+}

+ 40 - 0
Tests/Metadata/ClassMetadataTest.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace JMS\SerializerBundle\Tests\Metadata;
+
+use JMS\SerializerBundle\Metadata\PropertyMetadata;
+use JMS\SerializerBundle\Metadata\ClassMetadata;
+
+class ClassMetadataTest extends \PHPUnit_Framework_TestCase
+{
+	public function testSetAccessorOrder()
+	{
+		$metadata = new ClassMetadata('JMS\SerializerBundle\Tests\Metadata\PropertyMetadataOrder');
+		$metadata->addPropertyMetadata(new PropertyMetadata('JMS\SerializerBundle\Tests\Metadata\PropertyMetadataOrder', 'b'));
+		$metadata->addPropertyMetadata(new PropertyMetadata('JMS\SerializerBundle\Tests\Metadata\PropertyMetadataOrder', 'a'));
+		$this->assertEquals(array('b', 'a'), array_keys($metadata->propertyMetadata));
+
+		$metadata->setAccessorOrder(ClassMetadata::ACCESSOR_ORDER_ALPHABETICAL);
+		$this->assertEquals(array('a', 'b'), array_keys($metadata->propertyMetadata));
+
+		$metadata->setAccessorOrder(ClassMetadata::ACCESSOR_ORDER_CUSTOM, array('b', 'a'));
+		$this->assertEquals(array('b', 'a'), array_keys($metadata->propertyMetadata));
+
+		$metadata->setAccessorOrder(ClassMetadata::ACCESSOR_ORDER_CUSTOM, array('a', 'b'));
+		$this->assertEquals(array('a', 'b'), array_keys($metadata->propertyMetadata));
+
+		$metadata->setAccessorOrder(ClassMetadata::ACCESSOR_ORDER_CUSTOM, array('b'));
+		$this->assertEquals(array('b', 'a'), array_keys($metadata->propertyMetadata));
+
+		$metadata->setAccessorOrder(ClassMetadata::ACCESSOR_ORDER_CUSTOM, array('a'));
+		$this->assertEquals(array('a', 'b'), array_keys($metadata->propertyMetadata));
+
+		$metadata->setAccessorOrder(ClassMetadata::ACCESSOR_ORDER_CUSTOM, array('foo', 'bar'));
+		$this->assertEquals(array('b', 'a'), array_keys($metadata->propertyMetadata));
+	}
+}
+
+class PropertyMetadataOrder
+{
+	private $b, $a;
+}

+ 10 - 0
Tests/Serializer/BaseSerializationTest.php

@@ -18,6 +18,10 @@
 
 namespace JMS\SerializerBundle\Tests\Serializer;
 
+use JMS\SerializerBundle\Tests\Fixtures\AccessorOrderParent;
+
+use JMS\SerializerBundle\Tests\Fixtures\AccessorOrderChild;
+
 use JMS\SerializerBundle\Tests\Fixtures\GetSetObject;
 
 use JMS\SerializerBundle\Tests\Fixtures\IndexedCommentsBlogPost;
@@ -380,6 +384,12 @@ abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
         }
     }
 
+    public function testAccessorOrder()
+    {
+    	$this->assertEquals($this->getContent('accessor_order_child'), $this->serialize(new AccessorOrderChild()));
+    	$this->assertEquals($this->getContent('accessor_order_parent'), $this->serialize(new AccessorOrderParent()));
+    }
+
     abstract protected function getContent($key);
     abstract protected function getFormat();
 

+ 2 - 0
Tests/Serializer/JsonSerializationTest.php

@@ -57,6 +57,8 @@ class JsonSerializationTest extends BaseSerializationTest
             $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"}';
+            $outputs['accessor_order_child'] = '{"c":"c","d":"d","a":"a","b":"b"}';
+            $outputs['accessor_order_parent'] = '{"a":"a","b":"b"}';
         }
 
         if (!isset($outputs[$key])) {

+ 7 - 0
Tests/Serializer/xml/accessor_order_child.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<result>
+  <c><![CDATA[c]]></c>
+  <d><![CDATA[d]]></d>
+  <a><![CDATA[a]]></a>
+  <b><![CDATA[b]]></b>
+</result>

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

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

+ 4 - 0
Tests/Serializer/yml/accessor_order_child.yml

@@ -0,0 +1,4 @@
+c: c
+d: d
+a: a
+b: b

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

@@ -0,0 +1,2 @@
+a: a
+b: b