Passed
Pull Request — master (#225)
by Dmitriy
02:57
created

Dumper::dumpNested()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug;
6
7
use Closure;
8
use JetBrains\PhpStorm\Pure;
9
use Yiisoft\VarDumper\ClosureExporter;
10
11
final class Dumper
12
{
13
    private array $objects = [];
14
15
    private static ?ClosureExporter $closureExporter = null;
16
    private array $excludedClasses = [];
17
18
    /**
19
     * @param mixed $variable Variable to dump.
20
     */
21
    private function __construct(private mixed $variable, array $excludedClasses = [])
22
    {
23
        $this->excludedClasses = array_flip($excludedClasses);
24
    }
25
26
    /**
27
     * @param mixed $variable Variable to dump.
28 57
     *
29
     * @return self An instance containing variable to dump.
30 57
     */
31 57
    #[Pure]
32
    public static function create(mixed $variable, array $excludedClasses = []): self
33
    {
34
        return new self($variable, $excludedClasses);
35
    }
36
37
    /**
38
     * Export variable as JSON.
39
     *
40 57
     * @param int $depth Maximum depth that the dumper should go into the variable.
41
     * @param bool $format Whatever to format exported code.
42
     *
43 57
     * @return bool|string JSON string.
44
     */
45
    public function asJson(int $depth = 50, bool $format = false): string|bool
46
    {
47
        return $this->asJsonInternal($this->variable, $format, $depth, 0, false, true);
48
    }
49
50
    /**
51
     * Export variable as JSON summary of topmost items.
52
     *
53
     * @param int $depth Maximum depth that the dumper should go into the variable.
54 55
     * @param bool $prettyPrint Whatever to format exported code.
55
     *
56 55
     * @return bool|string JSON string containing summary.
57
     */
58
    public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string|bool
59
    {
60
        $this->buildObjectsCache($this->variable, $depth);
61
62
        return $this->asJsonInternal($this->objects, $prettyPrint, $depth, 1, true, false);
63
    }
64
65
    private function buildObjectsCache($variable, int $depth, int $level = 0): void
66
    {
67 34
        if ($depth <= $level) {
68
            return;
69 34
        }
70
        if (is_object($variable)) {
71 34
            if (array_key_exists($variable::class, $this->excludedClasses) ||
0 ignored issues
show
Coding Style introduced by
The first expression of a multi-line control structure must be on the line after the opening parenthesis
Loading history...
72
                array_key_exists($objectDescription = $this->getObjectDescription($variable), $this->objects)
73
            ) {
74 57
                return;
75
            }
76 57
            $this->objects[$objectDescription] = $variable;
77
            $variable = $this->getObjectProperties($variable);
78
        }
79 57
        if (is_array($variable)) {
80 17
            foreach ($variable as $value) {
81 17
                $this->buildObjectsCache($value, $depth, $level + 1);
82 16
            }
83
        }
84 17
    }
85 17
86
    private function asJsonInternal(
87 57
        $variable,
88 48
        bool $format,
89 46
        int $depth,
90
        int $objectCollapseLevel,
91
        bool $inlineObject,
92
        bool $buildCache,
93
    ): string|bool {
94 57
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
95
96 57
        if ($format) {
97
            $options |= JSON_PRETTY_PRINT;
98 57
        }
99
        if ($buildCache) {
100
            $this->buildObjectsCache($variable, $depth);
101
        }
102 57
103
        return json_encode(
104
            $this->dumpNestedInternal($variable, $depth, 0, $objectCollapseLevel, $inlineObject),
105 57
            $options,
106
        );
107 57
    }
108 57
109
    private function getObjectProperties(object $var): array
110
    {
111 17
        if (\__PHP_Incomplete_Class::class !== $var::class && method_exists($var, '__debugInfo')) {
112
            $var = $var->__debugInfo();
113 17
        }
114
115
        return (array)$var;
116
    }
117 17
118
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel, bool $inlineObject): mixed
119
    {
120 57
        $output = $var;
121
122 57
        switch (gettype($var)) {
123
            case 'array':
124 57
                if ($depth <= $level) {
125 57
                    return 'array [...]';
126 38
                }
127
128
                $output = [];
129
                foreach ($var as $key => $value) {
130 38
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
131 38
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel, $inlineObject);
132 37
                }
133 37
134
                break;
135
            case 'object':
136 38
                $objectDescription = $this->getObjectDescription($var);
137 56
                if ($depth <= $level || array_key_exists($var::class, $this->excludedClasses)) {
138 17
                    $output = $objectDescription . ' (...)';
139 17
                    break;
140
                }
141
142
                if ($var instanceof Closure) {
143
                    $output = [$objectDescription => $this->exportClosure($var)];
144 17
                    break;
145 10
                }
146 10
147
                if ($objectCollapseLevel < $level && array_key_exists($objectDescription, $this->objects)) {
148
                    $output = 'object@' . $objectDescription;
149 8
                    break;
150 5
                }
151 5
152
                $output = [];
153
                $properties = $this->getObjectProperties($var);
154 8
                if (empty($properties)) {
155 8
                    $output[$objectDescription] = '{stateless object}';
156 8
                    break;
157 5
                }
158 5
                foreach ($properties as $key => $value) {
159
                    $keyDisplay = $this->normalizeProperty((string)$key);
160 3
                    /**
161 3
                     * @psalm-suppress InvalidArrayOffset
162
                     */
163
                    $output[$objectDescription][$keyDisplay] = $this->dumpNestedInternal(
164
                        $value,
165 3
                        $depth,
166
                        $level + 1,
167
                        $objectCollapseLevel,
168
                        $inlineObject,
169
                    );
170
                }
171
                if ($inlineObject) {
172
                    $output = $output[$objectDescription];
173 3
                }
174 45
                break;
175 44
            case 'resource':
176 1
            case 'resource (closed)':
177 1
                $output = $this->getResourceDescription($var);
178
                break;
179
        }
180 57
181
        return $output;
182
    }
183 17
184
    private function getObjectDescription(object $object): string
185
    {
186 17
        return $object::class . '#' . spl_object_id($object);
187
    }
188
189 3
    private function normalizeProperty(string $property): string
190
    {
191 3
        $property = str_replace("\0", '::', trim($property));
192
193 3
        if (str_starts_with($property, '*::')) {
194
            return 'protected $' . substr($property, 3);
195
        }
196
197 3
        if (($pos = strpos($property, '::')) !== false) {
198
            return 'private $' . substr($property, $pos + 2);
199
        }
200
201 3
        return 'public $' . $property;
202
    }
203
204 1
    private function getResourceDescription($resource): array|string
205
    {
206 1
        if (!is_resource($resource)) {
207 1
            return '{closed resource}';
208 1
        }
209
210
        $type = get_resource_type($resource);
211
        if ($type === 'stream') {
212
            return stream_get_meta_data($resource);
213 1
        }
214
        if (!empty($type)) {
215
            return sprintf('{%s resource}', $type);
216
        }
217
218
        return '{resource}';
219
    }
220
221
    /**
222
     * Exports a {@see \Closure} instance.
223
     *
224
     * @param Closure $closure Closure instance.
225 10
     *
226
     * @throws \ReflectionException
227 10
     */
228 1
    private function exportClosure(Closure $closure): string
229
    {
230
        if (self::$closureExporter === null) {
231 10
            self::$closureExporter = new ClosureExporter();
232
        }
233
234
        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

234
        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...
235
    }
236
}
237