Passed
Push — master ( ba645d...9eba8c )
by Michael
07:09 queued 43s
created

Parser   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 565
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 241
c 1
b 0
f 0
dl 0
loc 565
rs 2
wmc 82

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
B parseArray() 0 83 11
B parse() 0 24 10
A parseUnknown() 0 6 1
A parseGeneric() 0 11 1
A haltParse() 0 3 1
A getDepthLimit() 0 3 1
A applyPlugins() 0 33 5
B addPlugin() 0 30 7
A clearPlugins() 0 3 1
A noRecurseCall() 0 15 6
A parseString() 0 17 1
A getCallerClass() 0 3 1
A setDepthLimit() 0 5 1
A setCallerClass() 0 5 1
A parseResource() 0 9 1
A parseDeep() 0 10 1
C parseObject() 0 103 15
C childHasPath() 0 27 14
A getCleanArray() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2013 Jonathan Vollebregt ([email protected]), Rokas Šleinius ([email protected])
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
9
 * this software and associated documentation files (the "Software"), to deal in
10
 * the Software without restriction, including without limitation the rights to
11
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12
 * the Software, and to permit persons to whom the Software is furnished to do so,
13
 * subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in all
16
 * copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
 */
25
26
namespace Kint\Parser;
27
28
use DomainException;
29
use Exception;
30
use Kint\Object\BasicObject;
31
use Kint\Object\BlobObject;
32
use Kint\Object\InstanceObject;
33
use Kint\Object\Representation\Representation;
34
use Kint\Object\ResourceObject;
35
use ReflectionObject;
36
use stdClass;
37
38
class Parser
39
{
40
    /**
41
     * Plugin triggers.
42
     *
43
     * These are constants indicating trigger points for plugins
44
     *
45
     * BEGIN: Before normal parsing
46
     * SUCCESS: After successful parsing
47
     * RECURSION: After parsing cancelled by recursion
48
     * DEPTH_LIMIT: After parsing cancelled by depth limit
49
     * COMPLETE: SUCCESS | RECURSION | DEPTH_LIMIT
50
     *
51
     * While a plugin's getTriggers may return any of these
52
     */
53
    const TRIGGER_NONE = 0;
54
    const TRIGGER_BEGIN = 1;
55
    const TRIGGER_SUCCESS = 2;
56
    const TRIGGER_RECURSION = 4;
57
    const TRIGGER_DEPTH_LIMIT = 8;
58
    const TRIGGER_COMPLETE = 14;
59
60
    protected $caller_class;
61
    protected $depth_limit = false;
62
    protected $marker;
63
    protected $object_hashes = array();
64
    protected $parse_break = false;
65
    protected $plugins = array();
66
67
    /**
68
     * @param false|int   $depth_limit Maximum depth to parse data
69
     * @param null|string $caller      Caller class name
70
     */
71
    public function __construct($depth_limit = false, $caller = null)
72
    {
73
        $this->marker = \uniqid("kint\0", true);
74
75
        $this->caller_class = $caller;
76
77
        if ($depth_limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $depth_limit of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
78
            $this->depth_limit = $depth_limit;
79
        }
80
    }
81
82
    /**
83
     * Set the caller class.
84
     *
85
     * @param null|string $caller Caller class name
86
     */
87
    public function setCallerClass($caller = null)
88
    {
89
        $this->noRecurseCall();
90
91
        $this->caller_class = $caller;
92
    }
93
94
    public function getCallerClass()
95
    {
96
        return $this->caller_class;
97
    }
98
99
    /**
100
     * Set the depth limit.
101
     *
102
     * @param false|int $depth_limit Maximum depth to parse data
103
     */
104
    public function setDepthLimit($depth_limit = false)
105
    {
106
        $this->noRecurseCall();
107
108
        $this->depth_limit = $depth_limit;
109
    }
110
111
    public function getDepthLimit()
112
    {
113
        return $this->depth_limit;
114
    }
115
116
    /**
117
     * Disables the depth limit and parses a variable.
118
     *
119
     * This should not be used unless you know what you're doing!
120
     *
121
     * @param mixed       $var The input variable
122
     * @param BasicObject $o   The base object
123
     *
124
     * @return BasicObject
125
     */
126
    public function parseDeep(&$var, BasicObject $o)
127
    {
128
        $depth_limit = $this->depth_limit;
129
        $this->depth_limit = false;
130
131
        $out = $this->parse($var, $o);
132
133
        $this->depth_limit = $depth_limit;
134
135
        return $out;
136
    }
137
138
    /**
139
     * Parses a variable into a Kint object structure.
140
     *
141
     * @param mixed       $var The input variable
142
     * @param BasicObject $o   The base object
143
     *
144
     * @return BasicObject
145
     */
146
    public function parse(&$var, BasicObject $o)
147
    {
148
        $o->type = \strtolower(\gettype($var));
149
150
        if (!$this->applyPlugins($var, $o, self::TRIGGER_BEGIN)) {
151
            return $o;
152
        }
153
154
        switch ($o->type) {
155
            case 'array':
156
                return $this->parseArray($var, $o);
157
            case 'boolean':
158
            case 'double':
159
            case 'integer':
160
            case 'null':
161
                return $this->parseGeneric($var, $o);
162
            case 'object':
163
                return $this->parseObject($var, $o);
164
            case 'resource':
165
                return $this->parseResource($var, $o);
166
            case 'string':
167
                return $this->parseString($var, $o);
168
            default:
169
                return $this->parseUnknown($var, $o);
170
        }
171
    }
172
173
    public function addPlugin(Plugin $p)
174
    {
175
        if (!$types = $p->getTypes()) {
176
            return false;
177
        }
178
179
        if (!$triggers = $p->getTriggers()) {
180
            return false;
181
        }
182
183
        $p->setParser($this);
184
185
        foreach ($types as $type) {
186
            if (!isset($this->plugins[$type])) {
187
                $this->plugins[$type] = array(
188
                    self::TRIGGER_BEGIN => array(),
189
                    self::TRIGGER_SUCCESS => array(),
190
                    self::TRIGGER_RECURSION => array(),
191
                    self::TRIGGER_DEPTH_LIMIT => array(),
192
                );
193
            }
194
195
            foreach ($this->plugins[$type] as $trigger => &$pool) {
196
                if ($triggers & $trigger) {
197
                    $pool[] = $p;
198
                }
199
            }
200
        }
201
202
        return true;
203
    }
204
205
    public function clearPlugins()
206
    {
207
        $this->plugins = array();
208
    }
209
210
    public function haltParse()
211
    {
212
        $this->parse_break = true;
213
    }
214
215
    public function childHasPath(InstanceObject $parent, BasicObject $child)
216
    {
217
        if ('object' === $parent->type && (null !== $parent->access_path || $child->static || $child->const)) {
218
            if (BasicObject::ACCESS_PUBLIC === $child->access) {
219
                return true;
220
            }
221
222
            if (BasicObject::ACCESS_PRIVATE === $child->access && $this->caller_class) {
223
                if ($this->caller_class === $child->owner_class) {
224
                    return true;
225
                }
226
            } elseif (BasicObject::ACCESS_PROTECTED === $child->access && $this->caller_class) {
227
                if ($this->caller_class === $child->owner_class) {
228
                    return true;
229
                }
230
231
                if (\is_subclass_of($this->caller_class, $child->owner_class)) {
232
                    return true;
233
                }
234
235
                if (\is_subclass_of($child->owner_class, $this->caller_class)) {
236
                    return true;
237
                }
238
            }
239
        }
240
241
        return false;
242
    }
243
244
    /**
245
     * Returns an array without the recursion marker in it.
246
     *
247
     * DO NOT pass an array that has had it's marker removed back
248
     * into the parser, it will result in an extra recursion
249
     *
250
     * @param array $array Array potentially containing a recursion marker
251
     *
252
     * @return array Array with recursion marker removed
253
     */
254
    public function getCleanArray(array $array)
255
    {
256
        unset($array[$this->marker]);
257
258
        return $array;
259
    }
260
261
    protected function noRecurseCall()
262
    {
263
        $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS);
264
265
        $caller_frame = array(
266
            'function' => __FUNCTION__,
267
        );
268
269
        while (isset($bt[0]['object']) && $bt[0]['object'] === $this) {
270
            $caller_frame = \array_shift($bt);
271
        }
272
273
        foreach ($bt as $frame) {
274
            if (isset($frame['object']) && $frame['object'] === $this) {
275
                throw new DomainException(__CLASS__.'::'.$caller_frame['function'].' cannot be called from inside a parse');
276
            }
277
        }
278
    }
279
280
    private function parseGeneric(&$var, BasicObject $o)
281
    {
282
        $rep = new Representation('Contents');
283
        $rep->contents = $var;
284
        $rep->implicit_label = true;
285
        $o->addRepresentation($rep);
286
        $o->value = $rep;
287
288
        $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS);
289
290
        return $o;
291
    }
292
293
    /**
294
     * Parses a string into a Kint BlobObject structure.
295
     *
296
     * @param string      $var The input variable
297
     * @param BasicObject $o   The base object
298
     *
299
     * @return BasicObject
300
     */
301
    private function parseString(&$var, BasicObject $o)
302
    {
303
        $string = new BlobObject();
304
        $string->transplant($o);
305
        $string->encoding = BlobObject::detectEncoding($var);
306
        $string->size = BlobObject::strlen($var, $string->encoding);
307
308
        $rep = new Representation('Contents');
309
        $rep->contents = $var;
310
        $rep->implicit_label = true;
311
312
        $string->addRepresentation($rep);
313
        $string->value = $rep;
314
315
        $this->applyPlugins($var, $string, self::TRIGGER_SUCCESS);
316
317
        return $string;
318
    }
319
320
    /**
321
     * Parses an array into a Kint object structure.
322
     *
323
     * @param array       $var The input variable
324
     * @param BasicObject $o   The base object
325
     *
326
     * @return BasicObject
327
     */
328
    private function parseArray(array &$var, BasicObject $o)
329
    {
330
        $array = new BasicObject();
331
        $array->transplant($o);
332
        $array->size = \count($var);
333
334
        if (isset($var[$this->marker])) {
335
            --$array->size;
336
            $array->hints[] = 'recursion';
337
338
            $this->applyPlugins($var, $array, self::TRIGGER_RECURSION);
339
340
            return $array;
341
        }
342
343
        $rep = new Representation('Contents');
344
        $rep->implicit_label = true;
345
        $array->addRepresentation($rep);
346
        $array->value = $rep;
347
348
        if (!$array->size) {
349
            $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS);
350
351
            return $array;
352
        }
353
354
        if ($this->depth_limit && $o->depth >= $this->depth_limit) {
355
            $array->hints[] = 'depth_limit';
356
357
            $this->applyPlugins($var, $array, self::TRIGGER_DEPTH_LIMIT);
358
359
            return $array;
360
        }
361
362
        $copy = \array_values($var);
363
364
        // It's really really hard to access numeric string keys in arrays,
365
        // and it's really really hard to access integer properties in
366
        // objects, so we just use array_values and index by counter to get
367
        // at it reliably for reference testing. This also affects access
368
        // paths since it's pretty much impossible to access these things
369
        // without complicated stuff you should never need to do.
370
        $i = 0;
371
372
        // Set the marker for recursion
373
        $var[$this->marker] = $array->depth;
374
375
        $refmarker = new stdClass();
376
377
        foreach ($var as $key => &$val) {
378
            if ($key === $this->marker) {
379
                continue;
380
            }
381
382
            $child = new BasicObject();
383
            $child->name = $key;
384
            $child->depth = $array->depth + 1;
385
            $child->access = BasicObject::ACCESS_NONE;
386
            $child->operator = BasicObject::OPERATOR_ARRAY;
387
388
            if (null !== $array->access_path) {
389
                if (\is_string($key) && (string) (int) $key === $key) {
390
                    $child->access_path = 'array_values('.$array->access_path.')['.$i.']'; // @codeCoverageIgnore
391
                } else {
392
                    $child->access_path = $array->access_path.'['.\var_export($key, true).']';
393
                }
394
            }
395
396
            $stash = $val;
397
            $copy[$i] = $refmarker;
398
            if ($val === $refmarker) {
399
                $child->reference = true;
400
                $val = $stash;
401
            }
402
403
            $rep->contents[] = $this->parse($val, $child);
404
            ++$i;
405
        }
406
407
        $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS);
408
        unset($var[$this->marker]);
409
410
        return $array;
411
    }
412
413
    /**
414
     * Parses an object into a Kint InstanceObject structure.
415
     *
416
     * @param object      $var The input variable
417
     * @param BasicObject $o   The base object
418
     *
419
     * @return BasicObject
420
     */
421
    private function parseObject(&$var, BasicObject $o)
422
    {
423
        $hash = \spl_object_hash($var);
424
        $values = (array) $var;
425
426
        $object = new InstanceObject();
427
        $object->transplant($o);
428
        $object->classname = \get_class($var);
429
        $object->hash = $hash;
430
        $object->size = \count($values);
431
432
        if (isset($this->object_hashes[$hash])) {
433
            $object->hints[] = 'recursion';
434
435
            $this->applyPlugins($var, $object, self::TRIGGER_RECURSION);
436
437
            return $object;
438
        }
439
440
        $this->object_hashes[$hash] = $object;
441
442
        if ($this->depth_limit && $o->depth >= $this->depth_limit) {
443
            $object->hints[] = 'depth_limit';
444
445
            $this->applyPlugins($var, $object, self::TRIGGER_DEPTH_LIMIT);
446
            unset($this->object_hashes[$hash]);
447
448
            return $object;
449
        }
450
451
        $reflector = new ReflectionObject($var);
452
453
        if ($reflector->isUserDefined()) {
454
            $object->filename = $reflector->getFileName();
455
            $object->startline = $reflector->getStartLine();
456
        }
457
458
        $rep = new Representation('Properties');
459
460
        $copy = \array_values($values);
461
        $refmarker = new stdClass();
462
        $i = 0;
463
464
        // Reflection will not show parent classes private properties, and if a
465
        // property was unset it will happly trigger a notice looking for it.
466
        foreach ($values as $key => &$val) {
467
            // Casting object to array:
468
            // private properties show in the form "\0$owner_class_name\0$property_name";
469
            // protected properties show in the form "\0*\0$property_name";
470
            // public properties show in the form "$property_name";
471
            // http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
472
473
            $child = new BasicObject();
474
            $child->depth = $object->depth + 1;
475
            $child->owner_class = $object->classname;
476
            $child->operator = BasicObject::OPERATOR_OBJECT;
477
            $child->access = BasicObject::ACCESS_PUBLIC;
478
479
            $split_key = \explode("\0", $key, 3);
480
481
            if (3 === \count($split_key) && '' === $split_key[0]) {
482
                $child->name = $split_key[2];
483
                if ('*' === $split_key[1]) {
484
                    $child->access = BasicObject::ACCESS_PROTECTED;
485
                } else {
486
                    $child->access = BasicObject::ACCESS_PRIVATE;
487
                    $child->owner_class = $split_key[1];
488
                }
489
            } elseif (KINT_PHP72) {
490
                $child->name = (string) $key;
491
            } else {
492
                $child->name = $key; // @codeCoverageIgnore
493
            }
494
495
            if ($this->childHasPath($object, $child)) {
496
                $child->access_path = $object->access_path;
497
498
                if (!KINT_PHP72 && \is_int($child->name)) {
499
                    $child->access_path = 'array_values((array) '.$child->access_path.')['.$i.']'; // @codeCoverageIgnore
500
                } elseif (\preg_match('/^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*$/', $child->name)) {
501
                    $child->access_path .= '->'.$child->name;
502
                } else {
503
                    $child->access_path .= '->{'.\var_export((string) $child->name, true).'}';
504
                }
505
            }
506
507
            $stash = $val;
508
            $copy[$i] = $refmarker;
509
            if ($val === $refmarker) {
510
                $child->reference = true;
511
                $val = $stash;
512
            }
513
514
            $rep->contents[] = $this->parse($val, $child);
515
            ++$i;
516
        }
517
518
        $object->addRepresentation($rep);
519
        $object->value = $rep;
520
        $this->applyPlugins($var, $object, self::TRIGGER_SUCCESS);
521
        unset($this->object_hashes[$hash]);
522
523
        return $object;
524
    }
525
526
    /**
527
     * Parses a resource into a Kint ResourceObject structure.
528
     *
529
     * @param resource    $var The input variable
530
     * @param BasicObject $o   The base object
531
     *
532
     * @return BasicObject
533
     */
534
    private function parseResource(&$var, BasicObject $o)
535
    {
536
        $resource = new ResourceObject();
537
        $resource->transplant($o);
538
        $resource->resource_type = \get_resource_type($var);
539
540
        $this->applyPlugins($var, $resource, self::TRIGGER_SUCCESS);
541
542
        return $resource;
543
    }
544
545
    /**
546
     * Parses an unknown into a Kint object structure.
547
     *
548
     * @param mixed       $var The input variable
549
     * @param BasicObject $o   The base object
550
     *
551
     * @return BasicObject
552
     */
553
    private function parseUnknown(&$var, BasicObject $o)
554
    {
555
        $o->type = 'unknown';
556
        $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS);
557
558
        return $o;
559
    }
560
561
    /**
562
     * Applies plugins for an object type.
563
     *
564
     * @param mixed       $var     variable
565
     * @param BasicObject $o       Kint object parsed so far
566
     * @param int         $trigger The trigger to check for the plugins
567
     *
568
     * @return bool Continue parsing
569
     */
570
    private function applyPlugins(&$var, BasicObject &$o, $trigger)
571
    {
572
        $break_stash = $this->parse_break;
573
574
        /** @var bool Psalm bug workaround */
575
        $this->parse_break = false;
576
577
        $plugins = array();
578
579
        if (isset($this->plugins[$o->type][$trigger])) {
580
            $plugins = $this->plugins[$o->type][$trigger];
581
        }
582
583
        foreach ($plugins as $plugin) {
584
            try {
585
                $plugin->parse($var, $o, $trigger);
586
            } catch (Exception $e) {
587
                \trigger_error(
588
                    'An exception ('.\get_class($e).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing Kint Parser Plugin "'.\get_class($plugin).'". Error message: '.$e->getMessage(),
589
                    E_USER_WARNING
590
                );
591
            }
592
593
            if ($this->parse_break) {
594
                $this->parse_break = $break_stash;
595
596
                return false;
597
            }
598
        }
599
600
        $this->parse_break = $break_stash;
601
602
        return true;
603
    }
604
}
605