1: | <?php |
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | |
23: | |
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: | |
42: | |
43: | |
44: | |
45: | |
46: | |
47: | |
48: | |
49: | |
50: | |
51: | |
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: | |
69: | |
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) { |
78: | $this->depth_limit = $depth_limit; |
79: | } |
80: | } |
81: | |
82: | |
83: | |
84: | |
85: | |
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: | |
101: | |
102: | |
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: | |
118: | |
119: | |
120: | |
121: | |
122: | |
123: | |
124: | |
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: | |
140: | |
141: | |
142: | |
143: | |
144: | |
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: | |
246: | |
247: | |
248: | |
249: | |
250: | |
251: | |
252: | |
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: | |
295: | |
296: | |
297: | |
298: | |
299: | |
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: | |
322: | |
323: | |
324: | |
325: | |
326: | |
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: | |
365: | |
366: | |
367: | |
368: | |
369: | |
370: | $i = 0; |
371: | |
372: | |
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.']'; |
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: | |
415: | |
416: | |
417: | |
418: | |
419: | |
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: | |
465: | |
466: | foreach ($values as $key => &$val) { |
467: | |
468: | |
469: | |
470: | |
471: | |
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; |
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.']'; |
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: | |
528: | |
529: | |
530: | |
531: | |
532: | |
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: | |
547: | |
548: | |
549: | |
550: | |
551: | |
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: | |
563: | |
564: | |
565: | |
566: | |
567: | |
568: | |
569: | |
570: | private function applyPlugins(&$var, BasicObject &$o, $trigger) |
571: | { |
572: | $break_stash = $this->parse_break; |
573: | |
574: | |
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: | |