Test Failed
Pull Request — master (#34)
by Alexander
03:13 queued 33s
created

VarDumper::asJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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