Passed
Push — master ( 67aa31...d47bdf )
by Kirill
03:20
created

Dumper::renderObject()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 48
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 48
rs 8.0555
cc 9
nc 20
nop 5
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Debug;
13
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerAwareTrait;
16
use Psr\Log\LoggerInterface;
17
use Spiral\Debug\Exception\DumperException;
18
use Spiral\Debug\Renderer\ConsoleRenderer;
19
use Spiral\Debug\Renderer\HtmlRenderer;
20
use Spiral\Debug\Renderer\PlainRenderer;
21
22
/**
23
 * Renderer exports the content of the given variable, array or object into human friendly form.
24
 */
25
class Dumper implements LoggerAwareInterface
26
{
27
    use LoggerAwareTrait;
28
29
    /**
30
     * Directives for dump output forwarding.
31
     */
32
    public const OUTPUT            = 0;
33
    public const RETURN            = 1;
34
    public const LOGGER            = 2;
35
    public const ERROR_LOG         = 3;
36
    public const OUTPUT_CLI        = 4;
37
    public const OUTPUT_CLI_COLORS = 5;
38
39
    /** @var int */
40
    private $maxLevel = 12;
41
42
    /**
43
     * Default render associations.
44
     *
45
     * @var array|RendererInterface[]
46
     */
47
    private $targets = [
48
        self::OUTPUT            => HtmlRenderer::class,
49
        self::OUTPUT_CLI        => PlainRenderer::class,
50
        self::OUTPUT_CLI_COLORS => ConsoleRenderer::class,
51
        self::RETURN            => HtmlRenderer::class,
52
        self::LOGGER            => PlainRenderer::class,
53
        self::ERROR_LOG         => PlainRenderer::class,
54
    ];
55
56
    /**
57
     * @param LoggerInterface $logger
58
     */
59
    public function __construct(LoggerInterface $logger = null)
60
    {
61
        if (!empty($logger)) {
62
            $this->setLogger($logger);
63
        }
64
    }
65
66
    /**
67
     * Set max nesting level for value dumping.
68
     *
69
     * @param int $maxLevel
70
     */
71
    public function setMaxLevel(int $maxLevel): void
72
    {
73
        $this->maxLevel = max($maxLevel, 1);
74
    }
75
76
    /**
77
     * Dump given value into target output.
78
     *
79
     * @param mixed $value
80
     * @param int   $target Possible options: OUTPUT, RETURN, ERROR_LOG, LOGGER.
81
     * @return string
82
     * @throws DumperException
83
     */
84
    public function dump($value, int $target = self::OUTPUT): ?string
85
    {
86
        $r = $this->getRenderer($target);
87
        $dump = $r->wrapContent($this->renderValue($r, $value));
88
89
        switch ($target) {
90
            case self::OUTPUT:
91
                echo $dump;
92
                break;
93
94
            case self::RETURN:
95
                return $dump;
96
97
            case self::LOGGER:
98
                if ($this->logger == null) {
99
                    throw new DumperException('Unable to dump value to log, no associated LoggerInterface');
100
                }
101
                $this->logger->debug($dump);
102
                break;
103
104
            case self::ERROR_LOG:
105
                error_log($dump, 0);
106
                break;
107
        }
108
109
        return null;
110
    }
111
112
    /**
113
     * Associate rendered with given output target.
114
     *
115
     * @param int               $target
116
     * @param RendererInterface $renderer
117
     * @return Dumper
118
     * @throws DumperException
119
     */
120
    public function setRenderer(int $target, RendererInterface $renderer): Dumper
121
    {
122
        if (!isset($this->targets[$target])) {
123
            throw new DumperException(sprintf('Undefined dump target %d', $target));
124
        }
125
126
        $this->targets[$target] = $renderer;
127
128
        return $this;
129
    }
130
131
    /**
132
     * Returns renderer instance associated with given output target. Automatically detects CLI mode, RR mode and
133
     * colorization support.
134
     *
135
     * @param int $target
136
     * @return RendererInterface
137
     * @throws DumperException
138
     */
139
    private function getRenderer(int $target): RendererInterface
140
    {
141
        if ($target == self::OUTPUT && System::isCLI()) {
142
            if (System::isColorsSupported(STDOUT)) {
143
                $target = self::OUTPUT_CLI_COLORS;
144
            } else {
145
                $target = self::OUTPUT_CLI;
146
            }
147
        }
148
149
        if (!isset($this->targets[$target])) {
150
            throw new DumperException(sprintf('Undefined dump target %d', $target));
151
        }
152
153
        if (is_string($this->targets[$target])) {
154
            $this->targets[$target] = new $this->targets[$target]();
155
        }
156
157
        return $this->targets[$target];
158
    }
159
160
    /**
161
     * Variable dumper. This is the oldest spiral function originally written in 2007. :).
162
     *
163
     * @param RendererInterface $r          Render to style value content.
164
     * @param mixed             $value
165
     * @param string            $name       Variable name, internal.
166
     * @param int               $level      Dumping level, internal.
167
     * @param bool              $hideHeader Hide array/object header, internal.
168
     *
169
     * @return string
170
     */
171
    private function renderValue(
172
        RendererInterface $r,
173
        $value,
174
        string $name = '',
175
        int $level = 0,
176
        bool $hideHeader = false
177
    ): string {
178
        if (!$hideHeader && !empty($name)) {
179
            $header = $r->indent($level) . $r->apply($name, 'name') . $r->apply(' = ', 'syntax', '=');
180
        } else {
181
            $header = $r->indent($level);
182
        }
183
184
        if ($level > $this->maxLevel) {
185
            //Renderer is not reference based, we can't dump too deep values
186
            return $r->indent($level) . $r->apply('-too deep-', 'maxLevel') . "\n";
187
        }
188
189
        $type = strtolower(gettype($value));
190
191
        if ($type == 'array') {
192
            return $header . $this->renderArray($r, $value, $level, $hideHeader);
193
        }
194
195
        if ($type == 'object') {
196
            return $header . $this->renderObject($r, $value, $level, $hideHeader);
197
        }
198
199
        if ($type == 'resource') {
200
            //No need to dump resource value
201
            $element = get_resource_type($value) . ' resource ';
202
203
            return $header . $r->apply($element, 'type', 'resource') . "\n";
204
        }
205
206
        //Value length
207
        $length = strlen((string)$value);
208
209
        //Including type size
210
        $header .= $r->apply("{$type}({$length})", 'type', $type);
211
212
        $element = null;
213
        switch ($type) {
214
            case 'string':
215
                $element = $r->escapeStrings() ? htmlspecialchars($value) : $value;
216
                break;
217
218
            case 'boolean':
219
                $element = ($value ? 'true' : 'false');
220
                break;
221
222
            default:
223
                if ($value !== null) {
224
                    //Not showing null value, type is enough
225
                    $element = var_export($value, true);
226
                }
227
        }
228
229
        //Including value
230
        return $header . ' ' . $r->apply($element, 'value', $type) . "\n";
231
    }
232
233
    /**
234
     * @param RendererInterface $r
235
     * @param array             $array
236
     * @param int               $level
237
     * @param bool              $hideHeader
238
     *
239
     * @return string
240
     */
241
    private function renderArray(RendererInterface $r, array $array, int $level, bool $hideHeader = false): string
242
    {
243
        if (!$hideHeader) {
244
            $count = count($array);
245
246
            //Array size and scope
247
            $output = $r->apply("array({$count})", 'type', 'array') . "\n";
248
            $output .= $r->indent($level) . $r->apply('[', 'syntax', '[') . "\n";
249
        } else {
250
            $output = '';
251
        }
252
253
        foreach ($array as $key => $value) {
254
            if (!is_numeric($key)) {
255
                if (is_string($key) && $r->escapeStrings()) {
256
                    $key = htmlspecialchars($key);
257
                }
258
259
                $key = "'{$key}'";
260
            }
261
262
            $output .= $this->renderValue($r, $value, "[{$key}]", $level + 1);
263
        }
264
265
        if (!$hideHeader) {
266
            //Closing array scope
267
            $output .= $r->indent($level) . $r->apply(']', 'syntax', ']') . "\n";
268
        }
269
270
        return $output;
271
    }
272
273
    /**
274
     * @param RendererInterface $r
275
     * @param object            $value
276
     * @param int               $level
277
     * @param bool              $hideHeader
278
     * @param string            $class
279
     *
280
     * @return string
281
     */
282
    private function renderObject(
283
        RendererInterface $r,
284
        $value,
285
        int $level,
286
        bool $hideHeader = false,
287
        string $class = ''
288
    ): string {
289
        if (!$hideHeader) {
290
            $type = ($class ?: get_class($value)) . ' object ';
291
292
            $header = $r->apply($type, 'type', 'object') . "\n";
293
            $header .= $r->indent($level) . $r->apply('(', 'syntax', '(') . "\n";
294
        } else {
295
            $header = '';
296
        }
297
298
        //Let's use method specifically created for dumping
299
        if (method_exists($value, '__debugInfo') || $value instanceof \Closure) {
300
            if ($value instanceof \Closure) {
301
                $debugInfo = $this->describeClosure($value);
302
            } else {
303
                $debugInfo = $value->__debugInfo();
304
            }
305
306
            if (is_array($debugInfo)) {
307
                //Pretty view
308
                $debugInfo = (object)$debugInfo;
309
            }
310
311
            if (is_object($debugInfo)) {
312
                //We are not including syntax elements here
313
                return $this->renderObject($r, $debugInfo, $level, false, get_class($value));
314
            }
315
316
            return $header
317
                . $this->renderValue($r, $debugInfo, '', $level + (is_scalar($value)), true)
318
                . $r->indent($level) . $r->apply(')', 'syntax', ')') . "\n";
319
        }
320
321
        $refection = new \ReflectionObject($value);
322
323
        $output = '';
324
        foreach ($refection->getProperties() as $property) {
325
            $output .= $this->renderProperty($r, $value, $property, $level);
326
        }
327
328
        //Header, content, footer
329
        return $header . $output . $r->indent($level) . $r->apply(')', 'syntax', ')') . "\n";
330
    }
331
332
    /**
333
     * @param RendererInterface   $r
334
     * @param object              $value
335
     * @param \ReflectionProperty $p
336
     * @param int                 $level
337
     *
338
     * @return string
339
     */
340
    private function renderProperty(RendererInterface $r, $value, \ReflectionProperty $p, int $level): string
341
    {
342
        if ($p->isStatic()) {
343
            return '';
344
        }
345
346
        if (
347
            !($value instanceof \stdClass)
348
            && is_string($p->getDocComment())
349
            && strpos($p->getDocComment(), '@internal') !== false
350
        ) {
351
            // Memory loop while reading doc comment for stdClass variables?
352
            // Report a PHP bug about treating comment INSIDE property declaration as doc comment.
353
            return '';
354
        }
355
356
        //Property access level
357
        $access = $this->getAccess($p);
358
359
        //To read private and protected properties
360
        $p->setAccessible(true);
361
362
        if ($value instanceof \stdClass) {
363
            $name = $r->apply($p->getName(), 'dynamic');
364
        } else {
365
            //Property name includes access level
366
            $name = $p->getName() . $r->apply(':' . $access, 'access', $access);
367
        }
368
369
        return $this->renderValue($r, $p->getValue($value), $name, $level + 1);
370
    }
371
372
    /**
373
     * Fetch information about the closure.
374
     *
375
     * @param \Closure $closure
376
     * @return array
377
     */
378
    private function describeClosure(\Closure $closure): array
379
    {
380
        try {
381
            $r = new \ReflectionFunction($closure);
382
        } catch (\ReflectionException $e) {
383
            return ['closure' => 'unable to resolve'];
384
        }
385
386
        return [
387
            'name' => $r->getName() . " (lines {$r->getStartLine()}:{$r->getEndLine()})",
388
            'file' => $r->getFileName(),
389
            'this' => $r->getClosureThis()
390
        ];
391
    }
392
393
    /**
394
     * Property access level label.
395
     *
396
     * @param \ReflectionProperty $p
397
     *
398
     * @return string
399
     */
400
    private function getAccess(\ReflectionProperty $p): string
401
    {
402
        if ($p->isPrivate()) {
403
            return 'private';
404
        } elseif ($p->isProtected()) {
405
            return 'protected';
406
        }
407
408
        return 'public';
409
    }
410
}
411