Passed
Push — master ( b2a7ab...efdb4a )
by Alexander
02:21
created

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

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

390
        $source = implode('', array_slice(/** @scrutinizer ignore-type */ file($fileName), $start, $end - $start));
Loading history...
391 39
        $tokens = token_get_all('<?php ' . $source);
392 39
        array_shift($tokens);
393
394 39
        $closureTokens = [];
395 39
        $pendingParenthesisCount = 0;
396 39
        $isShortClosure = false;
397 39
        $buffer = '';
398 39
        foreach ($tokens as $token) {
399 39
            if (!isset($token[0])) {
400
                continue;
401
            }
402 39
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
403 39
                $closureTokens[] = $token[1];
404 39
                if (!$isShortClosure && $token[0] === T_FN) {
405 31
                    $isShortClosure = true;
406
                }
407 39
                continue;
408
            }
409 39
            if ($closureTokens !== []) {
410 39
                $readableToken = $token[1] ?? $token;
411 39
                if ($this->isNextTokenIsPartOfNamespace($token)) {
412 20
                    $buffer .= $token[1];
413 20
                    if (!$this->isNextTokenIsPartOfNamespace(next($tokens)) && array_key_exists($buffer, $uses)) {
414 12
                        $readableToken = $uses[$buffer];
415 12
                        $buffer = '';
416
                    }
417
                }
418 39
                if ($token === '{' || $token === '[') {
419 12
                    $pendingParenthesisCount++;
420 39
                } elseif ($token === '}' || $token === ']') {
421 16
                    if ($pendingParenthesisCount === 0) {
422 4
                        break;
423
                    }
424 12
                    $pendingParenthesisCount--;
425 39
                } elseif ($token === ',' || $token === ';') {
426 35
                    if ($pendingParenthesisCount === 0) {
427 35
                        break;
428
                    }
429
                }
430 39
                $closureTokens[] = $readableToken;
431
            }
432
        }
433
434 39
        return implode('', $closureTokens);
435
    }
436
437 19
    public function asPhpString(): string
438
    {
439 19
        $this->beautify = false;
440 19
        return $this->export();
441
    }
442
443 39
    private function getUsesParser(): UseStatementParser
444
    {
445 39
        if ($this->useStatementParser === null) {
446 39
            $this->useStatementParser = new UseStatementParser();
447
        }
448
449 39
        return $this->useStatementParser;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->useStatementParser could return the type null which is incompatible with the type-hinted return Yiisoft\VarDumper\UseStatementParser. Consider adding an additional type-check to rule them out.
Loading history...
450
    }
451
452 39
    private function isNextTokenIsPartOfNamespace($token): bool
453
    {
454 39
        if (!is_array($token)) {
455 39
            return false;
456
        }
457
458 39
        return $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR;
459
    }
460
461 17
    private function getObjectDescription(object $object): string
462
    {
463 17
        return get_class($object) . '#' . spl_object_id($object);
464
    }
465
466 22
    private function exportVariable($variable): string
467
    {
468 22
        return var_export($variable, true);
469
    }
470
471 25
    private function asJsonInternal($variable, bool $prettyPrint, int $depth, int $objectCollapseLevel)
472
    {
473 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
474
475 25
        if ($prettyPrint) {
476
            $options |= JSON_PRETTY_PRINT;
477
        }
478
479 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
480
    }
481
}
482