浏览代码

Merge pull request #4 from adrienbrault/max-depth-strategy

Max depth strategy
Johannes 12 年之前
父节点
当前提交
5b8b4b474b

+ 41 - 0
doc/cookbook/exclusion_strategies.rst

@@ -112,3 +112,44 @@ You can then tell the serializer which groups to serialize in your controller::
     use JMS\Serializer\SerializationContext;
 
     $serializer->serialize(new BlogPost(), 'json', SerializationContext::create()->setGroups(array('list')));
+
+Limiting serialization depth of some properties
+-----------------------------------------------
+You can limit the depth of what will be serialized in a property with the
+``@MaxDepth`` annotation.
+This exclusion strategy is a bit different from the others, because it will
+affect the serialized content of others classes than the one you apply the
+annotation to.
+
+.. code-block :: php
+
+    use JMS\Serializer\Annotation\MaxDepth;
+
+    class User
+    {
+        private $username;
+
+        /** @MaxDepth(1) */
+        private $friends;
+
+        /** @MaxDepth(2) */
+        private $posts;
+    }
+
+    class Post
+    {
+        private $title;
+
+        private $author;
+    }
+
+In this example, serializing a user, because the max depth of the ``$friends``
+property is 1, the user friends would be serialized, but not their friends;
+and because the the max depth of the ``$posts`` property is 2, the posts would
+be serialized, and their author would also be serialized.
+
+You need to tell the serializer to take into account MaxDepth checks::
+
+    use JMS\Serializer\SerializationContext;
+
+    $serializer->serialize($data, 'json', SerializationContext::create()->enableMaxDepthChecks());

+ 7 - 1
doc/reference/annotations.rst

@@ -52,6 +52,12 @@ This annotation can be defined on a property to specifiy to if the property
 should be serialized when only serializing specific groups (see
 :doc:`../cookbook/exclusion_strategies`).
 
+@MaxDepth
+~~~~~~~~~
+This annotation can be defined on a property to limit the depth to which the
+content will be serialized. It is very useful when a property will contain a
+large object graph.
+
 @AccessType
 ~~~~~~~~~~~
 This annotation can be defined on a property, or a class to specify in which way
@@ -474,4 +480,4 @@ Resulting XML:
 
 .. code-block :: xml
 
-    <result name="firstname" value="Adrien"/>
+    <result name="firstname" value="Adrien"/>

+ 1 - 0
doc/reference/xml_reference.rst

@@ -25,6 +25,7 @@ XML Reference
                       groups="foo,bar"
                       xml-key-value-pairs="true"
                       xml-attribute-map="true"
+                      max-depth="2"
             >
                 <!-- You can also specify the type as element which is necessary if
                      your type contains "<" or ">" characters. -->

+ 1 - 0
doc/reference/yml_reference.rst

@@ -40,6 +40,7 @@ YAML Reference
                     key_attribute_name: foo
                     entry_name: bar
                 xml_attribute_map: true
+                max_depth: 2
 
         handler_callbacks:
             serialization:

+ 32 - 0
src/JMS/Serializer/Annotation/MaxDepth.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * Copyright 2011 Johannes M. Schmitt <schmittjoh@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace JMS\Serializer\Annotation;
+
+/**
+ * @Annotation
+ * @Target({"PROPERTY","METHOD"})
+ */
+final class MaxDepth
+{
+    /**
+     * @Required
+     * @var integer
+     */
+    public $depth;
+}

+ 8 - 0
src/JMS/Serializer/Context.php

@@ -19,6 +19,7 @@
 namespace JMS\Serializer;
 
 use JMS\Serializer\Exception\RuntimeException;
+use JMS\Serializer\Exclusion\DepthExclusionStrategy;
 use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
 use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
 use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
@@ -173,6 +174,13 @@ abstract class Context
         return $this;
     }
 
+    public function enableMaxDepthChecks()
+    {
+        $this->addExclusionStrategy(new DepthExclusionStrategy());
+
+        return $this;
+    }
+
     public function setSerializeNull($bool)
     {
         $this->serializeNull = (boolean) $bool;

+ 67 - 0
src/JMS/Serializer/Exclusion/DepthExclusionStrategy.php

@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * Copyright 2011 Johannes M. Schmitt <schmittjoh@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace JMS\Serializer\Exclusion;
+
+use JMS\Serializer\Context;
+use JMS\Serializer\Metadata\ClassMetadata;
+use JMS\Serializer\Metadata\PropertyMetadata;
+
+/**
+ * @author Adrien Brault <adrien.brault@gmail.com>
+ */
+class DepthExclusionStrategy implements ExclusionStrategyInterface
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function shouldSkipClass(ClassMetadata $metadata, Context $context)
+    {
+        return $this->isTooDeep($context);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function shouldSkipProperty(PropertyMetadata $property, Context $context)
+    {
+        return $this->isTooDeep($context);
+    }
+
+    private function isTooDeep(Context $context)
+    {
+        $depth = $context->getDepth();
+        $metadataStack = $context->getMetadataStack();
+
+        $nthProperty = 0;
+        // iterate from the first added items to the lasts
+        for ($i = $metadataStack->count() - 1; $i > 0; $i--) {
+            $metadata = $metadataStack[$i];
+            if ($metadata instanceof PropertyMetadata) {
+                $nthProperty++;
+                $relativeDepth = $depth - $nthProperty;
+
+                if (null !== $metadata->maxDepth && $relativeDepth > $metadata->maxDepth) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+}

+ 3 - 0
src/JMS/Serializer/Metadata/Driver/AnnotationDriver.php

@@ -52,6 +52,7 @@ use JMS\Serializer\Metadata\VirtualPropertyMetadata;
 use JMS\Serializer\Exception\InvalidArgumentException;
 use JMS\Serializer\Annotation\XmlAttributeMap;
 use Metadata\Driver\DriverInterface;
+use JMS\Serializer\Annotation\MaxDepth;
 
 class AnnotationDriver implements DriverInterface
 {
@@ -189,6 +190,8 @@ class AnnotationDriver implements DriverInterface
                         $propertyMetadata->inline = true;
                     } elseif ($annot instanceof XmlAttributeMap) {
                         $propertyMetadata->xmlAttributeMap = true;
+                    } elseif ($annot instanceof MaxDepth) {
+                        $propertyMetadata->maxDepth = $annot->depth;
                     }
                 }
 

+ 4 - 0
src/JMS/Serializer/Metadata/Driver/XmlDriver.php

@@ -187,6 +187,10 @@ class XmlDriver extends AbstractFileDriver
                         $pMetadata->xmlKeyValuePairs = 'true' === (string) $pElem->attributes()->{'xml-key-value-pairs'};
                     }
 
+                    if (isset($pElem->attributes()->{'max-depth'})) {
+                        $pMetadata->maxDepth = (int) $pElem->attributes()->{'max-depth'};
+                    }
+
                     //we need read-only before setter and getter set, because that method depends on flag being set
                     if (null !== $readOnly = $pElem->attributes()->{'read-only'}) {
                         $pMetadata->readOnly = 'true' === strtolower($readOnly);

+ 3 - 0
src/JMS/Serializer/Metadata/Driver/YamlDriver.php

@@ -193,6 +193,9 @@ class YamlDriver extends AbstractFileDriver
                         $pMetadata->inline = (Boolean) $pConfig['inline'];
                     }
 
+                    if (isset($pConfig['max_depth'])) {
+                        $pMetadata->maxDepth = (int) $pConfig['max_depth'];
+                    }
                 }
                 if ((ExclusionPolicy::NONE === $exclusionPolicy && !$isExclude)
                 || (ExclusionPolicy::ALL === $exclusionPolicy && $isExpose)) {

+ 3 - 0
src/JMS/Serializer/Metadata/PropertyMetadata.php

@@ -44,6 +44,7 @@ class PropertyMetadata extends BasePropertyMetadata
     public $inline = false;
     public $readOnly = false;
     public $xmlAttributeMap = false;
+    public $maxDepth = null;
 
     private static $typeParser;
 
@@ -113,6 +114,7 @@ class PropertyMetadata extends BasePropertyMetadata
             $this->inline,
             $this->readOnly,
             $this->xmlAttributeMap,
+            $this->maxDepth,
             parent::serialize(),
         ));
     }
@@ -137,6 +139,7 @@ class PropertyMetadata extends BasePropertyMetadata
             $this->inline,
             $this->readOnly,
             $this->xmlAttributeMap,
+            $this->maxDepth,
             $parentStr
         ) = unserialize($str);
 

+ 5 - 0
tests/JMS/Serializer/Tests/Fixtures/Node.php

@@ -22,8 +22,13 @@ use JMS\Serializer\Annotation as Serializer;
 
 class Node
 {
+    /**
+     * @Serializer\MaxDepth(2)
+     */
     public $children;
 
+    public $foo = 'bar';
+
     public function __construct($children = array())
     {
         $this->children = $children;

+ 34 - 0
tests/JMS/Serializer/Tests/Fixtures/Tree.php

@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * Copyright 2013 Johannes M. Schmitt <schmittjoh@gmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace JMS\Serializer\Tests\Fixtures;
+
+use JMS\Serializer\Annotation as Serializer;
+
+class Tree
+{
+    /**
+     * @Serializer\MaxDepth(10)
+     */
+    public $tree;
+
+    public function __construct($tree)
+    {
+        $this->tree = $tree;
+    }
+}

+ 7 - 0
tests/JMS/Serializer/Tests/Metadata/Driver/BaseDriverTest.php

@@ -145,6 +145,13 @@ abstract class BaseDriverTest extends \PHPUnit_Framework_TestCase
         $this->assertEquals(array(), $m->discriminatorMap);
     }
 
+    public function testMaxDepth()
+    {
+        $m = $this->getDriver()->loadMetadataForClass(new \ReflectionClass('JMS\Serializer\Tests\Fixtures\Node'));
+
+        $this->assertEquals(2, $m->propertyMetadata['children']->maxDepth);
+    }
+
     /**
      * @return DriverInterface
      */

+ 12 - 0
tests/JMS/Serializer/Tests/Metadata/Driver/php/Node.php

@@ -0,0 +1,12 @@
+<?php
+
+use JMS\Serializer\Metadata\ClassMetadata;
+use JMS\Serializer\Metadata\PropertyMetadata;
+
+$metadata = new ClassMetadata('JMS\Serializer\Tests\Fixtures\Node');
+
+$pMetadata = new PropertyMetadata('JMS\Serializer\Tests\Fixtures\Node', 'children');
+$pMetadata->maxDepth = 2;
+$metadata->addPropertyMetadata($pMetadata);
+
+return $metadata;

+ 6 - 0
tests/JMS/Serializer/Tests/Metadata/Driver/xml/Node.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<serializer>
+    <class name="JMS\Serializer\Tests\Fixtures\Node">
+        <property name="children" max-depth="2" />
+    </class>
+</serializer>

+ 4 - 0
tests/JMS/Serializer/Tests/Metadata/Driver/yml/Node.yml

@@ -0,0 +1,4 @@
+JMS\Serializer\Tests\Fixtures\Node:
+    properties:
+        children:
+            max_depth: 2

+ 24 - 0
tests/JMS/Serializer/Tests/Serializer/BaseSerializationTest.php

@@ -24,6 +24,7 @@ use JMS\Serializer\GraphNavigator;
 use JMS\Serializer\Handler\PhpCollectionHandler;
 use JMS\Serializer\SerializationContext;
 use JMS\Serializer\Tests\Fixtures\Discriminator\Car;
+use JMS\Serializer\Tests\Fixtures\Tree;
 use PhpCollection\Sequence;
 use Symfony\Component\Translation\MessageSelector;
 use Symfony\Component\Translation\IdentityTranslator;
@@ -82,6 +83,8 @@ use Symfony\Component\Form\FormError;
 use Symfony\Component\Validator\ConstraintViolation;
 use Symfony\Component\Validator\ConstraintViolationList;
 use PhpCollection\Map;
+use JMS\Serializer\Exclusion\DepthExclusionStrategy;
+use JMS\Serializer\Tests\Fixtures\Node;
 
 abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
 {
@@ -708,6 +711,27 @@ abstract class BaseSerializationTest extends \PHPUnit_Framework_TestCase
         );
     }
 
+    public function testDepthExclusionStrategy()
+    {
+        $context = SerializationContext::create()
+            ->addExclusionStrategy(new DepthExclusionStrategy())
+        ;
+
+        $data = new Tree(
+            new Node(array(
+                new Node(array(
+                    new Node(array(
+                        new Node(array(
+                            new Node(),
+                        )),
+                    )),
+                )),
+            ))
+        );
+
+        $this->assertEquals($this->getContent('tree'), $this->serializer->serialize($data, $this->getFormat(), $context));
+    }
+
     abstract protected function getContent($key);
     abstract protected function getFormat();
 

+ 1 - 0
tests/JMS/Serializer/Tests/Serializer/JsonSerializationTest.php

@@ -87,6 +87,7 @@ class JsonSerializationTest extends BaseSerializationTest
             $outputs['date_interval'] = '"PT45M"';
             $outputs['car'] = '{"km":5,"type":"car"}';
             $outputs['car_without_type'] = '{"km":5}';
+            $outputs['tree'] = '{"tree":{"children":[{"children":[{"children":[],"foo":"bar"}],"foo":"bar"}],"foo":"bar"}}';
         }
 
         if (!isset($outputs[$key])) {

+ 19 - 0
tests/JMS/Serializer/Tests/Serializer/xml/tree.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<result>
+  <tree>
+    <children>
+      <entry>
+        <children>
+          <entry>
+            <children>
+              <entry/>
+            </children>
+            <foo><![CDATA[bar]]></foo>
+          </entry>
+        </children>
+        <foo><![CDATA[bar]]></foo>
+      </entry>
+    </children>
+    <foo><![CDATA[bar]]></foo>
+  </tree>
+</result>

+ 10 - 0
tests/JMS/Serializer/Tests/Serializer/yml/tree.yml

@@ -0,0 +1,10 @@
+tree:
+    children:
+        -
+            children:
+                -
+                    children:
+                        -
+                    foo: bar
+            foo: bar
+    foo: bar