Skip to content

Commit e479b11

Browse files
committed
Optimize isTooDeep() with delta scanning in DepthExclusionStrategy
Use three fast paths based on stack delta analysis to avoid full scans on every shouldSkipProperty()/shouldSkipClass() call. Same stack count retunrs cached result O(1). Stack shrunk and was not too deep O(1). Stack grew, no maxDepth on stack, not too deep, check only deltas O(delta). hasMaxDepthOnStack flag tracks if any maxDepth attributes exist on the stack. When false, growth never triggers full scan. When true or cazched result is too deep, the logic falls back to original full scan.
1 parent 82a579a commit e479b11

File tree

6 files changed

+128
-2
lines changed

6 files changed

+128
-2
lines changed

src/Exclusion/DepthExclusionStrategy.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
*/
1414
final class DepthExclusionStrategy implements ExclusionStrategyInterface
1515
{
16+
private bool $cachedResult = false;
17+
private int $cachedStackCount = -1;
18+
private bool $hasMaxDepthOnStack = false;
19+
1620
public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool
1721
{
1822
return $this->isTooDeep($context);
@@ -25,24 +29,75 @@ public function shouldSkipProperty(PropertyMetadata $property, Context $context)
2529

2630
private function isTooDeep(Context $context): bool
2731
{
32+
$stack = $context->getMetadataStack();
33+
$currentCount = $stack->count();
34+
35+
if ($currentCount === $this->cachedStackCount) {
36+
return $this->cachedResult;
37+
}
38+
39+
if ($currentCount < $this->cachedStackCount && !$this->cachedResult) {
40+
$this->cachedStackCount = $currentCount;
41+
42+
return false;
43+
}
44+
45+
if (!$this->hasMaxDepthOnStack && !$this->cachedResult && $currentCount > $this->cachedStackCount) {
46+
$delta = $currentCount - $this->cachedStackCount;
47+
$found = false;
48+
$i = 0;
49+
foreach ($stack as $metadata) {
50+
if ($i >= $delta) {
51+
break;
52+
}
53+
54+
if ($metadata instanceof PropertyMetadata && $metadata->maxDepth !== null) {
55+
$found = true;
56+
break;
57+
}
58+
59+
$i++;
60+
}
61+
62+
if (!$found) {
63+
$this->cachedStackCount = $currentCount;
64+
65+
return false;
66+
}
67+
}
68+
69+
// Full scan
2870
$relativeDepth = 0;
71+
$top = $currentCount > 0 ? $stack->top() : null;
72+
$foundMaxDepth = false;
2973

30-
foreach ($context->getMetadataStack() as $metadata) {
74+
foreach ($stack as $metadata) {
3175
if (!$metadata instanceof PropertyMetadata) {
3276
continue;
3377
}
3478

3579
$relativeDepth++;
3680

37-
if (0 === $metadata->maxDepth && $context->getMetadataStack()->top() === $metadata) {
81+
if (null !== $metadata->maxDepth) {
82+
$foundMaxDepth = true;
83+
}
84+
85+
if (0 === $metadata->maxDepth && $top === $metadata) {
3886
continue;
3987
}
4088

4189
if (null !== $metadata->maxDepth && $relativeDepth > $metadata->maxDepth) {
90+
$this->cachedResult = true;
91+
$this->cachedStackCount = $currentCount;
92+
4293
return true;
4394
}
4495
}
4596

97+
$this->hasMaxDepthOnStack = $foundMaxDepth;
98+
$this->cachedResult = false;
99+
$this->cachedStackCount = $currentCount;
100+
46101
return false;
47102
}
48103
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JMS\Serializer\Tests\Fixtures\MaxDepth;
6+
7+
class SiblingMaxDepthChild
8+
{
9+
public string $name;
10+
11+
public ?SiblingMaxDepthChild $child = null;
12+
13+
public function __construct(string $name)
14+
{
15+
$this->name = $name;
16+
}
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JMS\Serializer\Tests\Fixtures\MaxDepth;
6+
7+
use JMS\Serializer\Annotation as Serializer;
8+
9+
class SiblingMaxDepthParent
10+
{
11+
#[Serializer\MaxDepth(depth: 3)]
12+
public SiblingMaxDepthChild $deep;
13+
14+
#[Serializer\MaxDepth(depth: 1)]
15+
public SiblingMaxDepthChild $shallow;
16+
17+
public function __construct()
18+
{
19+
$this->deep = new SiblingMaxDepthChild('deep');
20+
$this->shallow = new SiblingMaxDepthChild('shallow');
21+
}
22+
}

tests/Serializer/BaseSerializationTestCase.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
use JMS\Serializer\Tests\Fixtures\MaxDepth\Gh1382Baz;
8888
use JMS\Serializer\Tests\Fixtures\MaxDepth\Gh1382Foo;
8989
use JMS\Serializer\Tests\Fixtures\MaxDepth\Gh236Foo;
90+
use JMS\Serializer\Tests\Fixtures\MaxDepth\SiblingMaxDepthChild;
91+
use JMS\Serializer\Tests\Fixtures\MaxDepth\SiblingMaxDepthParent;
9092
use JMS\Serializer\Tests\Fixtures\NamedDateTimeArraysObject;
9193
use JMS\Serializer\Tests\Fixtures\NamedDateTimeImmutableArraysObject;
9294
use JMS\Serializer\Tests\Fixtures\Node;
@@ -1853,6 +1855,20 @@ public function testMaxDepthWithOneDepthObject()
18531855
self::assertEquals(static::getContent('maxdepth_1'), $serialized);
18541856
}
18551857

1858+
public function testMaxDepthWithSiblingDifferentDepths()
1859+
{
1860+
$data = new SiblingMaxDepthParent();
1861+
$data->deep->child = new SiblingMaxDepthChild('deep_1');
1862+
$data->deep->child->child = new SiblingMaxDepthChild('deep_2');
1863+
$data->shallow->child = new SiblingMaxDepthChild('shallow_1');
1864+
$data->shallow->child->child = new SiblingMaxDepthChild('shallow_2');
1865+
1866+
$context = SerializationContext::create()->enableMaxDepthChecks();
1867+
$serialized = $this->serialize($data, $context);
1868+
1869+
self::assertEquals(static::getContent('maxdepth_sibling'), $serialized);
1870+
}
1871+
18561872
public function testDeserializingIntoExistingObject()
18571873
{
18581874
if (!$this->hasDeserializer()) {

tests/Serializer/JsonSerializationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ protected static function getContent($key)
130130
$outputs['maxdepth_skippable_object'] = '{"a":{"xxx":"yyy"}}';
131131
$outputs['maxdepth_0'] = '{"a":{}}';
132132
$outputs['maxdepth_1'] = '{"a":{"b":12345}}';
133+
$outputs['maxdepth_sibling'] = '{"deep":{"name":"deep","child":{"name":"deep_1","child":{"name":"deep_2"}}},"shallow":{"name":"shallow"}}';
133134
$outputs['array_objects_nullable'] = '[]';
134135
$outputs['type_casting'] = '{"as_string":"8"}';
135136
$outputs['authors_inline'] = '[{"full_name":"foo"},{"full_name":"bar"}]';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<result>
3+
<deep>
4+
<name><![CDATA[deep]]></name>
5+
<child>
6+
<name><![CDATA[deep_1]]></name>
7+
<child>
8+
<name><![CDATA[deep_2]]></name>
9+
</child>
10+
</child>
11+
</deep>
12+
<shallow>
13+
<name><![CDATA[shallow]]></name>
14+
</shallow>
15+
</result>

0 commit comments

Comments
 (0)