Passed
Pull Request — master (#232)
by Dmitriy
12:45
created

Dumper::dumpNestedInternal()   D

Complexity

Conditions 18
Paths 13

Size

Total Lines 77
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 18.2795

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 54
nc 13
nop 5
dl 0
loc 77
ccs 38
cts 42
cp 0.9048
crap 18.2795
rs 4.8666
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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