Parser::parseResourceClosed()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 InvalidArgumentException;
32
use Kint\Utils;
33
use Kint\Value\AbstractValue;
34
use Kint\Value\ArrayValue;
35
use Kint\Value\ClosedResourceValue;
36
use Kint\Value\Context\ArrayContext;
37
use Kint\Value\Context\ClassDeclaredContext;
38
use Kint\Value\Context\ClassOwnedContext;
39
use Kint\Value\Context\ContextInterface;
40
use Kint\Value\Context\PropertyContext;
41
use Kint\Value\FixedWidthValue;
42
use Kint\Value\InstanceValue;
43
use Kint\Value\Representation\ContainerRepresentation;
44
use Kint\Value\Representation\StringRepresentation;
45
use Kint\Value\ResourceValue;
46
use Kint\Value\StringValue;
47
use Kint\Value\UninitializedValue;
48
use Kint\Value\UnknownValue;
49
use Kint\Value\VirtualValue;
50
use ReflectionClass;
51
use ReflectionObject;
52
use ReflectionProperty;
53
use ReflectionReference;
54
use Throwable;
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
        try {
177
            $this->noRecurseCall();
178
        } catch (DomainException $e) { // @codeCoverageIgnore
179
            \trigger_error('Calling Kint\\Parser::addPlugin from inside a parse is deprecated', E_USER_DEPRECATED); // @codeCoverageIgnore
180
        }
181
182
        if (!$types = $p->getTypes()) {
183
            return;
184
        }
185
186
        if (!$triggers = $p->getTriggers()) {
187
            return;
188
        }
189
190
        if ($triggers & self::TRIGGER_BEGIN && !$p instanceof PluginBeginInterface) {
191
            throw new InvalidArgumentException('Parsers triggered on begin must implement PluginBeginInterface');
192
        }
193
194
        if ($triggers & self::TRIGGER_COMPLETE && !$p instanceof PluginCompleteInterface) {
195
            throw new InvalidArgumentException('Parsers triggered on completion must implement PluginCompleteInterface');
196
        }
197
198
        $p->setParser($this);
199
200
        foreach ($types as $type) {
201
            $this->plugins[$type] ??= [
202
                self::TRIGGER_BEGIN => [],
203
                self::TRIGGER_SUCCESS => [],
204
                self::TRIGGER_RECURSION => [],
205
                self::TRIGGER_DEPTH_LIMIT => [],
206
            ];
207
208
            foreach ($this->plugins[$type] as $trigger => &$pool) {
209
                if ($triggers & $trigger) {
210
                    $pool[] = $p;
211
                }
212
            }
213
        }
214
    }
215
216
    public function clearPlugins(): void
217
    {
218
        try {
219
            $this->noRecurseCall();
220
        } catch (DomainException $e) { // @codeCoverageIgnore
221
            \trigger_error('Calling Kint\\Parser::clearPlugins from inside a parse is deprecated', E_USER_DEPRECATED); // @codeCoverageIgnore
222
        }
223
224
        $this->plugins = [];
225
    }
226
227
    protected function noRecurseCall(): void
228
    {
229
        $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS);
230
231
        \reset($bt);
232
        /** @psalm-var class-string $caller_frame['class'] */
233
        $caller_frame = \next($bt);
234
235
        foreach ($bt as $frame) {
236
            if (isset($frame['object']) && $frame['object'] === $this && 'parse' === $frame['function']) {
237
                throw new DomainException($caller_frame['class'].'::'.$caller_frame['function'].' cannot be called from inside a parse');
238
            }
239
        }
240
    }
241
242
    /**
243
     * @psalm-param null|bool|float|int &$var
244
     */
245
    private function parseFixedWidth(&$var, ContextInterface $c): AbstractValue
246
    {
247
        $v = new FixedWidthValue($c, $var);
248
249
        return $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS);
250
    }
251
252
    private function parseString(string &$var, ContextInterface $c): AbstractValue
253
    {
254
        $string = new StringValue($c, $var, Utils::detectEncoding($var));
255
256
        if (false !== $string->getEncoding() && \strlen($var)) {
257
            $string->addRepresentation(new StringRepresentation('Contents', $var, null, true));
258
        }
259
260
        return $this->applyPluginsComplete($var, $string, self::TRIGGER_SUCCESS);
261
    }
262
263
    private function parseArray(array &$var, ContextInterface $c): AbstractValue
264
    {
265
        $size = \count($var);
266
        $contents = [];
267
        $parentRef = ReflectionReference::fromArrayElement([&$var], 0)->getId();
268
269
        if (isset($this->array_ref_stack[$parentRef])) {
270
            $array = new ArrayValue($c, $size, $contents);
271
            $array->flags |= AbstractValue::FLAG_RECURSION;
272
273
            return $this->applyPluginsComplete($var, $array, self::TRIGGER_RECURSION);
274
        }
275
276
        try {
277
            $this->array_ref_stack[$parentRef] = true;
278
279
            $cdepth = $c->getDepth();
280
            $ap = $c->getAccessPath();
281
282
            if ($size > 0 && $this->depth_limit && $cdepth >= $this->depth_limit) {
283
                $array = new ArrayValue($c, $size, $contents);
284
                $array->flags |= AbstractValue::FLAG_DEPTH_LIMIT;
285
286
                return $this->applyPluginsComplete($var, $array, self::TRIGGER_DEPTH_LIMIT);
287
            }
288
289
            foreach ($var as $key => $_) {
290
                $child = new ArrayContext($key);
291
                $child->depth = $cdepth + 1;
292
                $child->reference = null !== ReflectionReference::fromArrayElement($var, $key);
293
294
                if (null !== $ap) {
295
                    $child->access_path = $ap.'['.\var_export($key, true).']';
296
                }
297
298
                $contents[$key] = $this->parse($var[$key], $child);
299
            }
300
301
            $array = new ArrayValue($c, $size, $contents);
302
303
            if ($contents) {
304
                $array->addRepresentation(new ContainerRepresentation('Contents', $contents, null, true));
305
            }
306
307
            return $this->applyPluginsComplete($var, $array, self::TRIGGER_SUCCESS);
308
        } finally {
309
            unset($this->array_ref_stack[$parentRef]);
310
        }
311
    }
312
313
    /**
314
     * @psalm-return ReflectionProperty[]
315
     */
316
    private function getPropsOrdered(ReflectionClass $r): array
317
    {
318
        if ($parent = $r->getParentClass()) {
319
            $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

319
            /** @scrutinizer ignore-call */ 
320
            $props = self::getPropsOrdered($parent);
Loading history...
320
        } else {
321
            $props = [];
322
        }
323
324
        foreach ($r->getProperties() as $prop) {
325
            if ($prop->isStatic()) {
326
                continue;
327
            }
328
329
            if ($prop->isPrivate()) {
330
                $props[] = $prop;
331
            } else {
332
                $props[$prop->name] = $prop;
333
            }
334
        }
335
336
        return $props;
337
    }
338
339
    /**
340
     * @codeCoverageIgnore
341
     *
342
     * @psalm-return ReflectionProperty[]
343
     */
344
    private function getPropsOrderedOld(ReflectionClass $r): array
345
    {
346
        $props = [];
347
348
        foreach ($r->getProperties() as $prop) {
349
            if ($prop->isStatic()) {
350
                continue;
351
            }
352
353
            $props[] = $prop;
354
        }
355
356
        while ($r = $r->getParentClass()) {
357
            foreach ($r->getProperties(ReflectionProperty::IS_PRIVATE) as $prop) {
358
                if ($prop->isStatic()) {
359
                    continue;
360
                }
361
362
                $props[] = $prop;
363
            }
364
        }
365
366
        return $props;
367
    }
368
369
    private function parseObject(object &$var, ContextInterface $c): AbstractValue
370
    {
371
        $hash = \spl_object_hash($var);
372
        $classname = \get_class($var);
373
374
        if (isset($this->object_hashes[$hash])) {
375
            $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var));
376
            $object->flags |= AbstractValue::FLAG_RECURSION;
377
378
            return $this->applyPluginsComplete($var, $object, self::TRIGGER_RECURSION);
379
        }
380
381
        try {
382
            $this->object_hashes[$hash] = true;
383
384
            $cdepth = $c->getDepth();
385
            $ap = $c->getAccessPath();
386
387
            if ($this->depth_limit && $cdepth >= $this->depth_limit) {
388
                $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var));
389
                $object->flags |= AbstractValue::FLAG_DEPTH_LIMIT;
390
391
                return $this->applyPluginsComplete($var, $object, self::TRIGGER_DEPTH_LIMIT);
392
            }
393
394
            if (KINT_PHP81) {
395
                $props = $this->getPropsOrdered(new ReflectionObject($var));
396
            } else {
397
                $props = $this->getPropsOrderedOld(new ReflectionObject($var)); // @codeCoverageIgnore
398
            }
399
400
            $values = (array) $var;
401
            $properties = [];
402
403
            foreach ($props as $rprop) {
404
                $rprop->setAccessible(true);
405
                $name = $rprop->getName();
406
407
                // Casting object to array:
408
                // private properties show in the form "\0$owner_class_name\0$property_name";
409
                // protected properties show in the form "\0*\0$property_name";
410
                // public properties show in the form "$property_name";
411
                // http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
412
                $key = $name;
413
                if ($rprop->isProtected()) {
414
                    $key = "\0*\0".$name;
415
                } elseif ($rprop->isPrivate()) {
416
                    $key = "\0".$rprop->getDeclaringClass()->getName()."\0".$name;
417
                }
418
                $initialized = \array_key_exists($key, $values);
419
                if ($key === (string) (int) $key) {
420
                    $key = (int) $key;
421
                }
422
423
                if ($rprop->isDefault()) {
424
                    $child = new PropertyContext(
425
                        $name,
426
                        $rprop->getDeclaringClass()->getName(),
427
                        ClassDeclaredContext::ACCESS_PUBLIC
428
                    );
429
430
                    $child->readonly = KINT_PHP81 && $rprop->isReadOnly();
431
432
                    if ($rprop->isProtected()) {
433
                        $child->access = ClassDeclaredContext::ACCESS_PROTECTED;
434
                    } elseif ($rprop->isPrivate()) {
435
                        $child->access = ClassDeclaredContext::ACCESS_PRIVATE;
436
                    }
437
438
                    if (KINT_PHP84) {
439
                        if ($rprop->isProtectedSet()) {
440
                            $child->access_set = ClassDeclaredContext::ACCESS_PROTECTED;
441
                        } elseif ($rprop->isPrivateSet()) {
442
                            $child->access_set = ClassDeclaredContext::ACCESS_PRIVATE;
443
                        }
444
445
                        $hooks = $rprop->getHooks();
446
                        if (isset($hooks['get'])) {
447
                            $child->hooks |= PropertyContext::HOOK_GET;
448
                            if ($hooks['get']->returnsReference()) {
449
                                $child->hooks |= PropertyContext::HOOK_GET_REF;
450
                            }
451
                        }
452
                        if (isset($hooks['set'])) {
453
                            $child->hooks |= PropertyContext::HOOK_SET;
454
455
                            $child->hook_set_type = (string) $rprop->getSettableType();
456
                            if ($child->hook_set_type !== (string) $rprop->getType()) {
457
                                $child->hooks |= PropertyContext::HOOK_SET_TYPE;
458
                            } elseif ('' === $child->hook_set_type) {
459
                                $child->hook_set_type = null;
460
                            }
461
                        }
462
                    }
463
                } else {
464
                    $child = new ClassOwnedContext($name, $rprop->getDeclaringClass()->getName());
465
                }
466
467
                $child->reference = $initialized && null !== ReflectionReference::fromArrayElement($values, $key);
468
                $child->depth = $cdepth + 1;
469
470
                if (null !== $ap && $child->isAccessible($this->caller_class)) {
471
                    /** @psalm-var string $child->name */
472
                    if (Utils::isValidPhpName($child->name)) {
473
                        $child->access_path = $ap.'->'.$child->name;
474
                    } else {
475
                        $child->access_path = $ap.'->{'.\var_export($child->name, true).'}';
476
                    }
477
                }
478
479
                if (KINT_PHP84 && $rprop->isVirtual()) {
480
                    $properties[] = new VirtualValue($child);
481
                } elseif (!$initialized) {
482
                    $properties[] = new UninitializedValue($child);
483
                } else {
484
                    $properties[] = $this->parse($values[$key], $child);
485
                }
486
            }
487
488
            $object = new InstanceValue($c, $classname, $hash, \spl_object_id($var));
489
            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...
490
                $object->setChildren($properties);
491
            }
492
493
            if ($properties) {
494
                $object->addRepresentation(new ContainerRepresentation('Properties', $properties));
495
            }
496
497
            return $this->applyPluginsComplete($var, $object, self::TRIGGER_SUCCESS);
498
        } finally {
499
            unset($this->object_hashes[$hash]);
500
        }
501
    }
502
503
    /**
504
     * @psalm-param resource $var
505
     */
506
    private function parseResource(&$var, ContextInterface $c): AbstractValue
507
    {
508
        $resource = new ResourceValue($c, \get_resource_type($var));
509
510
        $resource = $this->applyPluginsComplete($var, $resource, self::TRIGGER_SUCCESS);
511
512
        return $resource;
513
    }
514
515
    /**
516
     * @psalm-param mixed $var
517
     */
518
    private function parseResourceClosed(&$var, ContextInterface $c): AbstractValue
519
    {
520
        $v = new ClosedResourceValue($c);
521
522
        $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS);
523
524
        return $v;
525
    }
526
527
    /**
528
     * Catch-all for any unexpectedgettype.
529
     *
530
     * This should never happen.
531
     *
532
     * @codeCoverageIgnore
533
     *
534
     * @psalm-param mixed $var
535
     */
536
    private function parseUnknown(&$var, ContextInterface $c): AbstractValue
537
    {
538
        $v = new UnknownValue($c);
539
540
        $v = $this->applyPluginsComplete($var, $v, self::TRIGGER_SUCCESS);
541
542
        return $v;
543
    }
544
545
    /**
546
     * Applies plugins for a yet-unparsed value.
547
     *
548
     * @param mixed &$var The input variable
549
     */
550
    private function applyPluginsBegin(&$var, ContextInterface $c, string $type): ?AbstractValue
551
    {
552
        $plugins = $this->plugins[$type][self::TRIGGER_BEGIN] ?? [];
553
554
        foreach ($plugins as $plugin) {
555
            try {
556
                if ($v = $plugin->parseBegin($var, $c)) {
557
                    return $v;
558
                }
559
            } catch (Throwable $e) {
560
                \trigger_error(
561
                    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()),
562
                    E_USER_WARNING
563
                );
564
            }
565
        }
566
567
        return null;
568
    }
569
570
    /**
571
     * Applies plugins for a parsed AbstractValue.
572
     *
573
     * @param mixed &$var The input variable
574
     */
575
    private function applyPluginsComplete(&$var, AbstractValue $v, int $trigger): AbstractValue
576
    {
577
        $plugins = $this->plugins[$v->getType()][$trigger] ?? [];
578
579
        foreach ($plugins as $plugin) {
580
            try {
581
                $v = $plugin->parseComplete($var, $v, $trigger);
582
            } catch (Throwable $e) {
583
                \trigger_error(
584
                    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()),
585
                    E_USER_WARNING
586
                );
587
            }
588
        }
589
590
        return $v;
591
    }
592
}
593