Test Failed
Pull Request — master (#37)
by Alexander
02:25
created

VarDumper::asJsonObjectsMap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\VarDumper;
6
7
use Closure;
8
use Exception;
9
use IteratorAggregate;
10
use Yiisoft\Arrays\ArrayableInterface;
11
use function get_class;
12
use function in_array;
13
use function is_array;
14
use function is_object;
15
16
/**
17
 * VarDumper provides enhanced versions of the PHP functions {@see var_dump()} and {@see var_export()}.
18
 *
19
 * It can:
20
 *
21
 * - Correctly identify the recursively referenced objects in a complex object structure.
22
 * - Recursively control depth to avoid indefinite recursive display of some peculiar variables.
23
 * - Export closures and objects.
24
 * - Highlight output.
25
 * - Format output.
26
 */
27
final class VarDumper
28
{
29
    /**
30
     * @var mixed Variable to dump.
31
     */
32
    private $variable;
33
    private array $objects = [];
34
35
    private static ?ClosureExporter $closureExporter = null;
36
37
    /**
38
     * @param mixed $variable Variable to dump.
39 93
     */
40
    private function __construct($variable)
41 93
    {
42 93
        $this->variable = $variable;
43
    }
44
45
    /**
46
     * @param mixed $variable Variable to dump.
47
     *
48
     * @return static An instance containing variable to dump.
49 93
     */
50
    public static function create($variable): self
51 93
    {
52
        return new self($variable);
53
    }
54
55
    /**
56
     * Prints a variable.
57
     *
58
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
59
     * but is more robust when handling complex objects.
60
     *
61
     * @param mixed $variable Variable to be dumped.
62
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
63
     * @param bool $highlight Whether the result should be syntax-highlighted.
64
     */
65
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
66
    {
67
        echo self::create($variable)->asString($depth, $highlight);
68
    }
69
70
    /**
71
     * Dumps a variable in terms of a string.
72
     *
73
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
74
     * but is more robust when handling complex objects.
75
     *
76
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
77
     * @param bool $highlight Whether the result should be syntax-highlighted.
78
     *
79
     * @return string The string representation of the variable.
80 25
     */
81
    public function asString(int $depth = 10, bool $highlight = false): string
82 25
    {
83 25
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
84
        if ($highlight) {
85
            $result = highlight_string("<?php\n" . $output, true);
86
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
87
        }
88 25
89
        return $output;
90
    }
91 25
92
    private function dumpNested($variable, int $depth, int $objectCollapseLevel)
93 25
    {
94 25
        $this->buildObjectsCache($variable, $depth);
95
        return $this->dumpNestedInternal($variable, $depth, 0, $objectCollapseLevel);
96
    }
97
98
    /**
99
     * Export variable information as array.
100
     *
101
     * @param int $depth Maximum depth that the dumper should go into the variable.
102
     *
103
     * @return array Variable information as array.
104
     */
105 23
    public function asArray(int $depth = 50): array
106
    {
107 23
        return $this->asArrayInternal($this->variable, $depth, 0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->asArrayInt...s->variable, $depth, 0) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
108
    }
109
110
    /**
111
     * Export variable topmost items summary as array.
112
     *
113
     * @param int $depth Maximum depth that the dumper should go into the variable.
114
     *
115
     * @return array Array containing topmost items summary.
116
     */
117
    public function asArraySummary(int $depth = 50): array
118 2
    {
119
        $this->buildObjectsCache($this->variable, $depth);
120 2
121
        return $this->asArrayInternal($this->objects, $depth, 1);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->asArrayInt...is->objects, $depth, 1) could return the type string which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
122 2
    }
123
124
    /**
125
     * Exports a variable as a string containing PHP code.
126
     *
127
     * The string is a valid PHP expression that can be evaluated by PHP parser
128
     * and the evaluation result will give back the variable value.
129
     *
130
     * This method is similar to {@see var_export()}. The main difference is that
131
     * it generates more compact string representation using short array syntax.
132
     *
133
     * It also handles closures with {@see ClosureExporter} and objects
134
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
135
     *
136
     * @param bool $format Whatever to format code.
137
     *
138
     * @throws \ReflectionException
139
     *
140
     * @return string A PHP code representation of the variable.
141
     */
142
    public function export(bool $format = true): string
143 44
    {
144
        return $this->exportInternal($this->variable, $format, 0);
145 44
    }
146
147
    private function buildObjectsCache($variable, int $depth, int $level = 0): void
148 25
    {
149
        if ($depth <= $level) {
150 25
            return;
151
        }
152
        if (is_object($variable)) {
153 25
            if (in_array($variable, $this->objects, true)) {
154 13
                return;
155 12
            }
156
            $this->objects[] = $variable;
157 13
            $variable = $this->getObjectProperties($variable);
158 13
        }
159
        if (is_array($variable)) {
160 25
            foreach ($variable as $value) {
161 16
                $this->buildObjectsCache($value, $depth, $level + 1);
162 14
            }
163
        }
164
    }
165 25
166
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
167 25
    {
168
        $output = $var;
169 25
170
        switch (gettype($var)) {
171 25
            case 'array':
172 25
                if ($depth <= $level) {
173 6
                    return 'array [...]';
174
                }
175
176
                $output = [];
177 6
                foreach ($var as $key => $value) {
178 6
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
179 5
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
180 5
                }
181
182
                break;
183 6
            case 'object':
184 24
                $objectDescription = $this->getObjectDescription($var);
185 13
                if ($depth <= $level) {
186 13
                    $output = $objectDescription . ' (...)';
187
                    break;
188
                }
189
190
                if ($var instanceof Closure) {
191 13
                    $output = [$objectDescription => $this->exportClosure($var)];
192 10
                    break;
193 10
                }
194
195
                if ($objectCollapseLevel < $level && in_array($var, $this->objects, true)) {
196 4
                    $output = 'object@' . $objectDescription;
197 1
                    break;
198 1
                }
199
200
                $output = [];
201 4
                $properties = $this->getObjectProperties($var);
202 4
                if (empty($properties)) {
203 4
                    $output[$objectDescription] = '{stateless object}';
204 1
                    break;
205 1
                }
206
                foreach ($properties as $key => $value) {
207 3
                    $keyDisplay = $this->normalizeProperty((string) $key);
208 3
                    /**
209
                     * @psalm-suppress InvalidArrayOffset
210
                     */
211
                    $output[$objectDescription][$keyDisplay] = $this->dumpNestedInternal(
212 3
                        $value,
213 3
                        $depth,
214 3
                        $level + 1,
215 3
                        $objectCollapseLevel
216 3
                    );
217
                }
218
219
                break;
220 3
            case 'resource':
221 13
            case 'resource (closed)':
222 12
                $output = $this->getResourceDescription($var);
223 1
                break;
224 1
        }
225
226
        return $output;
227 25
    }
228
229
    private function normalizeProperty(string $property): string
230 3
    {
231
        $property = str_replace("\0", '::', trim($property));
232 3
233
        if (strpos($property, '*::') === 0) {
234 3
            return 'protected $' . substr($property, 3);
235
        }
236
237
        if (($pos = strpos($property, '::')) !== false) {
238 3
            return 'private $' . substr($property, $pos + 2);
239
        }
240
241
        return 'public $' . $property;
242 3
    }
243
244
    /**
245
     * @param mixed $var Variable to be dumped.
246
     * @param bool $format Whatever to format code.
247
     * @param int $depth Maximum depth.
248
     * @param int $level Current depth.
249
     *
250
     * @throws \ReflectionException
251
     *
252
     * @return string
253
     */
254
    private function dumpInternal($var, bool $format, int $depth, int $level): string
255 25
    {
256
        $type = gettype($var);
257 25
        switch ($type) {
258 25
            case 'resource':
259 25
            case 'resource (closed)':
260 24
                return '{resource}';
261 1
            case 'NULL':
262 24
                return 'null';
263 1
            case 'array':
264 23
                if ($depth <= $level) {
265 4
                    return '[...]';
266
                }
267
268
                if (empty($var)) {
269 4
                    return '[]';
270 1
                }
271
272
                $output = '';
273 3
                $keys = array_keys($var);
274 3
                $spaces = str_repeat(' ', $level * 4);
275 3
                $output .= '[';
276 3
                foreach ($keys as $name) {
277 3
                    if ($format) {
278 3
                        $output .= "\n" . $spaces . '    ';
279 3
                    }
280
                    $output .= $this->exportVariable($name);
281 3
                    $output .= ' => ';
282 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
283 3
                }
284
285
                return $format
286 3
                    ? $output . "\n" . $spaces . ']'
287 3
                    : $output . ']';
288 3
            case 'object':
289 22
                if ($var instanceof Closure) {
290 14
                    return $this->exportClosure($var);
291 11
                }
292
                if ($depth <= $level) {
293 5
                    return $this->getObjectDescription($var) . ' (...)';
294
                }
295
296
                $this->objects[] = $var;
297 5
                $spaces = str_repeat(' ', $level * 4);
298 5
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
299 5
                $objectProperties = $this->getObjectProperties($var);
300 5
                foreach ($objectProperties as $name => $value) {
301 5
                    $propertyName = strtr(trim((string) $name), "\0", '::');
302 4
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
303 4
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
304 4
                }
305
                return $output . "\n" . $spaces . ')';
306 5
            default:
307
                return $this->exportVariable($var);
308 10
        }
309
    }
310
311
    private function getObjectProperties($var): array
312 18
    {
313
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
314 18
            $var = $var->__debugInfo();
315 1
        }
316
317
        return (array)$var;
318 18
    }
319
320
    private function getResourceDescription($resource)
321 1
    {
322
        $type = get_resource_type($resource);
323 1
        if ($type === 'stream') {
324 1
            $desc = stream_get_meta_data($resource);
325 1
        } else {
326
            $desc = '{resource}';
327
        }
328
329
        return $desc;
330 1
    }
331
332
    /**
333
     * @param mixed $variable Variable to be exported.
334
     * @param bool $format Whatever to format code.
335
     * @param int $level Current depth.
336
     *
337
     * @throws \ReflectionException
338
     *
339
     * @return string
340
     */
341
    private function exportInternal($variable, bool $format, int $level): string
342 44
    {
343
        switch (gettype($variable)) {
344 44
            case 'NULL':
345 44
                return 'null';
346 2
            case 'array':
347 42
                if (empty($variable)) {
348 8
                    return '[]';
349 2
                }
350
351
                $keys = array_keys($variable);
352 6
                $outputKeys = ($keys !== range(0, count($variable) - 1));
353 6
                $spaces = str_repeat(' ', $level * 4);
354 6
                $output = '[';
355 6
                foreach ($keys as $key) {
356 6
                    if ($format) {
357 6
                        $output .= "\n" . $spaces . '    ';
358 3
                    }
359
                    if ($outputKeys) {
360 6
                        $output .= $this->exportVariable($key);
361 2
                        $output .= ' => ';
362 2
                    }
363
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
364 6
                    if ($format || next($keys) !== false) {
365 6
                        $output .= ',';
366 5
                    }
367
                }
368
                return $format
369 6
                    ? $output . "\n" . $spaces . ']'
370 3
                    : $output . ']';
371 6
            case 'object':
372 40
                if ($variable instanceof Closure) {
373 23
                    return $this->exportClosure($variable);
374 18
                }
375
376
                try {
377
                    return 'unserialize(' . $this->exportVariable(serialize($variable)) . ')';
378 5
                } catch (Exception $e) {
379 1
                    // Serialize may fail, for example: if object contains a `\Closure` instance
380
                    // so we use a fallback.
381
                    if ($variable instanceof ArrayableInterface) {
382 1
                        return $this->exportInternal($variable->toArray(), $format, $level);
383
                    }
384
385
                    if ($variable instanceof IteratorAggregate) {
386 1
                        return $this->exportInternal(iterator_to_array($variable), $format, $level);
387
                    }
388
389
                    if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
390 1
                        return $this->exportVariable($variable->__toString());
391
                    }
392
393
                    return $this->exportVariable(self::create($variable)->asString());
394 1
                }
395
            default:
396
                return $this->exportVariable($variable);
397 17
        }
398
    }
399
400
    /**
401
     * Exports a {@see \Closure} instance.
402
     *
403
     * @param Closure $closure Closure instance.
404
     *
405
     * @throws \ReflectionException
406
     *
407
     * @return string
408
     */
409
    private function exportClosure(Closure $closure): string
410 39
    {
411
        if (self::$closureExporter === null) {
412 39
            self::$closureExporter = new ClosureExporter();
413 1
        }
414
415
        return self::$closureExporter->export($closure);
0 ignored issues
show
Bug introduced by
The method export() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

415
        return self::$closureExporter->/** @scrutinizer ignore-call */ export($closure);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
416 39
    }
417
418
    private function getObjectDescription(object $object): string
419 18
    {
420
        return get_class($object) . '#' . spl_object_id($object);
421 18
    }
422
423
    private function exportVariable($variable): string
424 33
    {
425
        return var_export($variable, true);
426 33
    }
427
428
    private function asArrayInternal($variable, int $depth, int $objectCollapseLevel)
429 25
    {
430
        return $this->dumpNested($variable, $depth, $objectCollapseLevel);
431 25
    }
432
}
433