Passed
Push — master ( a4ffc4...615af5 )
by Alexander
03:23
created

Dumper   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Test Coverage

Coverage 90%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 88
c 1
b 0
f 0
dl 0
loc 221
ccs 81
cts 90
cp 0.9
rs 9.28
wmc 39

13 Methods

Rating   Name   Duplication   Size   Complexity  
A asJsonObjectsMap() 0 5 1
A __construct() 0 4 1
A normalizeProperty() 0 13 3
A getObjectProperties() 0 7 3
A create() 0 4 1
B buildObjectsCache() 0 16 7
A getObjectDescription() 0 4 1
A asJsonInternal() 0 9 2
A exportClosure() 0 7 2
A asJson() 0 3 1
A getResourceDescription() 0 10 2
A dumpNested() 0 4 1
C dumpNestedInternal() 0 61 14
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
    /**
14
     * @var mixed Variable to dump.
15
     */
16
    private mixed $variable;
17
18
    private array $excludedClasses;
19
20
    private array $objects = [];
21
22
    private static ?ClosureExporter $closureExporter = null;
23
24
    /**
25
     * @param mixed $variable Variable to dump.
26
     * @param array $excludedClasses
27
     */
28 57
    private function __construct(mixed $variable, array $excludedClasses = [])
29
    {
30 57
        $this->variable = $variable;
31 57
        $this->excludedClasses = $excludedClasses;
32
    }
33
34
    /**
35
     * @param mixed $variable Variable to dump.
36
     * @param array $excludedClasses
37
     *
38
     * @return self An instance containing variable to dump.
39
     */
40 57
    #[Pure]
41
    public static function create($variable, array $excludedClasses = []): self
42
    {
43 57
        return new self($variable, $excludedClasses);
44
    }
45
46
    /**
47
     * Export variable as JSON.
48
     *
49
     * @param int $depth Maximum depth that the dumper should go into the variable.
50
     * @param bool $format Whatever to format exported code.
51
     *
52
     * @return bool|string JSON string.
53
     */
54 55
    public function asJson(int $depth = 50, bool $format = false): string|bool
55
    {
56 55
        return $this->asJsonInternal($this->variable, $format, $depth, 0);
57
    }
58
59
    /**
60
     * Export variable as JSON summary of topmost items.
61
     *
62
     * @param int $depth Maximum depth that the dumper should go into the variable.
63
     * @param bool $prettyPrint Whatever to format exported code.
64
     *
65
     * @return bool|string JSON string containing summary.
66
     */
67 34
    public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string|bool
68
    {
69 34
        $this->buildObjectsCache($this->variable, $depth);
70
71 34
        return $this->asJsonInternal($this->objects, $prettyPrint, $depth, 1);
72
    }
73
74 57
    private function buildObjectsCache($variable, int $depth, int $level = 0): void
75
    {
76 57
        if ($depth <= $level) {
77
            return;
78
        }
79 57
        if (is_object($variable)) {
80 17
            if (in_array($variable, $this->objects, true)
81 17
                || in_array(get_class($variable), $this->excludedClasses, true)) {
82 16
                return;
83
            }
84 17
            $this->objects[] = $variable;
85 17
            $variable = $this->getObjectProperties($variable);
86
        }
87 57
        if (is_array($variable)) {
88 48
            foreach ($variable as $value) {
89 46
                $this->buildObjectsCache($value, $depth, $level + 1);
90
            }
91
        }
92
    }
93
94 57
    private function asJsonInternal($variable, bool $format, int $depth, int $objectCollapseLevel): string|bool
95
    {
96 57
        $options = JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE;
97
98 57
        if ($format) {
99
            $options |= JSON_PRETTY_PRINT;
100
        }
101
102 57
        return json_encode($this->dumpNested($variable, $depth, $objectCollapseLevel), $options);
103
    }
104
105 57
    private function dumpNested($variable, int $depth, int $objectCollapseLevel): mixed
106
    {
107 57
        $this->buildObjectsCache($variable, $depth);
108 57
        return $this->dumpNestedInternal($variable, $depth, 0, $objectCollapseLevel);
109
    }
110
111 17
    private function getObjectProperties($var): array
112
    {
113 17
        if ('__PHP_Incomplete_Class' !== get_class($var) && method_exists($var, '__debugInfo')) {
114
            $var = $var->__debugInfo();
115
        }
116
117 17
        return (array)$var;
118
    }
119
120 57
    private function dumpNestedInternal($var, int $depth, int $level, int $objectCollapseLevel = 0): mixed
121
    {
122 57
        $output = $var;
123
124 57
        switch (gettype($var)) {
125 57
            case 'array':
126 38
                if ($depth <= $level) {
127
                    return 'array [...]';
128
                }
129
130 38
                $output = [];
131 38
                foreach ($var as $key => $value) {
132 37
                    $keyDisplay = str_replace("\0", '::', trim((string)$key));
133 37
                    $output[$keyDisplay] = $this->dumpNestedInternal($value, $depth, $level + 1, $objectCollapseLevel);
134
                }
135
136 38
                break;
137 56
            case 'object':
138 17
                $objectDescription = $this->getObjectDescription($var);
139 17
                if ($depth <= $level || in_array(get_class($var), $this->excludedClasses, true)) {
140
                    $output = $objectDescription . ' (...)';
141
                    break;
142
                }
143
144 17
                if ($var instanceof Closure) {
145 10
                    $output = [$objectDescription => $this->exportClosure($var)];
146 10
                    break;
147
                }
148
149 8
                if ($objectCollapseLevel < $level && in_array($var, $this->objects, true)) {
150 5
                    $output = 'object@' . $objectDescription;
151 5
                    break;
152
                }
153
154 8
                $output = [];
155 8
                $properties = $this->getObjectProperties($var);
156 8
                if (empty($properties)) {
157 5
                    $output[$objectDescription] = '{stateless object}';
158 5
                    break;
159
                }
160 3
                foreach ($properties as $key => $value) {
161 3
                    $keyDisplay = $this->normalizeProperty((string)$key);
162
                    /**
163
                     * @psalm-suppress InvalidArrayOffset
164
                     */
165 3
                    $output[$objectDescription][$keyDisplay] = $this->dumpNestedInternal(
166
                        $value,
167
                        $depth,
168 3
                        $level + 1,
169
                        $objectCollapseLevel
170
                    );
171
                }
172
173 3
                break;
174 45
            case 'resource':
175 44
            case 'resource (closed)':
176 1
                $output = $this->getResourceDescription($var);
177 1
                break;
178
        }
179
180 57
        return $output;
181
    }
182
183 17
    #[Pure]
184
    private function getObjectDescription(object $object): string
185
    {
186 17
        return get_class($object) . '#' . 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
        $type = get_resource_type($resource);
207 1
        if ($type === 'stream') {
208 1
            $desc = stream_get_meta_data($resource);
209
        } else {
210
            $desc = '{resource}';
211
        }
212
213 1
        return $desc;
214
    }
215
216
    /**
217
     * Exports a {@see \Closure} instance.
218
     *
219
     * @param Closure $closure Closure instance.
220
     *
221
     * @throws \ReflectionException
222
     *
223
     * @return string
224
     */
225 10
    private function exportClosure(Closure $closure): string
226
    {
227 10
        if (self::$closureExporter === null) {
228 1
            self::$closureExporter = new ClosureExporter();
229
        }
230
231 10
        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

231
        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...
232
    }
233
}
234