Completed
Branch feature/pre-split (289154)
by Anton
03:22
created

Dumper::describeClosure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
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
    public function __construct(
54
        int $maxLevel = 10,
55
        Style $style = null,
56
        LoggerInterface $logger = null
57
    ) {
58
        $this->maxLevel = $maxLevel;
59
        $this->style = $style ?? new Style();
60
61
        if (!empty($logger)) {
62
            $this->setLogger($logger);
63
        }
64
    }
65
66
    /**
67
     * Set dump styler.
68
     *
69
     * @param Style $style
70
     *
71
     * @return self
72
     */
73
    public function setStyle(Style $style): Dumper
74
    {
75
        $this->style = $style;
76
77
        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
    public function dump($value, int $output = self::OUTPUT_ECHO): string
89
    {
90
        switch ($output) {
91
            case self::OUTPUT_ECHO:
92
                echo $this->style->wrapContainer($this->dumpValue($value, '', 0));
93
                break;
94
95
            case self::OUTPUT_LOG:
96
                if (!empty($this->logger)) {
97
                    $this->logger->debug($this->dump($value, self::OUTPUT_RETURN));
98
                }
99
                break;
100
101
            case self::OUTPUT_RETURN:
102
                return $this->style->wrapContainer($this->dumpValue($value, '', 0));
103
        }
104
105
        //Nothing to return
106
        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
    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
        $indent = $this->style->indent($level);
127
128
        if (!$hideHeader && !empty($name)) {
129
            //Showing element name (if any provided)
130
            $header = $indent . $this->style->apply($name, 'name');
131
132
            //Showing equal sing
133
            $header .= $this->style->apply(' = ', 'syntax', '=');
134
        } else {
135
            $header = $indent;
136
        }
137
138
        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
        $type = strtolower(gettype($value));
144
145
        if ($type == 'array') {
146
            return $header . $this->dumpArray($value, $level, $hideHeader);
147
        }
148
149
        if ($type == 'object') {
150
            return $header . $this->dumpObject($value, $level, $hideHeader);
151
        }
152
153
        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
        $length = strlen($value);
162
163
        //Including type size
164
        $header .= $this->style->apply("{$type}({$length})", 'type', $type);
165
166
        $element = null;
167
        switch ($type) {
168
            case 'string':
169
                $element = htmlspecialchars($value);
170
                break;
171
172
            case 'boolean':
173
                $element = ($value ? 'true' : 'false');
174
                break;
175
176
            default:
177
                if ($value !== null) {
178
                    //Not showing null value, type is enough
179
                    $element = var_export($value, true);
180
                }
181
        }
182
183
        //Including value
184
        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
    private function dumpArray(array $array, int $level, bool $hideHeader = false): string
195
    {
196
        $indent = $this->style->indent($level);
197
198
        if (!$hideHeader) {
199
            $count = count($array);
200
201
            //Array size and scope
202
            $output = $this->style->apply("array({$count})", 'type', 'array') . "\n";
203
            $output .= $indent . $this->style->apply('[', 'syntax', '[') . "\n";
204
        } else {
205
            $output = '';
206
        }
207
208
        foreach ($array as $key => $value) {
209
            if (!is_numeric($key)) {
210
                if (is_string($key)) {
211
                    $key = htmlspecialchars($key);
212
                }
213
214
                $key = "'{$key}'";
215
            }
216
217
            $output .= $this->dumpValue($value, "[{$key}]", $level + 1);
218
        }
219
220
        if (!$hideHeader) {
221
            //Closing array scope
222
            $output .= $indent . $this->style->apply(']', 'syntax', ']') . "\n";
223
        }
224
225
        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
    private function dumpObject(
237
        $object,
238
        int $level,
239
        bool $hideHeader = false,
240
        string $class = ''
241
    ): string {
242
        $indent = $this->style->indent($level);
243
244
        if (!$hideHeader) {
245
            $type = ($class ?: get_class($object)) . ' object ';
246
247
            $header = $this->style->apply($type, 'type', 'object') . "\n";
248
            $header .= $indent . $this->style->apply('(', 'syntax', '(') . "\n";
249
        } else {
250
            $header = '';
251
        }
252
253
        //Let's use method specifically created for dumping
254
        if (method_exists($object, '__debugInfo') || $object instanceof \Closure) {
255
            if ($object instanceof \Closure) {
256
                $debugInfo = $this->describeClosure($object);
257
            } else {
258
                $debugInfo = $object->__debugInfo();
259
            }
260
261
            if (is_array($debugInfo)) {
262
                //Pretty view
263
                $debugInfo = (object)$debugInfo;
264
            }
265
266
            if (is_object($debugInfo)) {
267
                //We are not including syntax elements here
268
                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
        $refection = new \ReflectionObject($object);
277
278
        $output = '';
279
        foreach ($refection->getProperties() as $property) {
280
            $output .= $this->dumpProperty($object, $property, $level);
281
        }
282
283
        //Header, content, footer
284
        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
    private function dumpProperty($object, \ReflectionProperty $property, int $level): string
295
    {
296
        if ($property->isStatic()) {
297
            return '';
298
        }
299
300
        if (
301
            !($object instanceof \stdClass)
302
            && 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
            return '';
307
        }
308
309
        //Property access level
310
        $access = $this->getAccess($property);
311
312
        //To read private and protected properties
313
        $property->setAccessible(true);
314
315
        if ($object instanceof \stdClass) {
316
            $name = $this->style->apply($property->getName(), 'dynamic');
317
        } else {
318
            //Property name includes access level
319
            $name = $property->getName() . $this->style->apply(':' . $access, 'access', $access);
320
        }
321
322
        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
    private function describeClosure(\Closure $closure): array
333
    {
334
        $reflection = new \ReflectionFunction($closure);
335
336
        return [
337
            'name' => $reflection->getName() . " (lines {$reflection->getStartLine()}:{$reflection->getEndLine()})",
338
            'file' => $reflection->getFileName(),
339
            'this' => $reflection->getClosureThis()
340
        ];
341
    }
342
343
    /**
344
     * Property access level label.
345
     *
346
     * @param \ReflectionProperty $property
347
     *
348
     * @return string
349
     */
350
    private function getAccess(\ReflectionProperty $property): string
351
    {
352
        if ($property->isPrivate()) {
353
            return 'private';
354
        } elseif ($property->isProtected()) {
355
            return 'protected';
356
        }
357
358
        return 'public';
359
    }
360
}
361