Passed
Pull Request — master (#34)
by Evgeniy
01:51
created

VarDumper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

416
        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...
417
    }
418
419 18
    private function getObjectDescription(object $object): string
420
    {
421 18
        return get_class($object) . '#' . spl_object_id($object);
422
    }
423
424 33
    private function exportVariable($variable): string
425
    {
426 33
        return var_export($variable, true);
427
    }
428
429 25
    private function asJsonInternal($variable, bool $format, int $depth, int $objectCollapseLevel)
430
    {
431 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
432
433 25
        if ($format) {
434
            $options |= JSON_PRETTY_PRINT;
435
        }
436
437 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
438
    }
439
}
440