blob: 6ecb883e83ad8760dee1630faa13a281f2a63356 [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
14use Symfony\Component\VarDumper\Caster\Caster;
15use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
16
17/**
18 * @author Nicolas Grekas <p@tchwork.com>
19 */
20class Data implements \ArrayAccess, \Countable, \IteratorAggregate
21{
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010022 private array $data;
23 private int $position = 0;
24 private int|string $key = 0;
25 private int $maxDepth = 20;
26 private int $maxItemsPerDepth = -1;
27 private int $useRefHandles = -1;
28 private array $context = [];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020029
30 /**
31 * @param array $data An array as returned by ClonerInterface::cloneVar()
32 */
33 public function __construct(array $data)
34 {
35 $this->data = $data;
36 }
37
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010038 public function getType(): ?string
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020039 {
40 $item = $this->data[$this->position][$this->key];
41
42 if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
43 $item = $item->value;
44 }
45 if (!$item instanceof Stub) {
46 return \gettype($item);
47 }
48 if (Stub::TYPE_STRING === $item->type) {
49 return 'string';
50 }
51 if (Stub::TYPE_ARRAY === $item->type) {
52 return 'array';
53 }
54 if (Stub::TYPE_OBJECT === $item->type) {
55 return $item->class;
56 }
57 if (Stub::TYPE_RESOURCE === $item->type) {
58 return $item->class.' resource';
59 }
60
61 return null;
62 }
63
64 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010065 * Returns a native representation of the original value.
66 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020067 * @param array|bool $recursive Whether values should be resolved recursively or not
68 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010069 * @return string|int|float|bool|array|Data[]|null
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020070 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010071 public function getValue(array|bool $recursive = false): string|int|float|bool|array|null
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020072 {
73 $item = $this->data[$this->position][$this->key];
74
75 if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
76 $item = $item->value;
77 }
78 if (!($item = $this->getStub($item)) instanceof Stub) {
79 return $item;
80 }
81 if (Stub::TYPE_STRING === $item->type) {
82 return $item->value;
83 }
84
85 $children = $item->position ? $this->data[$item->position] : [];
86
87 foreach ($children as $k => $v) {
88 if ($recursive && !($v = $this->getStub($v)) instanceof Stub) {
89 continue;
90 }
91 $children[$k] = clone $this;
92 $children[$k]->key = $k;
93 $children[$k]->position = $item->position;
94
95 if ($recursive) {
96 if (Stub::TYPE_REF === $v->type && ($v = $this->getStub($v->value)) instanceof Stub) {
97 $recursive = (array) $recursive;
98 if (isset($recursive[$v->position])) {
99 continue;
100 }
101 $recursive[$v->position] = true;
102 }
103 $children[$k] = $children[$k]->getValue($recursive);
104 }
105 }
106
107 return $children;
108 }
109
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100110 public function count(): int
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200111 {
112 return \count($this->getValue());
113 }
114
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100115 public function getIterator(): \Traversable
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200116 {
117 if (!\is_array($value = $this->getValue())) {
118 throw new \LogicException(sprintf('"%s" object holds non-iterable type "%s".', self::class, get_debug_type($value)));
119 }
120
121 yield from $value;
122 }
123
124 public function __get(string $key)
125 {
126 if (null !== $data = $this->seek($key)) {
127 $item = $this->getStub($data->data[$data->position][$data->key]);
128
129 return $item instanceof Stub || [] === $item ? $data : $item;
130 }
131
132 return null;
133 }
134
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100135 public function __isset(string $key): bool
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200136 {
137 return null !== $this->seek($key);
138 }
139
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100140 public function offsetExists(mixed $key): bool
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200141 {
142 return $this->__isset($key);
143 }
144
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100145 public function offsetGet(mixed $key): mixed
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200146 {
147 return $this->__get($key);
148 }
149
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100150 public function offsetSet(mixed $key, mixed $value): void
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200151 {
152 throw new \BadMethodCallException(self::class.' objects are immutable.');
153 }
154
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100155 public function offsetUnset(mixed $key): void
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200156 {
157 throw new \BadMethodCallException(self::class.' objects are immutable.');
158 }
159
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100160 public function __toString(): string
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200161 {
162 $value = $this->getValue();
163
164 if (!\is_array($value)) {
165 return (string) $value;
166 }
167
168 return sprintf('%s (count=%d)', $this->getType(), \count($value));
169 }
170
171 /**
172 * Returns a depth limited clone of $this.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200173 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100174 public function withMaxDepth(int $maxDepth): static
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200175 {
176 $data = clone $this;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100177 $data->maxDepth = $maxDepth;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200178
179 return $data;
180 }
181
182 /**
183 * Limits the number of elements per depth level.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200184 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100185 public function withMaxItemsPerDepth(int $maxItemsPerDepth): static
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200186 {
187 $data = clone $this;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100188 $data->maxItemsPerDepth = $maxItemsPerDepth;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200189
190 return $data;
191 }
192
193 /**
194 * Enables/disables objects' identifiers tracking.
195 *
196 * @param bool $useRefHandles False to hide global ref. handles
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200197 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100198 public function withRefHandles(bool $useRefHandles): static
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200199 {
200 $data = clone $this;
201 $data->useRefHandles = $useRefHandles ? -1 : 0;
202
203 return $data;
204 }
205
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100206 public function withContext(array $context): static
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200207 {
208 $data = clone $this;
209 $data->context = $context;
210
211 return $data;
212 }
213
214 /**
215 * Seeks to a specific key in nested data structures.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200216 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100217 public function seek(string|int $key): ?static
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200218 {
219 $item = $this->data[$this->position][$this->key];
220
221 if ($item instanceof Stub && Stub::TYPE_REF === $item->type && !$item->position) {
222 $item = $item->value;
223 }
224 if (!($item = $this->getStub($item)) instanceof Stub || !$item->position) {
225 return null;
226 }
227 $keys = [$key];
228
229 switch ($item->type) {
230 case Stub::TYPE_OBJECT:
231 $keys[] = Caster::PREFIX_DYNAMIC.$key;
232 $keys[] = Caster::PREFIX_PROTECTED.$key;
233 $keys[] = Caster::PREFIX_VIRTUAL.$key;
234 $keys[] = "\0$item->class\0$key";
235 // no break
236 case Stub::TYPE_ARRAY:
237 case Stub::TYPE_RESOURCE:
238 break;
239 default:
240 return null;
241 }
242
243 $data = null;
244 $children = $this->data[$item->position];
245
246 foreach ($keys as $key) {
247 if (isset($children[$key]) || \array_key_exists($key, $children)) {
248 $data = clone $this;
249 $data->key = $key;
250 $data->position = $item->position;
251 break;
252 }
253 }
254
255 return $data;
256 }
257
258 /**
259 * Dumps data with a DumperInterface dumper.
260 */
261 public function dump(DumperInterface $dumper)
262 {
263 $refs = [0];
264 $cursor = new Cursor();
265
266 if ($cursor->attr = $this->context[SourceContextProvider::class] ?? []) {
267 $cursor->attr['if_links'] = true;
268 $cursor->hashType = -1;
269 $dumper->dumpScalar($cursor, 'default', '^');
270 $cursor->attr = ['if_links' => true];
271 $dumper->dumpScalar($cursor, 'default', ' ');
272 $cursor->hashType = 0;
273 }
274
275 $this->dumpItem($dumper, $cursor, $refs, $this->data[$this->position][$this->key]);
276 }
277
278 /**
279 * Depth-first dumping of items.
280 *
281 * @param mixed $item A Stub object or the original value being dumped
282 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100283 private function dumpItem(DumperInterface $dumper, Cursor $cursor, array &$refs, mixed $item)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200284 {
285 $cursor->refIndex = 0;
286 $cursor->softRefTo = $cursor->softRefHandle = $cursor->softRefCount = 0;
287 $cursor->hardRefTo = $cursor->hardRefHandle = $cursor->hardRefCount = 0;
288 $firstSeen = true;
289
290 if (!$item instanceof Stub) {
291 $cursor->attr = [];
292 $type = \gettype($item);
293 if ($item && 'array' === $type) {
294 $item = $this->getStub($item);
295 }
296 } elseif (Stub::TYPE_REF === $item->type) {
297 if ($item->handle) {
298 if (!isset($refs[$r = $item->handle - (\PHP_INT_MAX >> 1)])) {
299 $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0];
300 } else {
301 $firstSeen = false;
302 }
303 $cursor->hardRefTo = $refs[$r];
304 $cursor->hardRefHandle = $this->useRefHandles & $item->handle;
305 $cursor->hardRefCount = 0 < $item->handle ? $item->refCount : 0;
306 }
307 $cursor->attr = $item->attr;
308 $type = $item->class ?: \gettype($item->value);
309 $item = $this->getStub($item->value);
310 }
311 if ($item instanceof Stub) {
312 if ($item->refCount) {
313 if (!isset($refs[$r = $item->handle])) {
314 $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0];
315 } else {
316 $firstSeen = false;
317 }
318 $cursor->softRefTo = $refs[$r];
319 }
320 $cursor->softRefHandle = $this->useRefHandles & $item->handle;
321 $cursor->softRefCount = $item->refCount;
322 $cursor->attr = $item->attr;
323 $cut = $item->cut;
324
325 if ($item->position && $firstSeen) {
326 $children = $this->data[$item->position];
327
328 if ($cursor->stop) {
329 if ($cut >= 0) {
330 $cut += \count($children);
331 }
332 $children = [];
333 }
334 } else {
335 $children = [];
336 }
337 switch ($item->type) {
338 case Stub::TYPE_STRING:
339 $dumper->dumpString($cursor, $item->value, Stub::STRING_BINARY === $item->class, $cut);
340 break;
341
342 case Stub::TYPE_ARRAY:
343 $item = clone $item;
344 $item->type = $item->class;
345 $item->class = $item->value;
346 // no break
347 case Stub::TYPE_OBJECT:
348 case Stub::TYPE_RESOURCE:
349 $withChildren = $children && $cursor->depth !== $this->maxDepth && $this->maxItemsPerDepth;
350 $dumper->enterHash($cursor, $item->type, $item->class, $withChildren);
351 if ($withChildren) {
352 if ($cursor->skipChildren) {
353 $withChildren = false;
354 $cut = -1;
355 } else {
356 $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, $item->type, null !== $item->class);
357 }
358 } elseif ($children && 0 <= $cut) {
359 $cut += \count($children);
360 }
361 $cursor->skipChildren = false;
362 $dumper->leaveHash($cursor, $item->type, $item->class, $withChildren, $cut);
363 break;
364
365 default:
366 throw new \RuntimeException(sprintf('Unexpected Stub type: "%s".', $item->type));
367 }
368 } elseif ('array' === $type) {
369 $dumper->enterHash($cursor, Cursor::HASH_INDEXED, 0, false);
370 $dumper->leaveHash($cursor, Cursor::HASH_INDEXED, 0, false, 0);
371 } elseif ('string' === $type) {
372 $dumper->dumpString($cursor, $item, false, 0);
373 } else {
374 $dumper->dumpScalar($cursor, $type, $item);
375 }
376 }
377
378 /**
379 * Dumps children of hash structures.
380 *
381 * @return int The final number of removed items
382 */
383 private function dumpChildren(DumperInterface $dumper, Cursor $parentCursor, array &$refs, array $children, int $hashCut, int $hashType, bool $dumpKeys): int
384 {
385 $cursor = clone $parentCursor;
386 ++$cursor->depth;
387 $cursor->hashType = $hashType;
388 $cursor->hashIndex = 0;
389 $cursor->hashLength = \count($children);
390 $cursor->hashCut = $hashCut;
391 foreach ($children as $key => $child) {
392 $cursor->hashKeyIsBinary = isset($key[0]) && !preg_match('//u', $key);
393 $cursor->hashKey = $dumpKeys ? $key : null;
394 $this->dumpItem($dumper, $cursor, $refs, $child);
395 if (++$cursor->hashIndex === $this->maxItemsPerDepth || $cursor->stop) {
396 $parentCursor->stop = true;
397
398 return $hashCut >= 0 ? $hashCut + $cursor->hashLength - $cursor->hashIndex : $hashCut;
399 }
400 }
401
402 return $hashCut;
403 }
404
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100405 private function getStub(mixed $item)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200406 {
407 if (!$item || !\is_array($item)) {
408 return $item;
409 }
410
411 $stub = new Stub();
412 $stub->type = Stub::TYPE_ARRAY;
413 foreach ($item as $stub->class => $stub->position) {
414 }
415 if (isset($item[0])) {
416 $stub->cut = $item[0];
417 }
418 $stub->value = $stub->cut + ($stub->position ? \count($this->data[$stub->position]) : 0);
419
420 return $stub;
421 }
422}