1: <?php
2:
3: /*
4: * The MIT License (MIT)
5: *
6: * Copyright (c) 2013 Jonathan Vollebregt (jnvsor@gmail.com), Rokas Šleinius (raveren@gmail.com)
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;
27:
28: use InvalidArgumentException;
29: use Kint\Object\BasicObject;
30: use Kint\Parser\Parser;
31: use Kint\Parser\Plugin;
32: use Kint\Renderer\Renderer;
33: use Kint\Renderer\TextRenderer;
34:
35: class Kint
36: {
37: const MODE_RICH = 'r';
38: const MODE_TEXT = 't';
39: const MODE_CLI = 'c';
40: const MODE_PLAIN = 'p';
41:
42: /**
43: * @var mixed Kint mode
44: *
45: * false: Disabled
46: * true: Enabled, default mode selection
47: * other: Manual mode selection
48: */
49: public static $enabled_mode = true;
50:
51: /**
52: * Default mode.
53: *
54: * @var string
55: */
56: public static $mode_default = self::MODE_RICH;
57:
58: /**
59: * Default mode in CLI with cli_detection on.
60: *
61: * @var string
62: */
63: public static $mode_default_cli = self::MODE_CLI;
64:
65: /**
66: * @var bool Return output instead of echoing
67: */
68: public static $return;
69:
70: /**
71: * @var string format of the link to the source file in trace entries.
72: *
73: * Use %f for file path, %l for line number.
74: *
75: * [!] EXAMPLE (works with for phpStorm and RemoteCall Plugin):
76: *
77: * Kint::$file_link_format = 'http://localhost:8091/?message=%f:%l';
78: */
79: public static $file_link_format = '';
80:
81: /**
82: * @var bool whether to display where kint was called from
83: */
84: public static $display_called_from = true;
85:
86: /**
87: * @var array base directories of your application that will be displayed instead of the full path.
88: *
89: * Keys are paths, values are replacement strings
90: *
91: * [!] EXAMPLE (for Laravel 5):
92: *
93: * Kint::$app_root_dirs = [
94: * base_path() => '<BASE>',
95: * app_path() => '<APP>',
96: * config_path() => '<CONFIG>',
97: * database_path() => '<DATABASE>',
98: * public_path() => '<PUBLIC>',
99: * resource_path() => '<RESOURCE>',
100: * storage_path() => '<STORAGE>',
101: * ];
102: *
103: * Defaults to [$_SERVER['DOCUMENT_ROOT'] => '<ROOT>']
104: */
105: public static $app_root_dirs = array();
106:
107: /**
108: * @var int max array/object levels to go deep, if zero no limits are applied
109: */
110: public static $max_depth = 6;
111:
112: /**
113: * @var bool expand all trees by default for rich view
114: */
115: public static $expanded = false;
116:
117: /**
118: * @var bool enable detection when Kint is command line.
119: *
120: * Formats output with whitespace only; does not HTML-escape it
121: */
122: public static $cli_detection = true;
123:
124: /**
125: * @var array Kint aliases. Add debug functions in Kint wrappers here to fix modifiers and backtraces
126: */
127: public static $aliases = array(
128: array('Kint\\Kint', 'dump'),
129: array('Kint\\Kint', 'trace'),
130: array('Kint\\Kint', 'dumpArray'),
131: );
132:
133: /**
134: * @var array<mixed, string> Array of modes to renderer class names
135: */
136: public static $renderers = array(
137: self::MODE_RICH => 'Kint\\Renderer\\RichRenderer',
138: self::MODE_PLAIN => 'Kint\\Renderer\\PlainRenderer',
139: self::MODE_TEXT => 'Kint\\Renderer\\TextRenderer',
140: self::MODE_CLI => 'Kint\\Renderer\\CliRenderer',
141: );
142:
143: public static $plugins = array(
144: 'Kint\\Parser\\ArrayObjectPlugin',
145: 'Kint\\Parser\\Base64Plugin',
146: 'Kint\\Parser\\BlacklistPlugin',
147: 'Kint\\Parser\\ClassMethodsPlugin',
148: 'Kint\\Parser\\ClassStaticsPlugin',
149: 'Kint\\Parser\\ClosurePlugin',
150: 'Kint\\Parser\\ColorPlugin',
151: 'Kint\\Parser\\DateTimePlugin',
152: 'Kint\\Parser\\FsPathPlugin',
153: 'Kint\\Parser\\IteratorPlugin',
154: 'Kint\\Parser\\JsonPlugin',
155: 'Kint\\Parser\\MicrotimePlugin',
156: 'Kint\\Parser\\SimpleXMLElementPlugin',
157: 'Kint\\Parser\\SplFileInfoPlugin',
158: 'Kint\\Parser\\SplObjectStoragePlugin',
159: 'Kint\\Parser\\StreamPlugin',
160: 'Kint\\Parser\\TablePlugin',
161: 'Kint\\Parser\\ThrowablePlugin',
162: 'Kint\\Parser\\TimestampPlugin',
163: 'Kint\\Parser\\TracePlugin',
164: 'Kint\\Parser\\XmlPlugin',
165: );
166:
167: protected static $plugin_pool = array();
168:
169: protected $parser;
170: protected $renderer;
171:
172: public function __construct(Parser $p, Renderer $r)
173: {
174: $this->parser = $p;
175: $this->renderer = $r;
176: }
177:
178: public function setParser(Parser $p)
179: {
180: $this->parser = $p;
181: }
182:
183: public function getParser()
184: {
185: return $this->parser;
186: }
187:
188: public function setRenderer(Renderer $r)
189: {
190: $this->renderer = $r;
191: }
192:
193: public function getRenderer()
194: {
195: return $this->renderer;
196: }
197:
198: public function setStatesFromStatics(array $statics)
199: {
200: $this->renderer->setStatics($statics);
201:
202: $this->parser->setDepthLimit(isset($statics['max_depth']) ? $statics['max_depth'] : false);
203: $this->parser->clearPlugins();
204:
205: if (!isset($statics['plugins'])) {
206: return;
207: }
208:
209: $plugins = array();
210:
211: foreach ($statics['plugins'] as $plugin) {
212: if ($plugin instanceof Plugin) {
213: $plugins[] = $plugin;
214: } elseif (\is_string($plugin) && \is_subclass_of($plugin, 'Kint\\Parser\\Plugin')) {
215: if (!isset(self::$plugin_pool[$plugin])) {
216: $p = new $plugin();
217: self::$plugin_pool[$plugin] = $p;
218: }
219: $plugins[] = self::$plugin_pool[$plugin];
220: }
221: }
222:
223: $plugins = $this->renderer->filterParserPlugins($plugins);
224:
225: foreach ($plugins as $plugin) {
226: $this->parser->addPlugin($plugin);
227: }
228: }
229:
230: public function setStatesFromCallInfo(array $info)
231: {
232: $this->renderer->setCallInfo($info);
233:
234: if (isset($info['modifiers']) && \is_array($info['modifiers']) && \in_array('+', $info['modifiers'], true)) {
235: $this->parser->setDepthLimit(false);
236: }
237:
238: $this->parser->setCallerClass(isset($info['caller']['class']) ? $info['caller']['class'] : null);
239: }
240:
241: /**
242: * Renders a list of vars including the pre and post renders.
243: *
244: * @param array $vars Data to dump
245: * @param BasicObject[] $base Base objects
246: *
247: * @return string
248: */
249: public function dumpAll(array $vars, array $base)
250: {
251: if (\array_keys($vars) !== \array_keys($base)) {
252: throw new InvalidArgumentException('Kint::dumpAll requires arrays of identical size and keys as arguments');
253: }
254:
255: $output = $this->renderer->preRender();
256:
257: if ($vars === array()) {
258: $output .= $this->renderer->renderNothing();
259: }
260:
261: foreach ($vars as $key => $arg) {
262: if (!$base[$key] instanceof BasicObject) {
263: throw new InvalidArgumentException('Kint::dumpAll requires all elements of the second argument to be BasicObject instances');
264: }
265: $output .= $this->dumpVar($arg, $base[$key]);
266: }
267:
268: $output .= $this->renderer->postRender();
269:
270: return $output;
271: }
272:
273: /**
274: * Dumps and renders a var.
275: *
276: * @param mixed $var Data to dump
277: * @param BasicObject $base Base object
278: *
279: * @return string
280: */
281: public function dumpVar(&$var, BasicObject $base)
282: {
283: return $this->renderer->render(
284: $this->parser->parse($var, $base)
285: );
286: }
287:
288: /**
289: * Gets all static settings at once.
290: *
291: * @return array Current static settings
292: */
293: public static function getStatics()
294: {
295: return array(
296: 'aliases' => self::$aliases,
297: 'app_root_dirs' => self::$app_root_dirs,
298: 'cli_detection' => self::$cli_detection,
299: 'display_called_from' => self::$display_called_from,
300: 'enabled_mode' => self::$enabled_mode,
301: 'expanded' => self::$expanded,
302: 'file_link_format' => self::$file_link_format,
303: 'max_depth' => self::$max_depth,
304: 'mode_default' => self::$mode_default,
305: 'mode_default_cli' => self::$mode_default_cli,
306: 'plugins' => self::$plugins,
307: 'renderers' => self::$renderers,
308: 'return' => self::$return,
309: );
310: }
311:
312: /**
313: * Creates a Kint instances based on static settings.
314: *
315: * Also calls setStatesFromStatics for you
316: *
317: * @param array $statics array of statics as returned by getStatics
318: *
319: * @return null|\Kint\Kint
320: */
321: public static function createFromStatics(array $statics)
322: {
323: $mode = false;
324:
325: if (isset($statics['enabled_mode'])) {
326: $mode = $statics['enabled_mode'];
327:
328: if (true === $statics['enabled_mode'] && isset($statics['mode_default'])) {
329: $mode = $statics['mode_default'];
330:
331: if (PHP_SAPI === 'cli' && !empty($statics['cli_detection']) && isset($statics['mode_default_cli'])) {
332: $mode = $statics['mode_default_cli'];
333: }
334: }
335: }
336:
337: if (!$mode) {
338: return null;
339: }
340:
341: if (!isset($statics['renderers'][$mode])) {
342: $renderer = new TextRenderer();
343: } else {
344: /** @var Renderer */
345: $renderer = new $statics['renderers'][$mode]();
346: }
347:
348: return new self(new Parser(), $renderer);
349: }
350:
351: /**
352: * Creates base objects given parameter info.
353: *
354: * @param array $params Parameters as returned from getCallInfo
355: * @param int $argc Number of arguments the helper was called with
356: *
357: * @return BasicObject[] Base objects for the arguments
358: */
359: public static function getBasesFromParamInfo(array $params, $argc)
360: {
361: static $blacklist = array(
362: 'null',
363: 'true',
364: 'false',
365: 'array(...)',
366: 'array()',
367: '[...]',
368: '[]',
369: '(...)',
370: '()',
371: '"..."',
372: 'b"..."',
373: "'...'",
374: "b'...'",
375: );
376:
377: $params = \array_values($params);
378: $bases = array();
379:
380: for ($i = 0; $i < $argc; ++$i) {
381: if (isset($params[$i])) {
382: $param = $params[$i];
383: } else {
384: $param = null;
385: }
386:
387: if (!isset($param['name']) || \is_numeric($param['name'])) {
388: $name = null;
389: } elseif (\in_array(\strtolower($param['name']), $blacklist, true)) {
390: $name = null;
391: } else {
392: $name = $param['name'];
393: }
394:
395: if (isset($param['path'])) {
396: $access_path = $param['path'];
397:
398: if (!empty($param['expression'])) {
399: $access_path = '('.$access_path.')';
400: }
401: } else {
402: $access_path = '$'.$i;
403: }
404:
405: $bases[] = BasicObject::blank($name, $access_path);
406: }
407:
408: return $bases;
409: }
410:
411: /**
412: * Gets call info from the backtrace, alias, and argument count.
413: *
414: * Aliases must be normalized beforehand (Utils::normalizeAliases)
415: *
416: * @param array $aliases Call aliases as found in Kint::$aliases
417: * @param array[] $trace Backtrace
418: * @param int $argc Number of arguments
419: *
420: * @return array{params:null|array, modifiers:array, callee:null|array, caller:null|array, trace:array[]} Call info
421: */
422: public static function getCallInfo(array $aliases, array $trace, $argc)
423: {
424: $found = false;
425: $callee = null;
426: $caller = null;
427: $miniTrace = array();
428:
429: foreach ($trace as $index => $frame) {
430: if (Utils::traceFrameIsListed($frame, $aliases)) {
431: $found = true;
432: $miniTrace = array();
433: }
434:
435: if (!Utils::traceFrameIsListed($frame, array('spl_autoload_call'))) {
436: $miniTrace[] = $frame;
437: }
438: }
439:
440: if ($found) {
441: $callee = \reset($miniTrace) ?: null;
442:
443: /** @var null|array Psalm bug workaround */
444: $caller = \next($miniTrace) ?: null;
445: }
446:
447: foreach ($miniTrace as $index => $frame) {
448: if ((0 === $index && $callee === $frame) || isset($frame['file'], $frame['line'])) {
449: unset($frame['object'], $frame['args']);
450: $miniTrace[$index] = $frame;
451: } else {
452: unset($miniTrace[$index]);
453: }
454: }
455:
456: $miniTrace = \array_values($miniTrace);
457:
458: $call = self::getSingleCall($callee ?: array(), $argc);
459:
460: $ret = array(
461: 'params' => null,
462: 'modifiers' => array(),
463: 'callee' => $callee,
464: 'caller' => $caller,
465: 'trace' => $miniTrace,
466: );
467:
468: if ($call) {
469: $ret['params'] = $call['parameters'];
470: $ret['modifiers'] = $call['modifiers'];
471: }
472:
473: return $ret;
474: }
475:
476: /**
477: * Dumps a backtrace.
478: *
479: * Functionally equivalent to Kint::dump(1) or Kint::dump(debug_backtrace(true))
480: *
481: * @return int|string
482: */
483: public static function trace()
484: {
485: if (!self::$enabled_mode) {
486: return 0;
487: }
488:
489: Utils::normalizeAliases(self::$aliases);
490:
491: $args = \func_get_args();
492:
493: $call_info = self::getCallInfo(self::$aliases, \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), \count($args));
494:
495: $statics = self::getStatics();
496:
497: if (\in_array('~', $call_info['modifiers'], true)) {
498: $statics['enabled_mode'] = self::MODE_TEXT;
499: }
500:
501: $kintstance = self::createFromStatics($statics);
502: if (!$kintstance) {
503: // Should never happen
504: return 0; // @codeCoverageIgnore
505: }
506:
507: if (\in_array('-', $call_info['modifiers'], true)) {
508: while (\ob_get_level()) {
509: \ob_end_clean();
510: }
511: }
512:
513: $kintstance->setStatesFromStatics($statics);
514: $kintstance->setStatesFromCallInfo($call_info);
515:
516: $trimmed_trace = array();
517: $trace = \debug_backtrace(true);
518:
519: foreach ($trace as $frame) {
520: if (Utils::traceFrameIsListed($frame, self::$aliases)) {
521: $trimmed_trace = array();
522: }
523:
524: $trimmed_trace[] = $frame;
525: }
526:
527: $output = $kintstance->dumpAll(
528: array($trimmed_trace),
529: array(BasicObject::blank('Kint\\Kint::trace()', 'debug_backtrace(true)'))
530: );
531:
532: if (self::$return || \in_array('@', $call_info['modifiers'], true)) {
533: return $output;
534: }
535:
536: echo $output;
537:
538: if (\in_array('-', $call_info['modifiers'], true)) {
539: \flush(); // @codeCoverageIgnore
540: }
541:
542: return 0;
543: }
544:
545: /**
546: * Dumps some data.
547: *
548: * Functionally equivalent to Kint::dump(1) or Kint::dump(debug_backtrace(true))
549: *
550: * @return int|string
551: */
552: public static function dump()
553: {
554: if (!self::$enabled_mode) {
555: return 0;
556: }
557:
558: Utils::normalizeAliases(self::$aliases);
559:
560: $args = \func_get_args();
561:
562: $call_info = self::getCallInfo(self::$aliases, \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), \count($args));
563:
564: $statics = self::getStatics();
565:
566: if (\in_array('~', $call_info['modifiers'], true)) {
567: $statics['enabled_mode'] = self::MODE_TEXT;
568: }
569:
570: $kintstance = self::createFromStatics($statics);
571: if (!$kintstance) {
572: // Should never happen
573: return 0; // @codeCoverageIgnore
574: }
575:
576: if (\in_array('-', $call_info['modifiers'], true)) {
577: while (\ob_get_level()) {
578: \ob_end_clean();
579: }
580: }
581:
582: $kintstance->setStatesFromStatics($statics);
583: $kintstance->setStatesFromCallInfo($call_info);
584:
585: // If the call is Kint::dump(1) then dump a backtrace instead
586: if ($args === array(1) && (!isset($call_info['params'][0]['name']) || '1' === $call_info['params'][0]['name'])) {
587: $args = \debug_backtrace(true);
588: $trace = array();
589:
590: foreach ($args as $index => $frame) {
591: if (Utils::traceFrameIsListed($frame, self::$aliases)) {
592: $trace = array();
593: }
594:
595: $trace[] = $frame;
596: }
597:
598: if (isset($call_info['callee']['function'])) {
599: $tracename = $call_info['callee']['function'].'(1)';
600: if (isset($call_info['callee']['class'], $call_info['callee']['type'])) {
601: $tracename = $call_info['callee']['class'].$call_info['callee']['type'].$tracename;
602: }
603: } else {
604: $tracename = 'Kint\\Kint::dump(1)';
605: }
606:
607: $tracebase = BasicObject::blank($tracename, 'debug_backtrace(true)');
608:
609: $output = $kintstance->dumpAll(array($trace), array($tracebase));
610: } else {
611: $bases = self::getBasesFromParamInfo(
612: isset($call_info['params']) ? $call_info['params'] : array(),
613: \count($args)
614: );
615: $output = $kintstance->dumpAll($args, $bases);
616: }
617:
618: if (self::$return || \in_array('@', $call_info['modifiers'], true)) {
619: return $output;
620: }
621:
622: echo $output;
623:
624: if (\in_array('-', $call_info['modifiers'], true)) {
625: \flush(); // @codeCoverageIgnore
626: }
627:
628: return 0;
629: }
630:
631: /**
632: * generic path display callback, can be configured in app_root_dirs; purpose is
633: * to show relevant path info and hide as much of the path as possible.
634: *
635: * @param string $file
636: *
637: * @return string
638: */
639: public static function shortenPath($file)
640: {
641: $file = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', $file)), 'strlen'));
642:
643: $longest_match = 0;
644: $match = '/';
645:
646: foreach (self::$app_root_dirs as $path => $alias) {
647: if (empty($path)) {
648: continue;
649: }
650:
651: $path = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', $path)), 'strlen'));
652:
653: if (\array_slice($file, 0, \count($path)) === $path && \count($path) > $longest_match) {
654: $longest_match = \count($path);
655: $match = $alias;
656: }
657: }
658:
659: if ($longest_match) {
660: $file = \array_merge(array($match), \array_slice($file, $longest_match));
661:
662: return \implode('/', $file);
663: }
664:
665: // fallback to find common path with Kint dir
666: $kint = \array_values(\array_filter(\explode('/', \str_replace('\\', '/', KINT_DIR)), 'strlen'));
667:
668: foreach ($file as $i => $part) {
669: if (!isset($kint[$i]) || $kint[$i] !== $part) {
670: return ($i ? '.../' : '/').\implode('/', \array_slice($file, $i));
671: }
672: }
673:
674: return '/'.\implode('/', $file);
675: }
676:
677: public static function getIdeLink($file, $line)
678: {
679: return \str_replace(array('%f', '%l'), array($file, $line), self::$file_link_format);
680: }
681:
682: /**
683: * Returns specific function call info from a stack trace frame, or null if no match could be found.
684: *
685: * @param array $frame The stack trace frame in question
686: * @param int $argc The amount of arguments received
687: *
688: * @return null|array{parameters:array, modifiers:array} params and modifiers, or null if a specific call could not be determined
689: */
690: protected static function getSingleCall(array $frame, $argc)
691: {
692: if (!isset($frame['file'], $frame['line'], $frame['function']) || !\is_readable($frame['file'])) {
693: return null;
694: }
695:
696: if (empty($frame['class'])) {
697: $callfunc = $frame['function'];
698: } else {
699: $callfunc = array($frame['class'], $frame['function']);
700: }
701:
702: $calls = CallFinder::getFunctionCalls(
703: \file_get_contents($frame['file']),
704: $frame['line'],
705: $callfunc
706: );
707:
708: $return = null;
709:
710: foreach ($calls as $call) {
711: $is_unpack = false;
712:
713: // Handle argument unpacking as a last resort
714: if (KINT_PHP56) {
715: foreach ($call['parameters'] as $i => &$param) {
716: if (0 === \strpos($param['name'], '...')) {
717: if ($i < $argc && $i === \count($call['parameters']) - 1) {
718: for ($j = 1; $j + $i < $argc; ++$j) {
719: $call['parameters'][] = array(
720: 'name' => 'array_values('.\substr($param['name'], 3).')['.$j.']',
721: 'path' => 'array_values('.\substr($param['path'], 3).')['.$j.']',
722: 'expression' => false,
723: );
724: }
725:
726: $param['name'] = 'reset('.\substr($param['name'], 3).')';
727: $param['path'] = 'reset('.\substr($param['path'], 3).')';
728: $param['expression'] = false;
729: } else {
730: $call['parameters'] = \array_slice($call['parameters'], 0, $i);
731: }
732:
733: $is_unpack = true;
734: break;
735: }
736:
737: if ($i >= $argc) {
738: continue 2;
739: }
740: }
741: }
742:
743: if ($is_unpack || \count($call['parameters']) === $argc) {
744: if (null === $return) {
745: $return = $call;
746: } else {
747: // If we have multiple calls on the same line with the same amount of arguments,
748: // we can't be sure which it is so just return null and let them figure it out
749: return null;
750: }
751: }
752: }
753:
754: return $return;
755: }
756: }
757: