Test Failed
Pull Request — master (#34)
by Alexander
03:07
created

VarDumper::asPhpString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 4
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()}, {@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
 * - Pretty-print 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 $prettyPrint Whatever to format exported code.
102
     *
103
     * @return string JSON string.
104
     */
105
    public function asJson(int $depth = 50, bool $prettyPrint = false): string
106
    {
107
        return $this->asJsonInternal($this->variable, $prettyPrint, $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
     * @return string A PHP code representation of the variable.
139 6
     * @throws \ReflectionException
140
     */
141
    public function export(bool $format = true): string
142
    {
143 6
        return $this->exportInternal($this->variable, $format, 0);
144 6
    }
145 5
146 5
    private function buildObjectsCache($variable, int $depth, int $level = 0): void
147
    {
148
        if ($depth <= $level) {
149 6
            return;
150 24
        }
151 13
        if (is_object($variable)) {
152 13
            if (in_array($variable, $this->objects, true)) {
153
                return;
154
            }
155
            $this->objects[] = $variable;
156
            $variable = $this->getObjectProperties($variable);
157 13
        }
158 10
        if (is_array($variable)) {
159 10
            foreach ($variable as $value) {
160
                $this->buildObjectsCache($value, $depth, $level + 1);
161
            }
162 4
        }
163 1
    }
164 1
165
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0)
166
    {
167 4
        $output = $var;
168 4
169 4
        switch (gettype($var)) {
170 1
            case 'array':
171 1
                if ($depth <= $level) {
172
                    return 'array [...]';
173 3
                }
174 3
175
                $output = [];
176
                foreach ($var as $key => $value) {
177
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
178 3
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
179 3
                }
180 3
181 3
                break;
182 3
            case 'object':
183
                $objectDescription = $this->getObjectDescription($var);
184
                if ($depth <= $level) {
185
                    $output = $objectDescription . ' (...)';
186 3
                    break;
187 13
                }
188 12
189 1
                if ($var instanceof Closure) {
190 1
                    $output = [$objectDescription => $this->exportClosure($var)];
191
                    break;
192
                }
193 25
194
                if ($objectCollapseLevel < $level && in_array($var, $this->objects, true)) {
195
                    $output = 'object@' . $objectDescription;
196 3
                    break;
197
                }
198 3
199
                $output = [];
200 3
                $properties = $this->getObjectProperties($var);
201
                if (empty($properties)) {
202
                    $output[$objectDescription] = '{stateless object}';
203
                    break;
204 3
                }
205
                foreach ($properties as $key => $value) {
206
                    $keyDisplay = $this->normalizeProperty((string) $key);
207
                    /**
208 3
                     * @psalm-suppress InvalidArrayOffset
209
                     */
210
                    $output[$objectDescription][$keyDisplay] = $this->dumpNestedInternal(
211
                        $value,
212
                        $depth,
213
                        $level + 1,
214
                        $objectCollapseLevel
215
                    );
216
                }
217
218
                break;
219
            case 'resource':
220 25
            case 'resource (closed)':
221
                $output = $this->getResourceDescription($var);
222 25
                break;
223 25
        }
224 25
225 24
        return $output;
226 1
    }
227 24
228 1
    private function normalizeProperty(string $property): string
229 23
    {
230 4
        $property = str_replace("\0", '::', trim($property));
231
232
        if (strpos($property, '*::') === 0) {
233
            return 'protected $' . substr($property, 3);
234 4
        }
235 1
236
        if (($pos = strpos($property, '::')) !== false) {
237
            return 'private $' . substr($property, $pos + 2);
238 3
        }
239 3
240 3
        return 'public $' . $property;
241 3
    }
242 3
243 3
    /**
244 3
     * @param mixed $var Variable to be dumped.
245
     * @param bool $format Whatever to format code.
246 3
     * @param int $depth Maximum depth.
247 3
     * @param int $level Current depth.
248 3
     *
249
     * @return string
250
     * @throws \ReflectionException
251 3
     */
252 3
    private function dumpInternal($var, bool $format, int $depth, int $level): string
253 3
    {
254 22
        $type = gettype($var);
255 14
        switch ($type) {
256 11
            case 'resource':
257
            case 'resource (closed)':
258 5
                return '{resource}';
259
            case 'NULL':
260
                return 'null';
261
            case 'array':
262 5
                if ($depth <= $level) {
263 5
                    return '[...]';
264 5
                }
265 5
266 5
                if (empty($var)) {
267 4
                    return '[]';
268 4
                }
269 4
270
                $output = '';
271 5
                $keys = array_keys($var);
272
                $spaces = str_repeat(' ', $level * 4);
273 10
                $output .= '[';
274
                foreach ($keys as $name) {
275
                    if ($format) {
276
                        $output .= "\n" . $spaces . '    ';
277 18
                    }
278
                    $output .= $this->exportVariable($name);
279 18
                    $output .= ' => ';
280 1
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
281
                }
282
283 18
                return $format
284
                    ? $output . "\n" . $spaces . ']'
285
                    : $output . ']';
286 1
            case 'object':
287
                if ($var instanceof Closure) {
288 1
                    return $this->exportClosure($var);
289 1
                }
290 1
                if ($depth <= $level) {
291
                    return $this->getObjectDescription($var) . ' (...)';
292
                }
293
294
                $this->objects[] = $var;
295 1
                $spaces = str_repeat(' ', $level * 4);
296
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
297
                $objectProperties = $this->getObjectProperties($var);
298
                foreach ($objectProperties as $name => $value) {
299
                    $propertyName = strtr(trim((string) $name), "\0", '::');
300
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
301
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
302
                }
303
                return $output . "\n" . $spaces . ')';
304
            default:
305
                return $this->exportVariable($var);
306 44
        }
307
    }
308 44
309 44
    private function getObjectProperties($var): array
310 2
    {
311 42
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
312 8
            $var = $var->__debugInfo();
313 2
        }
314
315
        return (array)$var;
316 6
    }
317 6
318 6
    private function getResourceDescription($resource)
319 6
    {
320 6
        $type = get_resource_type($resource);
321 6
        if ($type === 'stream') {
322 3
            $desc = stream_get_meta_data($resource);
323
        } else {
324 6
            $desc = '{resource}';
325 2
        }
326 2
327
        return $desc;
328 6
    }
329 6
330 5
    /**
331
     * @param mixed $variable Variable to be exported.
332
     * @param bool $format Whatever to format code.
333 6
     * @param int $level Current depth.
334 3
     *
335 6
     * @return string
336 40
     * @throws \ReflectionException
337 23
     */
338 18
    private function exportInternal($variable, bool $format, int $level): string
339
    {
340
        switch (gettype($variable)) {
341
            case 'NULL':
342 5
                return 'null';
343 1
            case 'array':
344
                if (empty($variable)) {
345
                    return '[]';
346 1
                }
347
348
                $keys = array_keys($variable);
349
                $outputKeys = ($keys !== range(0, count($variable) - 1));
350 1
                $spaces = str_repeat(' ', $level * 4);
351
                $output = '[';
352
                foreach ($keys as $key) {
353
                    if ($format) {
354 1
                        $output .= "\n" . $spaces . '    ';
355
                    }
356
                    if ($outputKeys) {
357
                        $output .= $this->exportVariable($key);
358 1
                        $output .= ' => ';
359
                    }
360
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
361 17
                    if ($format || next($keys) !== false) {
362
                        $output .= ',';
363
                    }
364
                }
365
                return $format
366
                    ? $output . "\n" . $spaces . ']'
367
                    : $output . ']';
368
            case 'object':
369
                if ($variable instanceof Closure) {
370
                    return $this->exportClosure($variable);
371
                }
372
373
                try {
374 39
                    return 'unserialize(' . $this->exportVariable(serialize($variable)) . ')';
375
                } catch (Exception $e) {
376 39
                    // Serialize may fail, for example: if object contains a `\Closure` instance
377 1
                    // so we use a fallback.
378
                    if ($variable instanceof ArrayableInterface) {
379
                        return $this->exportInternal($variable->toArray(), $format, $level);
380 39
                    }
381
382
                    if ($variable instanceof IteratorAggregate) {
383 19
                        return $this->exportInternal(iterator_to_array($variable), $format, $level);
384
                    }
385 19
386 19
                    if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
387
                        return $this->exportVariable($variable->__toString());
388
                    }
389 18
390
                    return $this->exportVariable(self::create($variable)->asString());
391 18
                }
392
            default:
393
                return $this->exportVariable($variable);
394 33
        }
395
    }
396 33
397
    /**
398
     * Exports a {@see \Closure} instance.
399 25
     *
400
     * @param Closure $closure Closure instance.
401 25
     *
402
     * @throws \ReflectionException
403 25
     *
404
     * @return string
405
     */
406
    private function exportClosure(Closure $closure): string
407 25
    {
408
        if (self::$closureExporter === null) {
409
            self::$closureExporter = new ClosureExporter();
410
        }
411
412
        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

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