Passed
Push — master ( c69a4e...3c6756 )
by Michael
21:44 queued 13:20
created

Parser::parseObject()   F

Complexity

Conditions 30
Paths > 20000

Size

Total Lines 135
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 30
eloc 82
nc 192682
nop 2
dl 0
loc 135
rs 0
c 0
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
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2013 Jonathan Vollebregt ([email protected]), Rokas Šleinius ([email protected])
9
 *
10
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
11
 * this software and associated documentation files (the "Software"), to deal in
12
 * the Software without restriction, including without limitation the rights to
13
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
 * the Software, and to permit persons to whom the Software is furnished to do so,
15
 * subject to the following conditions:
16
 *
17
 * The above copyright notice and this permission notice shall be included in all
18
 * copies or substantial portions of the Software.
19
 *
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
 */
27
28
namespace Kint\Parser;
29
30
use DomainException;
31
use Exception;
32
use InvalidArgumentException;
33
use Kint\Utils;
34
use Kint\Value\AbstractValue;
35
use Kint\Value\ArrayValue;
36
use Kint\Value\ClosedResourceValue;
37
use Kint\Value\Context\ArrayContext;
38
use Kint\Value\Context\ClassDeclaredContext;
39
use Kint\Value\Context\ClassOwnedContext;
40
use Kint\Value\Context\ContextInterface;
41
use Kint\Value\Context\PropertyContext;
42
use Kint\Value\FixedWidthValue;
43
use Kint\Value\InstanceValue;
44
use Kint\Value\Representation\ContainerRepresentation;
45
use Kint\Value\Representation\StringRepresentation;
46
use Kint\Value\ResourceValue;
47
use Kint\Value\StringValue;
48
use Kint\Value\UninitializedValue;
49
use Kint\Value\UnknownValue;
50
use Kint\Value\VirtualValue;
51
use ReflectionClass;
52
use ReflectionObject;
53
use ReflectionProperty;
54
use ReflectionReference;
55
56
/**
57
 * @psalm-type ParserTrigger int-mask-of<Parser::TRIGGER_*>
58
 */
59
class Parser
60
{
61
    /**
62
     * Plugin triggers.
63
     *
64
     * These are constants indicating trigger points for plugins
65
     *
66
     * BEGIN: Before normal parsing
67
     * SUCCESS: After successful parsing
68
     * RECURSION: After parsing cancelled by recursion
69
     * DEPTH_LIMIT: After parsing cancelled by depth limit
70
     * COMPLETE: SUCCESS | RECURSION | DEPTH_LIMIT
71
     *
72
     * While a plugin's getTriggers may return any of these only one should
73
     * be given to the plugin when PluginInterface::parse is called
74
     */
75
    public const TRIGGER_NONE = 0;
76
    public const TRIGGER_BEGIN = 1 << 0;
77
    public const TRIGGER_SUCCESS = 1 << 1;
78
    public const TRIGGER_RECURSION = 1 << 2;
79
    public const TRIGGER_DEPTH_LIMIT = 1 << 3;
80
    public const TRIGGER_COMPLETE = self::TRIGGER_SUCCESS | self::TRIGGER_RECURSION | self::TRIGGER_DEPTH_LIMIT;
81
82
    /** @psalm-var ?class-string */
83
    protected ?string $caller_class;
84
    protected int $depth_limit = 0;
85
    protected array $array_ref_stack = [];
86
    protected array $object_hashes = [];
87
    protected array $plugins = [];
88
89
    /**
90
     * @param int     $depth_limit Maximum depth to parse data
91
     * @param ?string $caller      Caller class name
92
     *
93
     * @psalm-param ?class-string $caller
94
     */
95
    public function __construct(int $depth_limit = 0, ?string $caller = null)
96
    {
97
        $this->depth_limit = $depth_limit;
98
        $this->caller_class = $caller;
99
    }
100
101
    /**
102
     * Set the caller class.
103
     *
104
     * @psalm-param ?class-string $caller
105
     */
106
    public function setCallerClass(?string $caller = null): void
107
    {
108
        $this->noRecurseCall();
109
110
        $this->caller_class = $caller;
111
    }
112
113
    /** @psalm-return ?class-string */
114
    public function getCallerClass(): ?string
115
    {
116
        return $this->caller_class;
117
    }
118
119
    /**
120
     * Set the depth limit.
121
     *
122
     * @param int $depth_limit Maximum depth to parse data, 0 for none
123
     */
124
    public function setDepthLimit(int $depth_limit = 0): void
125
    {
126
        $this->noRecurseCall();
127
128
        $this->depth_limit = $depth_limit;
129
    }
130
131
    public function getDepthLimit(): int
132
    {
133
        return $this->depth_limit;
134
    }
135
136
    /**
137
     * Parses a variable into a Kint object structure.
138
     *
139
     * @param mixed &$var The input variable
140
     */
141
    public function parse(&$var, ContextInterface $c): AbstractValue
142
    {
143
        $type = \strtolower(\gettype($var));
144
145
        if ($v = $this->applyPluginsBegin($var, $c, $type)) {
146
            return $v;
147
        }
148
149
        switch ($type) {
150
            case 'array':
151
                return $this->parseArray($var, $c);
152
            case 'boolean':
153
            case 'double':
154
            case 'integer':
155
            case 'null':
156
                return $this->parseFixedWidth($var, $c);
157
            case 'object':
158
                return $this->parseObject($var, $c);
159
            case 'resource':
160
                return $this->parseResource($var, $c);
161
            case 'string':
162
                return $this->parseString($var, $c);
163
            case 'resource (closed)':
164
                return $this->parseResourceClosed($var, $c);
165
166
            case 'unknown type': // @codeCoverageIgnore
167
            default:
168
                // These should never happen. Unknown is resource (closed) from old
169
                // PHP versions and there shouldn't be any other types.
170
                return $this->parseUnknown($var, $c); // @codeCoverageIgnore
171
        }
172
    }
173
174
    public function addPlugin(PluginInterface $p): void
175
    {
176
        if (!$types = $p->getTypes()) {
177
            return;
178
        }
179
180
        if (!$triggers = $p->getTriggers()) {
181
            return;
182
        }
183
184
        if ($triggers & self::TRIGGER_BEGIN && !$p instanceof PluginBeginInterface) {
185
            throw new InvalidArgumentException('Parsers triggered on begin must implement PluginBeginInterface');
186
        }
187
188
        if ($triggers & self::TRIGGER_COMPLETE && !$p instanceof PluginCompleteInterface) {
189
            throw new InvalidArgumentException('Parsers triggered on completion must implement PluginCompleteInterface');
190
        }
191
192
        $p->setParser($this);
193
194
        foreach ($types as $type) {
195
            $this->plugins[$type] ??= [
196
                self::TRIGGER_BEGIN => [],
197
                self::TRIGGER_SUCCESS => [],
198
                self::TRIGGER_RECURSION => [],
199
                self::TRIGGER_DEPTH_LIMIT => [],
200
            ];
201
202
            foreach ($this->plugins[$type] as $trigger => &$pool) {
203
                if ($triggers & $trigger) {
204
                    $pool[] = $p;
205
                }
206
            }
207
        }
208
    }
209
210
    public function clearPlugins(): void
211
    {
212
        $this->plugins = [];
213
    }
214
215
    protected function noRecurseCall(): void
216
    {
217
        $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS);
218
219
        $caller_frame = [
220
            'function' => __FUNCTION__,
221
        ];
222
223
        while (isset($bt[0]['object']) && $bt[0]['object'] === $this) {
224
            $caller_frame = \array_shift($bt);
225
        }
226
227
        foreach ($bt as $frame) {
228
            if (isset($frame['object']) && $frame['object'] === $this) {
229
                throw new DomainException(__CLASS__.'::'.$caller_frame['function'].' cannot be called from inside a parse');
230
            }
231
        }
232
    }
233
234
    /**
235
     * @psalm-param null|bool|float|int &$var
236
     */
237
    private function parseFixedWidth(&$var, ContextInterface $c): AbstractValue
238
    {
239
        $v = new FixedWidthValue($c, $var);
240
241
        return $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS);
242
    }
243
244
    private function parseString(string &$var, ContextInterface $c): AbstractValue
245
    {
246
        $string = new StringValue($c, $var, Utils::detectEncoding($var));
247
248
        if (false !== $string->getEncoding() && \strlen($var)) {
249
            $string->addRepresentation(new StringRepresentation('Contents', $var, null, true));
250
        }
251
252
        return $this->applyPluginsComplete($var, $string, self::TRIGGER_SUCCESS);
253
    }
254
255
    private function parseArray(array &$var, ContextInterface $c): AbstractValue
256
    {
257
        $size = \count($var);
258
        $contents = [];
259
        $parentRef = ReflectionReference::fromArrayElement([&$var], 0)->getId();
260
261
        if (isset($this->array_ref_stack[$parentRef])) {
262
            $array = new ArrayValue($c, $size, $contents);
263
            $array->flags |= AbstractValue::FLAG_RECURSION;
264
265
            return $this->applyPluginsComplete($var, $array, self::TRIGGER_RECURSION);
266
        }
267
268
        $this->array_ref_stack[$parentRef] = true;
269
270
        $cdepth = $c->getDepth();
271
        $ap = $c->getAccessPath();
272
273
        if ($size > 0 && $this->depth_limit && $cdepth >= $this->depth_limit) {
274
            $array = new ArrayValue($c, $size, $contents);
275
            $array->flags |= AbstractValue::FLAG_DEPTH_LIMIT;
276
277
            $array = $this->applyPluginsComplete($var, $array, self::TRIGGER_DEPTH_LIMIT);
278
279
            unset($this->array_ref_stack[$parentRef]);
280
281
            return $array;
282
        }
283
284
        foreach ($var as $key => $_) {
285
            $child = new ArrayContext($key);
286
            $child->depth = $cdepth + 1;
287
            $child->reference = null !== ReflectionReference::fromArrayElement($var, $key);
288
289
            if (null !== $ap) {
290
                $child->access_path = $ap.'['.\var_export($key, true).']';
291
            }
292
293
            $contents[$key] = $this->parse($var[$key], $child);
294
        }
295
296
        $array = new ArrayValue($c, $size, $contents);
297
298
        if ($contents) {
299
            $array->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true));
300
        }
301
302
        $array = $this->applyPluginsComplete($var, $array, self::TRIGGER_SUCCESS);
303
304
        unset($this->array_ref_stack[$parentRef]);
305
306
        return $array;
307
    }
308
309
    /**
310
     * @psalm-return ReflectionProperty[]
311
     */
312
    private function getPropsOrdered(ReflectionClass $r): array
313
    {
314
        if ($parent = $r->getParentClass()) {
315
            $props = self::getPropsOrdered($parent);
0 ignored issues
show
Bug Best Practice introduced by
The method Kint\Parser\Parser::getPropsOrdered() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

315
            /** @scrutinizer ignore-call */ 
316
            $props = self::getPropsOrdered($parent);
Loading history...
316
        } else {
317
            $props = [];
318
        }
319
320
        foreach ($r->getProperties() as $prop) {
321
            if ($prop->isStatic()) {
322
                continue;
323
            }
324
325
            if ($prop->isPrivate()) {
326
                $props[] = $prop;
327
            } else {
328
                $props[$prop->name] = $prop;
329
            }
330
        }
331
332
        return $props;
333
    }
334
335
    /**
336
     * @codeCoverageIgnore
337
     *
338
     * @psalm-return ReflectionProperty[]
339
     */
340
    private function getPropsOrderedOld(ReflectionClass $r): array
341
    {
342
        $props = [];
343
344
        foreach ($r->getProperties() as $prop) {
345
            if ($prop->isStatic()) {
346
                continue;
347
            }
348
349
            $props[] = $prop;
350
        }
351
352
        while ($r = $r->getParentClass()) {
353
            foreach ($r->getProperties(ReflectionProperty::IS_PRIVATE) as $prop) {
354
                if ($prop->isStatic()) {
355
                    continue;
356
                }
357
358
                $props[] = $prop;
359
            }
360
        }
361
362
        return $props;
363
    }
364
365
    private function parseObject(object &$var, ContextInterface $c): AbstractValue
366
    {
367
        $hash = \spl_object_hash($var);
368
        $classname = \get_class($var);
369
370
        if (isset($this->object_hashes[$hash])) {
371
            $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var));
372
            $object->flags |= AbstractValue::FLAG_RECURSION;
373
374
            return $this->applyPluginsComplete($var, $object, self::TRIGGER_RECURSION);
375
        }
376
377
        $this->object_hashes[$hash] = true;
378
379
        $cdepth = $c->getDepth();
380
        $ap = $c->getAccessPath();
381
382
        if ($this->depth_limit && $cdepth >= $this->depth_limit) {
383
            $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var));
384
            $object->flags |= AbstractValue::FLAG_DEPTH_LIMIT;
385
386
            $object = $this->applyPluginsComplete($var, $object, self::TRIGGER_DEPTH_LIMIT);
387
388
            unset($this->object_hashes[$hash]);
389
390
            return $object;
391
        }
392
393
        if (KINT_PHP81) {
394
            $props = $this->getPropsOrdered(new ReflectionObject($var));
395
        } else {
396
            $props = $this->getPropsOrderedOld(new ReflectionObject($var)); // @codeCoverageIgnore
397
        }
398
399
        $values = (array) $var;
400
        $properties = [];
401
402
        foreach ($props as $rprop) {
403
            $rprop->setAccessible(true);
404
            $name = $rprop->getName();
405
406
            // Casting object to array:
407
            // private properties show in the form "\0$owner_class_name\0$property_name";
408
            // protected properties show in the form "\0*\0$property_name";
409
            // public properties show in the form "$property_name";
410
            // http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
411
            $key = $name;
412
            if ($rprop->isProtected()) {
413
                $key = "\0*\0".$name;
414
            } elseif ($rprop->isPrivate()) {
415
                $key = "\0".$rprop->getDeclaringClass()->getName()."\0".$name;
416
            }
417
            $initialized = \array_key_exists($key, $values);
418
            if ($key === (string) (int) $key) {
419
                $key = (int) $key;
420
            }
421
422
            if ($rprop->isDefault()) {
423
                $child = new PropertyContext(
424
                    $name,
425
                    $rprop->getDeclaringClass()->getName(),
426
                    ClassDeclaredContext::ACCESS_PUBLIC
427
                );
428
429
                $child->readonly = KINT_PHP81 && $rprop->isReadOnly();
430
431
                if ($rprop->isProtected()) {
432
                    $child->access = ClassDeclaredContext::ACCESS_PROTECTED;
433
                } elseif ($rprop->isPrivate()) {
434
                    $child->access = ClassDeclaredContext::ACCESS_PRIVATE;
435
                }
436
437
                if (KINT_PHP84) {
438
                    if ($rprop->isProtectedSet()) {
439
                        $child->access_set = ClassDeclaredContext::ACCESS_PROTECTED;
440
                    } elseif ($rprop->isPrivateSet()) {
441
                        $child->access_set = ClassDeclaredContext::ACCESS_PRIVATE;
442
                    }
443
444
                    $hooks = $rprop->getHooks();
445
                    if (isset($hooks['get'])) {
446
                        $child->hooks |= PropertyContext::HOOK_GET;
447
                        if ($hooks['get']->returnsReference()) {
448
                            $child->hooks |= PropertyContext::HOOK_GET_REF;
449
                        }
450
                    }
451
                    if (isset($hooks['set'])) {
452
                        $child->hooks |= PropertyContext::HOOK_SET;
453
454
                        $child->hook_set_type = (string) $rprop->getSettableType();
455
                        if ($child->hook_set_type !== (string) $rprop->getType()) {
456
                            $child->hooks |= PropertyContext::HOOK_SET_TYPE;
457
                        } elseif ('' === $child->hook_set_type) {
458
                            $child->hook_set_type = null;
459
                        }
460
                    }
461
                }
462
            } else {
463
                $child = new ClassOwnedContext($name, $rprop->getDeclaringClass()->getName());
464
            }
465
466
            $child->reference = $initialized && null !== ReflectionReference::fromArrayElement($values, $key);
467
            $child->depth = $cdepth + 1;
468
469
            if (null !== $ap && $child->isAccessible($this->caller_class)) {
470
                /** @psalm-var string $child->name */
471
                if (Utils::isValidPhpName($child->name)) {
472
                    $child->access_path = $ap.'->'.$child->name;
473
                } else {
474
                    $child->access_path = $ap.'->{'.\var_export($child->name, true).'}';
475
                }
476
            }
477
478
            if (KINT_PHP84 && $rprop->isVirtual()) {
479
                $properties[] = new VirtualValue($child);
480
            } elseif (!$initialized) {
481
                $properties[] = new UninitializedValue($child);
482
            } else {
483
                $properties[] = $this->parse($values[$key], $child);
484
            }
485
        }
486
487
        $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var));
488
        if ($props) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $props of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
489
            $object->setChildren($properties);
490
        }
491
492
        if ($properties) {
493
            $object->addRepresentation(new ContainerRepresentation('Properties', $properties));
494
        }
495
496
        $object = $this->applyPluginsComplete($var, $object, self::TRIGGER_SUCCESS);
497
        unset($this->object_hashes[$hash]);
498
499
        return $object;
500
    }
501
502
    /**
503
     * @psalm-param resource $var
504
     */
505
    private function parseResource(&$var, ContextInterface $c): AbstractValue
506
    {
507
        $resource = new ResourceValue($c, \get_resource_type($var));
508
509
        $resource = $this->applyPluginsComplete($var, $resource, self::TRIGGER_SUCCESS);
510
511
        return $resource;
512
    }
513
514
    /**
515
     * @psalm-param mixed $var
516
     */
517
    private function parseResourceClosed(&$var, ContextInterface $c): AbstractValue
518
    {
519
        $v = new ClosedResourceValue($c);
520
521
        $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS);
522
523
        return $v;
524
    }
525
526
    /**
527
     * Catch-all for any unexpectedgettype.
528
     *
529
     * This should never happen.
530
     *
531
     * @codeCoverageIgnore
532
     *
533
     * @psalm-param mixed $var
534
     */
535
    private function parseUnknown(&$var, ContextInterface $c): AbstractValue
536
    {
537
        $v = new UnknownValue($c);
538
539
        $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS);
540
541
        return $v;
542
    }
543
544
    /**
545
     * Applies plugins for a yet-unparsed value.
546
     *
547
     * @param mixed &$var The input variable
548
     */
549
    private function applyPluginsBegin(&$var, ContextInterface $c, string $type): ?AbstractValue
550
    {
551
        $plugins = $this->plugins[$type][self::TRIGGER_BEGIN] ?? [];
552
553
        foreach ($plugins as $plugin) {
554
            try {
555
                if ($v = $plugin->parseBegin($var, $c)) {
556
                    return $v;
557
                }
558
            } catch (Exception $e) {
559
                \trigger_error(
560
                    'An exception ('.Utils::errorSanitizeString(\get_class($e)).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing "'.Utils::errorSanitizeString(\get_class($plugin)).'"->parseBegin. Error message: '.Utils::errorSanitizeString($e->getMessage()),
561
                    E_USER_WARNING
562
                );
563
            }
564
        }
565
566
        return null;
567
    }
568
569
    /**
570
     * Applies plugins for a parsed AbstractValue.
571
     *
572
     * @param mixed &$var The input variable
573
     */
574
    private function applyPluginsComplete(&$var, AbstractValue $v, int $trigger): AbstractValue
575
    {
576
        $plugins = $this->plugins[$v->getType()][$trigger] ?? [];
577
578
        foreach ($plugins as $plugin) {
579
            try {
580
                $v = $plugin->parseComplete($var, $v, $trigger);
581
            } catch (Exception $e) {
582
                \trigger_error(
583
                    'An exception ('.Utils::errorSanitizeString(\get_class($e)).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing "'.Utils::errorSanitizeString(\get_class($plugin)).'"->parseComplete. Error message: '.Utils::errorSanitizeString($e->getMessage()),
584
                    E_USER_WARNING
585
                );
586
            }
587
        }
588
589
        return $v;
590
    }
591
}
592