Dumper::dumpObject()   B
last analyzed

Complexity

Conditions 9
Paths 30

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 9.2055

Importance

Changes 0
Metric Value
cc 9
nc 30
nop 4
dl 0
loc 50
rs 7.5353
c 0
b 0
f 0
ccs 19
cts 22
cp 0.8636
crap 9.2055
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Debug;
10
11
use Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerAwareTrait;
13
use Psr\Log\LoggerInterface;
14
use Spiral\Core\Component;
15
use Spiral\Core\Container\SingletonInterface;
16
use Spiral\Debug\Dumper\Style;
17
18
/**
19
 * One of the oldest spiral parts, used to dump variables content in user friendly way.
20
 *
21
 * @todo need cli style
22
 */
23
class Dumper extends Component implements SingletonInterface, LoggerAwareInterface
24
{
25
    use LoggerAwareTrait;
26
27
    /**
28
     * Options for dump() function to specify output.
29
     */
30
    const OUTPUT_ECHO   = 0;
31
    const OUTPUT_RETURN = 1;
32
    const OUTPUT_LOG    = 2;
33
34
    /**
35
     * Deepest level to be dumped.
36
     *
37
     * @var int
38
     */
39
    private $maxLevel = 10;
40
41
    /**
42
     * @invisible
43
     *
44
     * @var Style
45
     */
46
    private $style = null;
47
48
    /**
49
     * @param int             $maxLevel
50
     * @param Style           $style Light styler to be used by default.
51
     * @param LoggerInterface $logger
52
     */
53 11
    public function __construct(
54
        int $maxLevel = 10,
55
        Style $style = null,
56
        LoggerInterface $logger = null
57
    ) {
58 11
        $this->maxLevel = $maxLevel;
59 11
        $this->style = $style ?? new Style();
60
61 11
        if (!empty($logger)) {
62 1
            $this->setLogger($logger);
63
        }
64 11
    }
65
66
    /**
67
     * Set dump styler.
68
     *
69
     * @param Style $style
70
     *
71
     * @return self
72
     */
73 1
    public function setStyle(Style $style): Dumper
74
    {
75 1
        $this->style = $style;
76
77 1
        return $this;
78
    }
79
80
    /**
81
     * Dump specified value. Dumper will automatically detect CLI mode in OUTPUT_ECHO mode.
82
     *
83
     * @param mixed $value
84
     * @param int   $output
85
     *
86
     * @return string
87
     */
88 11
    public function dump($value, int $output = self::OUTPUT_ECHO): string
89
    {
90
        switch ($output) {
91 11
            case self::OUTPUT_ECHO:
92 1
                echo $this->style->wrapContainer($this->dumpValue($value, '', 0));
93 1
                break;
94
95 11
            case self::OUTPUT_LOG:
96 1
                if (!empty($this->logger)) {
97 1
                    $this->logger->debug($this->dump($value, self::OUTPUT_RETURN));
98
                }
99 1
                break;
100
101 11
            case self::OUTPUT_RETURN:
102 11
                return $this->style->wrapContainer($this->dumpValue($value, '', 0));
103
        }
104
105
        //Nothing to return
106 2
        return '';
107
    }
108
109
    /**
110
     * Variable dumper. This is the oldest spiral function originally written in 2007. :).
111
     *
112
     * @param mixed  $value
113
     * @param string $name       Variable name, internal.
114
     * @param int    $level      Dumping level, internal.
115
     * @param bool   $hideHeader Hide array/object header, internal.
116
     *
117
     * @return string
118
     */
119 11
    private function dumpValue(
120
        $value,
121
        string $name = '',
122
        int $level = 0,
123
        bool $hideHeader = false
124
    ): string {
125
        //Any dump starts with initial indent (level based)
126 11
        $indent = $this->style->indent($level);
127
128 11
        if (!$hideHeader && !empty($name)) {
129
            //Showing element name (if any provided)
130 6
            $header = $indent . $this->style->apply($name, 'name');
131
132
            //Showing equal sing
133 6
            $header .= $this->style->apply(' = ', 'syntax', '=');
134
        } else {
135 11
            $header = $indent;
136
        }
137
138 11
        if ($level > $this->maxLevel) {
139
            //Dumper is not reference based, we can't dump too deep values
140
            return $indent . $this->style->apply('-too deep-', 'maxLevel') . "\n";
141
        }
142
143 11
        $type = strtolower(gettype($value));
144
145 11
        if ($type == 'array') {
146 4
            return $header . $this->dumpArray($value, $level, $hideHeader);
147
        }
148
149 11
        if ($type == 'object') {
150 5
            return $header . $this->dumpObject($value, $level, $hideHeader);
151
        }
152
153 11
        if ($type == 'resource') {
154
            //No need to dump resource value
155
            $element = get_resource_type($value) . ' resource ';
156
157
            return $header . $this->style->apply($element, 'type', 'resource') . "\n";
158
        }
159
160
        //Value length
161 11
        $length = strlen($value);
162
163
        //Including type size
164 11
        $header .= $this->style->apply("{$type}({$length})", 'type', $type);
165
166 11
        $element = null;
167
        switch ($type) {
168 11
            case 'string':
169 9
                $element = htmlspecialchars($value);
170 9
                break;
171
172 5
            case 'boolean':
173 3
                $element = ($value ? 'true' : 'false');
174 3
                break;
175
176
            default:
177 5
                if ($value !== null) {
178
                    //Not showing null value, type is enough
179 2
                    $element = var_export($value, true);
180
                }
181
        }
182
183
        //Including value
184 11
        return $header . ' ' . $this->style->apply($element, 'value', $type) . "\n";
185
    }
186
187
    /**
188
     * @param array $array
189
     * @param int   $level
190
     * @param bool  $hideHeader
191
     *
192
     * @return string
193
     */
194 4
    private function dumpArray(array $array, int $level, bool $hideHeader = false): string
195
    {
196 4
        $indent = $this->style->indent($level);
197
198 4
        if (!$hideHeader) {
199 4
            $count = count($array);
200
201
            //Array size and scope
202 4
            $output = $this->style->apply("array({$count})", 'type', 'array') . "\n";
203 4
            $output .= $indent . $this->style->apply('[', 'syntax', '[') . "\n";
204
        } else {
205
            $output = '';
206
        }
207
208 4
        foreach ($array as $key => $value) {
209 1
            if (!is_numeric($key)) {
210
                if (is_string($key)) {
211
                    $key = htmlspecialchars($key);
212
                }
213
214
                $key = "'{$key}'";
215
            }
216
217 1
            $output .= $this->dumpValue($value, "[{$key}]", $level + 1);
218
        }
219
220 4
        if (!$hideHeader) {
221
            //Closing array scope
222 4
            $output .= $indent . $this->style->apply(']', 'syntax', ']') . "\n";
223
        }
224
225 4
        return $output;
226
    }
227
228
    /**
229
     * @param object $object
230
     * @param int    $level
231
     * @param bool   $hideHeader
232
     * @param string $class
233
     *
234
     * @return string
235
     */
236 5
    private function dumpObject(
237
        $object,
238
        int $level,
239
        bool $hideHeader = false,
240
        string $class = ''
241
    ): string {
242 5
        $indent = $this->style->indent($level);
243
244 5
        if (!$hideHeader) {
245 5
            $type = ($class ?: get_class($object)) . ' object ';
246
247 5
            $header = $this->style->apply($type, 'type', 'object') . "\n";
248 5
            $header .= $indent . $this->style->apply('(', 'syntax', '(') . "\n";
249
        } else {
250
            $header = '';
251
        }
252
253
        //Let's use method specifically created for dumping
254 5
        if (method_exists($object, '__debugInfo') || $object instanceof \Closure) {
255 2
            if ($object instanceof \Closure) {
256 1
                $debugInfo = $this->describeClosure($object);
257
            } else {
258 1
                $debugInfo = $object->__debugInfo();
259
            }
260
261 2
            if (is_array($debugInfo)) {
262
                //Pretty view
263 2
                $debugInfo = (object)$debugInfo;
264
            }
265
266 2
            if (is_object($debugInfo)) {
267
                //We are not including syntax elements here
268 2
                return $this->dumpObject($debugInfo, $level, false, get_class($object));
269
            }
270
271
            return $header
272
                . $this->dumpValue($debugInfo, '', $level + (is_scalar($object)), true)
273
                . $indent . $this->style->apply(')', 'syntax', ')') . "\n";
274
        }
275
276 5
        $refection = new \ReflectionObject($object);
277
278 5
        $output = '';
279 5
        foreach ($refection->getProperties() as $property) {
280 5
            $output .= $this->dumpProperty($object, $property, $level);
281
        }
282
283
        //Header, content, footer
284 5
        return $header . $output . $indent . $this->style->apply(')', 'syntax', ')') . "\n";
285
    }
286
287
    /**
288
     * @param object              $object
289
     * @param \ReflectionProperty $property
290
     * @param int                 $level
291
     *
292
     * @return string
293
     */
294 5
    private function dumpProperty($object, \ReflectionProperty $property, int $level): string
295
    {
296 5
        if ($property->isStatic()) {
297
            return '';
298
        }
299
300
        if (
301 5
            !($object instanceof \stdClass)
302 5
            && strpos($property->getDocComment(), '@invisible') !== false
303
        ) {
304
            //Memory loop while reading doc comment for stdClass variables?
305
            //Report a PHP bug about treating comment INSIDE property declaration as doc comment.
306 3
            return '';
307
        }
308
309
        //Property access level
310 5
        $access = $this->getAccess($property);
311
312
        //To read private and protected properties
313 5
        $property->setAccessible(true);
314
315 5
        if ($object instanceof \stdClass) {
316 2
            $name = $this->style->apply($property->getName(), 'dynamic');
317
        } else {
318
            //Property name includes access level
319 4
            $name = $property->getName() . $this->style->apply(':' . $access, 'access', $access);
320
        }
321
322 5
        return $this->dumpValue($property->getValue($object), $name, $level + 1);
323
    }
324
325
    /**
326
     * Fetch information about the closure.
327
     *
328
     * @param \Closure $closure
329
     *
330
     * @return array
331
     */
332 1
    private function describeClosure(\Closure $closure): array
333
    {
334 1
        $reflection = new \ReflectionFunction($closure);
335
336
        return [
337 1
            'name' => $reflection->getName() . " (lines {$reflection->getStartLine()}:{$reflection->getEndLine()})",
338 1
            'file' => $reflection->getFileName(),
339 1
            'this' => $reflection->getClosureThis()
340
        ];
341
    }
342
343
    /**
344
     * Property access level label.
345
     *
346
     * @param \ReflectionProperty $property
347
     *
348
     * @return string
349
     */
350 5
    private function getAccess(\ReflectionProperty $property): string
351
    {
352 5
        if ($property->isPrivate()) {
353 1
            return 'private';
354 4
        } elseif ($property->isProtected()) {
355 3
            return 'protected';
356
        }
357
358 2
        return 'public';
359
    }
360
}
361