blob: 90d5ac9bcb8daa9a01aa73c86a0fa86234cdc55e [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\VarDumper\Cloner;
13
14/**
15 * @author Nicolas Grekas <p@tchwork.com>
16 */
17class VarCloner extends AbstractCloner
18{
19 private static $gid;
20 private static $arrayCache = [];
21
22 /**
23 * {@inheritdoc}
24 */
25 protected function doClone($var)
26 {
27 $len = 1; // Length of $queue
28 $pos = 0; // Number of cloned items past the minimum depth
29 $refsCounter = 0; // Hard references counter
30 $queue = [[$var]]; // This breadth-first queue is the return value
31 $hardRefs = []; // Map of original zval ids to stub objects
32 $objRefs = []; // Map of original object handles to their stub object counterpart
33 $objects = []; // Keep a ref to objects to ensure their handle cannot be reused while cloning
34 $resRefs = []; // Map of original resource handles to their stub object counterpart
35 $values = []; // Map of stub objects' ids to original values
36 $maxItems = $this->maxItems;
37 $maxString = $this->maxString;
38 $minDepth = $this->minDepth;
39 $currentDepth = 0; // Current tree depth
40 $currentDepthFinalIndex = 0; // Final $queue index for current tree depth
41 $minimumDepthReached = 0 === $minDepth; // Becomes true when minimum tree depth has been reached
42 $cookie = (object) []; // Unique object used to detect hard references
43 $a = null; // Array cast for nested structures
44 $stub = null; // Stub capturing the main properties of an original item value
45 // or null if the original value is used directly
46
47 if (!$gid = self::$gid) {
48 $gid = self::$gid = md5(random_bytes(6)); // Unique string used to detect the special $GLOBALS variable
49 }
50 $arrayStub = new Stub();
51 $arrayStub->type = Stub::TYPE_ARRAY;
52 $fromObjCast = false;
53
54 for ($i = 0; $i < $len; ++$i) {
55 // Detect when we move on to the next tree depth
56 if ($i > $currentDepthFinalIndex) {
57 ++$currentDepth;
58 $currentDepthFinalIndex = $len - 1;
59 if ($currentDepth >= $minDepth) {
60 $minimumDepthReached = true;
61 }
62 }
63
64 $refs = $vals = $queue[$i];
65 foreach ($vals as $k => $v) {
66 // $v is the original value or a stub object in case of hard references
67
68 if (\PHP_VERSION_ID >= 70400) {
69 $zvalIsRef = null !== \ReflectionReference::fromArrayElement($vals, $k);
70 } else {
71 $refs[$k] = $cookie;
72 $zvalIsRef = $vals[$k] === $cookie;
73 }
74
75 if ($zvalIsRef) {
76 $vals[$k] = &$stub; // Break hard references to make $queue completely
77 unset($stub); // independent from the original structure
78 if ($v instanceof Stub && isset($hardRefs[spl_object_id($v)])) {
79 $vals[$k] = $refs[$k] = $v;
80 if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) {
81 ++$v->value->refCount;
82 }
83 ++$v->refCount;
84 continue;
85 }
86 $refs[$k] = $vals[$k] = new Stub();
87 $refs[$k]->value = $v;
88 $h = spl_object_id($refs[$k]);
89 $hardRefs[$h] = &$refs[$k];
90 $values[$h] = $v;
91 $vals[$k]->handle = ++$refsCounter;
92 }
93 // Create $stub when the original value $v can not be used directly
94 // If $v is a nested structure, put that structure in array $a
95 switch (true) {
96 case null === $v:
97 case \is_bool($v):
98 case \is_int($v):
99 case \is_float($v):
100 continue 2;
101 case \is_string($v):
102 if ('' === $v) {
103 continue 2;
104 }
105 if (!preg_match('//u', $v)) {
106 $stub = new Stub();
107 $stub->type = Stub::TYPE_STRING;
108 $stub->class = Stub::STRING_BINARY;
109 if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) {
110 $stub->cut = $cut;
111 $stub->value = substr($v, 0, -$cut);
112 } else {
113 $stub->value = $v;
114 }
115 } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) {
116 $stub = new Stub();
117 $stub->type = Stub::TYPE_STRING;
118 $stub->class = Stub::STRING_UTF8;
119 $stub->cut = $cut;
120 $stub->value = mb_substr($v, 0, $maxString, 'UTF-8');
121 } else {
122 continue 2;
123 }
124 $a = null;
125 break;
126
127 case \is_array($v):
128 if (!$v) {
129 continue 2;
130 }
131 $stub = $arrayStub;
132 $stub->class = Stub::ARRAY_INDEXED;
133
134 $j = -1;
135 foreach ($v as $gk => $gv) {
136 if ($gk !== ++$j) {
137 $stub->class = Stub::ARRAY_ASSOC;
138 break;
139 }
140 }
141 $a = $v;
142
143 if (Stub::ARRAY_ASSOC === $stub->class) {
144 // Copies of $GLOBALS have very strange behavior,
145 // let's detect them with some black magic
146 if (\PHP_VERSION_ID < 80100 && ($a[$gid] = true) && isset($v[$gid])) {
147 unset($v[$gid]);
148 $a = [];
149 foreach ($v as $gk => &$gv) {
150 if ($v === $gv) {
151 unset($v);
152 $v = new Stub();
153 $v->value = [$v->cut = \count($gv), Stub::TYPE_ARRAY => 0];
154 $v->handle = -1;
155 $gv = &$hardRefs[spl_object_id($v)];
156 $gv = $v;
157 }
158
159 $a[$gk] = &$gv;
160 }
161 unset($gv);
162 } else {
163 $a = $v;
164 }
165 }
166 break;
167
168 case \is_object($v):
169 if (empty($objRefs[$h = spl_object_id($v)])) {
170 $stub = new Stub();
171 $stub->type = Stub::TYPE_OBJECT;
172 $stub->class = \get_class($v);
173 $stub->value = $v;
174 $stub->handle = $h;
175 $a = $this->castObject($stub, 0 < $i);
176 if ($v !== $stub->value) {
177 if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) {
178 break;
179 }
180 $stub->handle = $h = spl_object_id($stub->value);
181 }
182 $stub->value = null;
183 if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) {
184 $stub->cut = \count($a);
185 $a = null;
186 }
187 }
188 if (empty($objRefs[$h])) {
189 $objRefs[$h] = $stub;
190 $objects[] = $v;
191 } else {
192 $stub = $objRefs[$h];
193 ++$stub->refCount;
194 $a = null;
195 }
196 break;
197
198 default: // resource
199 if (empty($resRefs[$h = (int) $v])) {
200 $stub = new Stub();
201 $stub->type = Stub::TYPE_RESOURCE;
202 if ('Unknown' === $stub->class = @get_resource_type($v)) {
203 $stub->class = 'Closed';
204 }
205 $stub->value = $v;
206 $stub->handle = $h;
207 $a = $this->castResource($stub, 0 < $i);
208 $stub->value = null;
209 if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) {
210 $stub->cut = \count($a);
211 $a = null;
212 }
213 }
214 if (empty($resRefs[$h])) {
215 $resRefs[$h] = $stub;
216 } else {
217 $stub = $resRefs[$h];
218 ++$stub->refCount;
219 $a = null;
220 }
221 break;
222 }
223
224 if ($a) {
225 if (!$minimumDepthReached || 0 > $maxItems) {
226 $queue[$len] = $a;
227 $stub->position = $len++;
228 } elseif ($pos < $maxItems) {
229 if ($maxItems < $pos += \count($a)) {
230 $a = \array_slice($a, 0, $maxItems - $pos, true);
231 if ($stub->cut >= 0) {
232 $stub->cut += $pos - $maxItems;
233 }
234 }
235 $queue[$len] = $a;
236 $stub->position = $len++;
237 } elseif ($stub->cut >= 0) {
238 $stub->cut += \count($a);
239 $stub->position = 0;
240 }
241 }
242
243 if ($arrayStub === $stub) {
244 if ($arrayStub->cut) {
245 $stub = [$arrayStub->cut, $arrayStub->class => $arrayStub->position];
246 $arrayStub->cut = 0;
247 } elseif (isset(self::$arrayCache[$arrayStub->class][$arrayStub->position])) {
248 $stub = self::$arrayCache[$arrayStub->class][$arrayStub->position];
249 } else {
250 self::$arrayCache[$arrayStub->class][$arrayStub->position] = $stub = [$arrayStub->class => $arrayStub->position];
251 }
252 }
253
254 if ($zvalIsRef) {
255 $refs[$k]->value = $stub;
256 } else {
257 $vals[$k] = $stub;
258 }
259 }
260
261 if ($fromObjCast) {
262 $fromObjCast = false;
263 $refs = $vals;
264 $vals = [];
265 $j = -1;
266 foreach ($queue[$i] as $k => $v) {
267 foreach ([$k => true] as $gk => $gv) {
268 }
269 if ($gk !== $k) {
270 $vals = (object) $vals;
271 $vals->{$k} = $refs[++$j];
272 $vals = (array) $vals;
273 } else {
274 $vals[$k] = $refs[++$j];
275 }
276 }
277 }
278
279 $queue[$i] = $vals;
280 }
281
282 foreach ($values as $h => $v) {
283 $hardRefs[$h] = $v;
284 }
285
286 return $queue;
287 }
288}