Passed
Push — master ( 55f627...47fe5a )
by Dmitriy
02:36 queued 02:02
created

VarDumper::exportClosure()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

392
        return $this->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...
393
    }
394
395 19
    public function asPhpString(): string
396
    {
397 19
        $this->beautify = false;
398 19
        return $this->export();
399
    }
400
401 17
    private function getObjectDescription(object $object): string
402
    {
403 17
        return get_class($object) . '#' . spl_object_id($object);
404
    }
405
406 22
    private function exportVariable($variable): string
407
    {
408 22
        return var_export($variable, true);
409
    }
410
411 25
    private function asJsonInternal($variable, bool $prettyPrint, int $depth, int $objectCollapseLevel)
412
    {
413 25
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
414
415 25
        if ($prettyPrint) {
416
            $options |= JSON_PRETTY_PRINT;
417
        }
418
419 25
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
420
    }
421
}
422