Passed
Pull Request — master (#45)
by Evgeniy
02:06
created

VarDumper::getObjectDescription()   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 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 3
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
declare(strict_types=1);
4
5
namespace Yiisoft\VarDumper;
6
7
use __PHP_Incomplete_Class;
8
use Closure;
9
use Exception;
10
use IteratorAggregate;
11
use ReflectionException;
12
use Yiisoft\Arrays\ArrayableInterface;
13
14
use function array_keys;
15
use function count;
16
use function get_class;
17
use function gettype;
18
use function highlight_string;
19
use function method_exists;
20
use function next;
21
use function preg_replace;
22
use function range;
23
use function spl_object_id;
24
use function str_repeat;
25
use function strtr;
26
use function trim;
27
use function var_export;
28
29
/**
30
 * VarDumper provides enhanced versions of the PHP functions {@see var_dump()} and {@see var_export()}.
31
 * It can:
32
 *
33
 * - Correctly identify the recursively referenced objects in a complex object structure.
34
 * - Recursively control depth to avoid indefinite recursive display of some peculiar variables.
35
 * - Export closures and objects.
36
 * - Highlight output.
37
 * - Format output.
38
 */
39
final class VarDumper
40
{
41
    /**
42
     * @var mixed Variable to dump.
43
     */
44
    private $variable;
45
    private array $objects = [];
46
47
    private static ?ClosureExporter $closureExporter = null;
48
49
    /**
50
     * @param mixed $variable Variable to dump.
51
     */
52 76
    private function __construct($variable)
53
    {
54 76
        $this->variable = $variable;
55 76
    }
56
57
    /**
58
     * @param mixed $variable Variable to dump.
59
     *
60
     * @return static An instance containing variable to dump.
61
     */
62 76
    public static function create($variable): self
63
    {
64 76
        return new self($variable);
65
    }
66
67
    /**
68
     * Prints a variable.
69
     *
70
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
71
     * but is more robust when handling complex objects.
72
     *
73
     * @param mixed $variable Variable to be dumped.
74
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
75
     * @param bool $highlight Whether the result should be syntax-highlighted.
76
     */
77 4
    public static function dump($variable, int $depth = 10, bool $highlight = true): void
78
    {
79 4
        echo self::create($variable)->asString($depth, $highlight);
80 4
    }
81
82
    /**
83
     * Dumps a variable in terms of a string.
84
     *
85
     * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()}
86
     * but is more robust when handling complex objects.
87
     *
88
     * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10.
89
     * @param bool $highlight Whether the result should be syntax-highlighted.
90
     *
91
     * @return string The string representation of the variable.
92
     */
93 30
    public function asString(int $depth = 10, bool $highlight = false): string
94
    {
95 30
        $output = $this->dumpInternal($this->variable, true, $depth, 0);
96
97 30
        if ($highlight) {
98 1
            $result = highlight_string("<?php\n" . $output, true);
99 1
            $output = preg_replace('/&lt;\\?php<br \\/>/', '', $result, 1);
100
        }
101
102 30
        return $output;
103
    }
104
105
    /**
106
     * Exports a variable as a string containing PHP code.
107
     *
108
     * The string is a valid PHP expression that can be evaluated by PHP parser
109
     * and the evaluation result will give back the variable value.
110
     *
111
     * This method is similar to {@see var_export()}. The main difference is that
112
     * it generates more compact string representation using short array syntax.
113
     *
114
     * It also handles closures with {@see ClosureExporter} and objects
115
     * by using the PHP functions {@see serialize()} and {@see unserialize()}.
116
     *
117
     * @param bool $format Whatever to format code.
118
     *
119
     * @throws ReflectionException
120
     *
121
     * @return string A PHP code representation of the variable.
122
     */
123 48
    public function export(bool $format = true): string
124
    {
125 48
        return $this->exportInternal($this->variable, $format, 0);
126
    }
127
128
    /**
129
     * @param mixed $var Variable to be dumped.
130
     * @param bool $format Whatever to format code.
131
     * @param int $depth Maximum depth.
132
     * @param int $level Current depth.
133
     *
134
     * @throws ReflectionException
135
     *
136
     * @return string
137
     */
138 30
    private function dumpInternal($var, bool $format, int $depth, int $level): string
139
    {
140 30
        switch (gettype($var)) {
141 30
            case 'resource':
142 29
            case 'resource (closed)':
143 1
                return '{resource}';
144 29
            case 'NULL':
145 1
                return 'null';
146 28
            case 'array':
147 5
                if ($depth <= $level) {
148 1
                    return '[...]';
149
                }
150
151 4
                if (empty($var)) {
152 1
                    return '[]';
153
                }
154
155 3
                $output = '';
156 3
                $keys = array_keys($var);
157 3
                $spaces = str_repeat(' ', $level * 4);
158 3
                $output .= '[';
159
160 3
                foreach ($keys as $name) {
161 3
                    if ($format) {
162 3
                        $output .= "\n" . $spaces . '    ';
163
                    }
164 3
                    $output .= $this->exportVariable($name);
165 3
                    $output .= ' => ';
166 3
                    $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1);
167
                }
168
169 3
                return $format
170 3
                    ? $output . "\n" . $spaces . ']'
171 3
                    : $output . ']';
172 26
            case 'object':
173 16
                if ($var instanceof Closure) {
174 11
                    return $this->exportClosure($var);
175
                }
176
177 7
                if ($depth <= $level) {
178 1
                    return $this->getObjectDescription($var) . ' (...)';
179
                }
180
181 6
                $this->objects[] = $var;
182 6
                $spaces = str_repeat(' ', $level * 4);
183 6
                $output = $this->getObjectDescription($var) . "\n" . $spaces . '(';
184 6
                $objectProperties = $this->getObjectProperties($var);
185
186
                /** @psalm-var mixed $value */
187 6
                foreach ($objectProperties as $name => $value) {
188 4
                    $propertyName = strtr(trim((string) $name), "\0", '::');
189 4
                    $output .= "\n" . $spaces . "    [$propertyName] => ";
190 4
                    $output .= $this->dumpInternal($value, $format, $depth, $level + 1);
191
                }
192 6
                return $output . "\n" . $spaces . ')';
193
            default:
194 12
                return $this->exportVariable($var);
195
        }
196
    }
197
198
    /**
199
     * @param mixed $variable Variable to be exported.
200
     * @param bool $format Whatever to format code.
201
     * @param int $level Current depth.
202
     *
203
     * @throws ReflectionException
204
     *
205
     * @return string
206
     */
207 48
    private function exportInternal($variable, bool $format, int $level): string
208
    {
209 48
        switch (gettype($variable)) {
210 48
            case 'NULL':
211 2
                return 'null';
212 46
            case 'array':
213 10
                if (empty($variable)) {
214 2
                    return '[]';
215
                }
216
217 8
                $keys = array_keys($variable);
218 8
                $outputKeys = ($keys !== range(0, count($variable) - 1));
219 8
                $spaces = str_repeat(' ', $level * 4);
220 8
                $output = '[';
221
222 8
                foreach ($keys as $key) {
223 8
                    if ($format) {
224 5
                        $output .= "\n" . $spaces . '    ';
225
                    }
226 8
                    if ($outputKeys) {
227 4
                        $output .= $this->exportVariable($key);
228 4
                        $output .= ' => ';
229
                    }
230 8
                    $output .= $this->exportInternal($variable[$key], $format, $level + 1);
231 8
                    if ($format || next($keys) !== false) {
232 7
                        $output .= ',';
233
                    }
234
                }
235
236 8
                return $format
237 5
                    ? $output . "\n" . $spaces . ']'
238 8
                    : $output . ']';
239 44
            case 'object':
240 27
                if ($variable instanceof Closure) {
241 20
                    return $this->exportClosure($variable);
242
                }
243
244
                try {
245 9
                    return "unserialize({$this->exportVariable(serialize($variable))})";
246 5
                } catch (Exception $e) {
247
                    // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback.
248 5
                    if ($variable instanceof ArrayableInterface) {
249 1
                        return $this->exportInternal($variable->toArray(), $format, $level);
250
                    }
251
252 4
                    if ($variable instanceof IteratorAggregate) {
253 1
                        return $this->exportInternal(iterator_to_array($variable), $format, $level);
254
                    }
255
256 3
                    if ('__PHP_Incomplete_Class' !== get_class($variable) && method_exists($variable, '__toString')) {
257 1
                        return $this->exportVariable($variable->__toString());
258
                    }
259
260 2
                    return $this->exportVariable(self::create($variable)->asString());
261
                }
262
            default:
263 17
                return $this->exportVariable($variable);
264
        }
265
    }
266
267
    /**
268
     * Exports a {@see \Closure} instance.
269
     *
270
     * @param Closure $closure Closure instance.
271
     *
272
     * @throws ReflectionException
273
     *
274
     * @return string
275
     */
276 31
    private function exportClosure(Closure $closure): string
277
    {
278 31
        if (self::$closureExporter === null) {
279
            self::$closureExporter = new ClosureExporter();
280
        }
281
282 31
        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

282
        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...
283
    }
284
285
    /**
286
     * @param mixed $variable
287
     *
288
     * @return string
289
     */
290 39
    private function exportVariable($variable): string
291
    {
292 39
        return var_export($variable, true);
293
    }
294
295 7
    private function getObjectDescription(object $object): string
296
    {
297 7
        return get_class($object) . '#' . spl_object_id($object);
298
    }
299
300 6
    private function getObjectProperties(object $var): array
301
    {
302 6
        if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) {
303
            /** @var array $var */
304 1
            $var = $var->__debugInfo();
305
        }
306
307 6
        return (array) $var;
308
    }
309
}
310