Dumper.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (https://nette.org)
  4. * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  5. */
  6. declare(strict_types=1);
  7. namespace Nette\PhpGenerator;
  8. use Nette;
  9. /**
  10. * PHP code generator utils.
  11. */
  12. final class Dumper
  13. {
  14. private const IndentLength = 4;
  15. /** @var int */
  16. public $maxDepth = 50;
  17. /** @var int */
  18. public $wrapLength = 120;
  19. /** @var string */
  20. public $indentation = "\t";
  21. /**
  22. * Returns a PHP representation of a variable.
  23. */
  24. public function dump($var, int $column = 0): string
  25. {
  26. return $this->dumpVar($var, [], 0, $column);
  27. }
  28. private function dumpVar(&$var, array $parents = [], int $level = 0, int $column = 0): string
  29. {
  30. if ($var === null) {
  31. return 'null';
  32. } elseif (is_string($var)) {
  33. return $this->dumpString($var);
  34. } elseif (is_array($var)) {
  35. return $this->dumpArray($var, $parents, $level, $column);
  36. } elseif ($var instanceof Literal) {
  37. return $this->dumpLiteral($var, $level);
  38. } elseif (is_object($var)) {
  39. return $this->dumpObject($var, $parents, $level);
  40. } elseif (is_resource($var)) {
  41. throw new Nette\InvalidArgumentException('Cannot dump resource.');
  42. } else {
  43. return var_export($var, true);
  44. }
  45. }
  46. private function dumpString(string $s): string
  47. {
  48. static $special = [
  49. "\r" => '\r',
  50. "\n" => '\n',
  51. "\t" => '\t',
  52. "\e" => '\e',
  53. '\\' => '\\\\',
  54. ];
  55. $utf8 = preg_match('##u', $s);
  56. $escaped = preg_replace_callback(
  57. $utf8 ? '#[\p{C}\\\\]#u' : '#[\x00-\x1F\x7F-\xFF\\\\]#',
  58. function ($m) use ($special) {
  59. return $special[$m[0]] ?? (strlen($m[0]) === 1
  60. ? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT) . ''
  61. : '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}');
  62. },
  63. $s
  64. );
  65. return $s === str_replace('\\\\', '\\', $escaped)
  66. ? "'" . preg_replace('#\'|\\\\(?=[\'\\\\]|$)#D', '\\\\$0', $s) . "'"
  67. : '"' . addcslashes($escaped, '"$') . '"';
  68. }
  69. private static function utf8Ord(string $c): int
  70. {
  71. $ord0 = ord($c[0]);
  72. if ($ord0 < 0x80) {
  73. return $ord0;
  74. } elseif ($ord0 < 0xE0) {
  75. return ($ord0 << 6) + ord($c[1]) - 0x3080;
  76. } elseif ($ord0 < 0xF0) {
  77. return ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080;
  78. } else {
  79. return ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080;
  80. }
  81. }
  82. private function dumpArray(array &$var, array $parents, int $level, int $column): string
  83. {
  84. if (empty($var)) {
  85. return '[]';
  86. } elseif ($level > $this->maxDepth || in_array($var, $parents, true)) {
  87. throw new Nette\InvalidArgumentException('Nesting level too deep or recursive dependency.');
  88. }
  89. $space = str_repeat($this->indentation, $level);
  90. $outInline = '';
  91. $outWrapped = "\n$space";
  92. $parents[] = $var;
  93. $counter = 0;
  94. $hideKeys = is_int(($tmp = array_keys($var))[0]) && $tmp === range($tmp[0], $tmp[0] + count($var) - 1);
  95. foreach ($var as $k => &$v) {
  96. $keyPart = $hideKeys && $k === $counter
  97. ? ''
  98. : $this->dumpVar($k) . ' => ';
  99. $counter = is_int($k) ? max($k + 1, $counter) : $counter;
  100. $outInline .= ($outInline === '' ? '' : ', ') . $keyPart;
  101. $outInline .= $this->dumpVar($v, $parents, 0, $column + strlen($outInline));
  102. $outWrapped .= $this->indentation
  103. . $keyPart
  104. . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart))
  105. . ",\n$space";
  106. }
  107. array_pop($parents);
  108. $wrap = strpos($outInline, "\n") !== false || $level * self::IndentLength + $column + strlen($outInline) + 3 > $this->wrapLength; // 3 = [],
  109. return '[' . ($wrap ? $outWrapped : $outInline) . ']';
  110. }
  111. private function dumpObject($var, array $parents, int $level): string
  112. {
  113. if ($var instanceof \Serializable) {
  114. return 'unserialize(' . $this->dumpString(serialize($var)) . ')';
  115. } elseif ($var instanceof \UnitEnum) {
  116. return '\\' . get_class($var) . '::' . $var->name;
  117. } elseif ($var instanceof \Closure) {
  118. $inner = Nette\Utils\Callback::unwrap($var);
  119. if (Nette\Utils\Callback::isStatic($inner)) {
  120. return PHP_VERSION_ID < 80100
  121. ? '\Closure::fromCallable(' . $this->dump($inner) . ')'
  122. : implode('::', (array) $inner) . '(...)';
  123. }
  124. throw new Nette\InvalidArgumentException('Cannot dump closure.');
  125. }
  126. $class = get_class($var);
  127. if ((new \ReflectionObject($var))->isAnonymous()) {
  128. throw new Nette\InvalidArgumentException('Cannot dump anonymous class.');
  129. } elseif (in_array($class, [\DateTime::class, \DateTimeImmutable::class], true)) {
  130. return $this->format("new \\$class(?, new \\DateTimeZone(?))", $var->format('Y-m-d H:i:s.u'), $var->getTimeZone()->getName());
  131. }
  132. $arr = (array) $var;
  133. $space = str_repeat($this->indentation, $level);
  134. if ($level > $this->maxDepth || in_array($var, $parents, true)) {
  135. throw new Nette\InvalidArgumentException('Nesting level too deep or recursive dependency.');
  136. }
  137. $out = "\n";
  138. $parents[] = $var;
  139. if (method_exists($var, '__sleep')) {
  140. foreach ($var->__sleep() as $v) {
  141. $props[$v] = $props["\x00*\x00$v"] = $props["\x00$class\x00$v"] = true;
  142. }
  143. }
  144. foreach ($arr as $k => &$v) {
  145. if (!isset($props) || isset($props[$k])) {
  146. $out .= $space . $this->indentation
  147. . ($keyPart = $this->dumpVar($k) . ' => ')
  148. . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart))
  149. . ",\n";
  150. }
  151. }
  152. array_pop($parents);
  153. $out .= $space;
  154. return $class === \stdClass::class
  155. ? "(object) [$out]"
  156. : '\\' . self::class . "::createObject('$class', [$out])";
  157. }
  158. private function dumpLiteral(Literal $var, int $level): string
  159. {
  160. $s = $var->formatWith($this);
  161. $s = Nette\Utils\Strings::indent(trim($s), $level, $this->indentation);
  162. return ltrim($s, $this->indentation);
  163. }
  164. /**
  165. * Generates PHP statement. Supports placeholders: ? \? $? ->? ::? ...? ...?: ?*
  166. */
  167. public function format(string $statement, ...$args): string
  168. {
  169. $tokens = preg_split('#(\.\.\.\?:?|\$\?|->\?|::\?|\\\\\?|\?\*|\?(?!\w))#', $statement, -1, PREG_SPLIT_DELIM_CAPTURE);
  170. $res = '';
  171. foreach ($tokens as $n => $token) {
  172. if ($n % 2 === 0) {
  173. $res .= $token;
  174. } elseif ($token === '\?') {
  175. $res .= '?';
  176. } elseif (!$args) {
  177. throw new Nette\InvalidArgumentException('Insufficient number of arguments.');
  178. } elseif ($token === '?') {
  179. $res .= $this->dump(array_shift($args), strlen($res) - strrpos($res, "\n"));
  180. } elseif ($token === '...?' || $token === '...?:' || $token === '?*') {
  181. $arg = array_shift($args);
  182. if (!is_array($arg)) {
  183. throw new Nette\InvalidArgumentException('Argument must be an array.');
  184. }
  185. $res .= $this->dumpArguments($arg, strlen($res) - strrpos($res, "\n"), $token === '...?:');
  186. } else { // $ -> ::
  187. $arg = array_shift($args);
  188. if ($arg instanceof Literal || !Helpers::isIdentifier($arg)) {
  189. $arg = '{' . $this->dumpVar($arg) . '}';
  190. }
  191. $res .= substr($token, 0, -1) . $arg;
  192. }
  193. }
  194. if ($args) {
  195. throw new Nette\InvalidArgumentException('Insufficient number of placeholders.');
  196. }
  197. return $res;
  198. }
  199. private function dumpArguments(array &$var, int $column, bool $named): string
  200. {
  201. $outInline = $outWrapped = '';
  202. foreach ($var as $k => &$v) {
  203. $k = !$named || is_int($k) ? '' : $k . ': ';
  204. $outInline .= $outInline === '' ? '' : ', ';
  205. $outInline .= $k . $this->dumpVar($v, [$var], 0, $column + strlen($outInline));
  206. $outWrapped .= ($outWrapped === '' ? '' : ',') . "\n"
  207. . $this->indentation . $k . $this->dumpVar($v, [$var], 1);
  208. }
  209. return count($var) > 1 && (strpos($outInline, "\n") !== false || $column + strlen($outInline) > $this->wrapLength)
  210. ? $outWrapped . "\n"
  211. : $outInline;
  212. }
  213. /**
  214. * @internal
  215. */
  216. public static function createObject(string $class, array $props): object
  217. {
  218. return unserialize('O' . substr(serialize($class), 1, -1) . substr(serialize($props), 1));
  219. }
  220. }