1<?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        $indexedArrays = [];       // Map of queue indexes that hold numerically indexed arrays
32        $hardRefs = [];            // Map of original zval ids to stub objects
33        $objRefs = [];             // Map of original object handles to their stub object counterpart
34        $objects = [];             // Keep a ref to objects to ensure their handle cannot be reused while cloning
35        $resRefs = [];             // Map of original resource handles to their stub object counterpart
36        $values = [];              // Map of stub objects' ids to original values
37        $maxItems = $this->maxItems;
38        $maxString = $this->maxString;
39        $minDepth = $this->minDepth;
40        $currentDepth = 0;              // Current tree depth
41        $currentDepthFinalIndex = 0;    // Final $queue index for current tree depth
42        $minimumDepthReached = 0 === $minDepth; // Becomes true when minimum tree depth has been reached
43        $cookie = (object) [];          // Unique object used to detect hard references
44        $a = null;                      // Array cast for nested structures
45        $stub = null;                   // Stub capturing the main properties of an original item value
46                                        // or null if the original value is used directly
47
48        if (!$gid = self::$gid) {
49            $gid = self::$gid = md5(random_bytes(6)); // Unique string used to detect the special $GLOBALS variable
50        }
51        $arrayStub = new Stub();
52        $arrayStub->type = Stub::TYPE_ARRAY;
53        $fromObjCast = false;
54
55        for ($i = 0; $i < $len; ++$i) {
56            // Detect when we move on to the next tree depth
57            if ($i > $currentDepthFinalIndex) {
58                ++$currentDepth;
59                $currentDepthFinalIndex = $len - 1;
60                if ($currentDepth >= $minDepth) {
61                    $minimumDepthReached = true;
62                }
63            }
64
65            $refs = $vals = $queue[$i];
66            if (\PHP_VERSION_ID < 70200 && empty($indexedArrays[$i])) {
67                // see https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts
68                foreach ($vals as $k => $v) {
69                    if (\is_int($k)) {
70                        continue;
71                    }
72                    foreach ([$k => true] as $gk => $gv) {
73                    }
74                    if ($gk !== $k) {
75                        $fromObjCast = true;
76                        $refs = $vals = array_values($queue[$i]);
77                        break;
78                    }
79                }
80            }
81            foreach ($vals as $k => $v) {
82                // $v is the original value or a stub object in case of hard references
83
84                if (\PHP_VERSION_ID >= 70400) {
85                    $zvalIsRef = null !== \ReflectionReference::fromArrayElement($vals, $k);
86                } else {
87                    $refs[$k] = $cookie;
88                    $zvalIsRef = $vals[$k] === $cookie;
89                }
90
91                if ($zvalIsRef) {
92                    $vals[$k] = &$stub;         // Break hard references to make $queue completely
93                    unset($stub);               // independent from the original structure
94                    if ($v instanceof Stub && isset($hardRefs[spl_object_id($v)])) {
95                        $vals[$k] = $refs[$k] = $v;
96                        if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) {
97                            ++$v->value->refCount;
98                        }
99                        ++$v->refCount;
100                        continue;
101                    }
102                    $refs[$k] = $vals[$k] = new Stub();
103                    $refs[$k]->value = $v;
104                    $h = spl_object_id($refs[$k]);
105                    $hardRefs[$h] = &$refs[$k];
106                    $values[$h] = $v;
107                    $vals[$k]->handle = ++$refsCounter;
108                }
109                // Create $stub when the original value $v can not be used directly
110                // If $v is a nested structure, put that structure in array $a
111                switch (true) {
112                    case null === $v:
113                    case \is_bool($v):
114                    case \is_int($v):
115                    case \is_float($v):
116                        continue 2;
117
118                    case \is_string($v):
119                        if ('' === $v) {
120                            continue 2;
121                        }
122                        if (!preg_match('//u', $v)) {
123                            $stub = new Stub();
124                            $stub->type = Stub::TYPE_STRING;
125                            $stub->class = Stub::STRING_BINARY;
126                            if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) {
127                                $stub->cut = $cut;
128                                $stub->value = substr($v, 0, -$cut);
129                            } else {
130                                $stub->value = $v;
131                            }
132                        } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) {
133                            $stub = new Stub();
134                            $stub->type = Stub::TYPE_STRING;
135                            $stub->class = Stub::STRING_UTF8;
136                            $stub->cut = $cut;
137                            $stub->value = mb_substr($v, 0, $maxString, 'UTF-8');
138                        } else {
139                            continue 2;
140                        }
141                        $a = null;
142                        break;
143
144                    case \is_array($v):
145                        if (!$v) {
146                            continue 2;
147                        }
148                        $stub = $arrayStub;
149                        $stub->class = Stub::ARRAY_INDEXED;
150
151                        $j = -1;
152                        foreach ($v as $gk => $gv) {
153                            if ($gk !== ++$j) {
154                                $stub->class = Stub::ARRAY_ASSOC;
155                                break;
156                            }
157                        }
158                        $a = $v;
159
160                        if (Stub::ARRAY_ASSOC === $stub->class) {
161                            // Copies of $GLOBALS have very strange behavior,
162                            // let's detect them with some black magic
163                            $a[$gid] = true;
164
165                            // Happens with copies of $GLOBALS
166                            if (isset($v[$gid])) {
167                                unset($v[$gid]);
168                                $a = [];
169                                foreach ($v as $gk => &$gv) {
170                                    $a[$gk] = &$gv;
171                                }
172                                unset($gv);
173                            } else {
174                                $a = $v;
175                            }
176                        } elseif (\PHP_VERSION_ID < 70200) {
177                            $indexedArrays[$len] = true;
178                        }
179                        break;
180
181                    case \is_object($v):
182                    case $v instanceof \__PHP_Incomplete_Class:
183                        if (empty($objRefs[$h = spl_object_id($v)])) {
184                            $stub = new Stub();
185                            $stub->type = Stub::TYPE_OBJECT;
186                            $stub->class = \get_class($v);
187                            $stub->value = $v;
188                            $stub->handle = $h;
189                            $a = $this->castObject($stub, 0 < $i);
190                            if ($v !== $stub->value) {
191                                if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) {
192                                    break;
193                                }
194                                $stub->handle = $h = spl_object_id($stub->value);
195                            }
196                            $stub->value = null;
197                            if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) {
198                                $stub->cut = \count($a);
199                                $a = null;
200                            }
201                        }
202                        if (empty($objRefs[$h])) {
203                            $objRefs[$h] = $stub;
204                            $objects[] = $v;
205                        } else {
206                            $stub = $objRefs[$h];
207                            ++$stub->refCount;
208                            $a = null;
209                        }
210                        break;
211
212                    default: // resource
213                        if (empty($resRefs[$h = (int) $v])) {
214                            $stub = new Stub();
215                            $stub->type = Stub::TYPE_RESOURCE;
216                            if ('Unknown' === $stub->class = @get_resource_type($v)) {
217                                $stub->class = 'Closed';
218                            }
219                            $stub->value = $v;
220                            $stub->handle = $h;
221                            $a = $this->castResource($stub, 0 < $i);
222                            $stub->value = null;
223                            if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) {
224                                $stub->cut = \count($a);
225                                $a = null;
226                            }
227                        }
228                        if (empty($resRefs[$h])) {
229                            $resRefs[$h] = $stub;
230                        } else {
231                            $stub = $resRefs[$h];
232                            ++$stub->refCount;
233                            $a = null;
234                        }
235                        break;
236                }
237
238                if ($a) {
239                    if (!$minimumDepthReached || 0 > $maxItems) {
240                        $queue[$len] = $a;
241                        $stub->position = $len++;
242                    } elseif ($pos < $maxItems) {
243                        if ($maxItems < $pos += \count($a)) {
244                            $a = \array_slice($a, 0, $maxItems - $pos);
245                            if ($stub->cut >= 0) {
246                                $stub->cut += $pos - $maxItems;
247                            }
248                        }
249                        $queue[$len] = $a;
250                        $stub->position = $len++;
251                    } elseif ($stub->cut >= 0) {
252                        $stub->cut += \count($a);
253                        $stub->position = 0;
254                    }
255                }
256
257                if ($arrayStub === $stub) {
258                    if ($arrayStub->cut) {
259                        $stub = [$arrayStub->cut, $arrayStub->class => $arrayStub->position];
260                        $arrayStub->cut = 0;
261                    } elseif (isset(self::$arrayCache[$arrayStub->class][$arrayStub->position])) {
262                        $stub = self::$arrayCache[$arrayStub->class][$arrayStub->position];
263                    } else {
264                        self::$arrayCache[$arrayStub->class][$arrayStub->position] = $stub = [$arrayStub->class => $arrayStub->position];
265                    }
266                }
267
268                if ($zvalIsRef) {
269                    $refs[$k]->value = $stub;
270                } else {
271                    $vals[$k] = $stub;
272                }
273            }
274
275            if ($fromObjCast) {
276                $fromObjCast = false;
277                $refs = $vals;
278                $vals = [];
279                $j = -1;
280                foreach ($queue[$i] as $k => $v) {
281                    foreach ([$k => true] as $gk => $gv) {
282                    }
283                    if ($gk !== $k) {
284                        $vals = (object) $vals;
285                        $vals->{$k} = $refs[++$j];
286                        $vals = (array) $vals;
287                    } else {
288                        $vals[$k] = $refs[++$j];
289                    }
290                }
291            }
292
293            $queue[$i] = $vals;
294        }
295
296        foreach ($values as $h => $v) {
297            $hardRefs[$h] = $v;
298        }
299
300        return $queue;
301    }
302}
303