| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* This file is part of the Nette Framework (https://nette.org) |
| 5 |
* Copyright (c) 2004 David Grudl (https://davidgrudl.com) |
| 6 |
*/ |
| 7 |
declare (strict_types=1); |
| 8 |
namespace Packetery\Nette\PhpGenerator; |
| 9 |
|
| 10 |
use Packetery\Nette; |
| 11 |
/** |
| 12 |
* PHP code generator utils. |
| 13 |
*/ |
| 14 |
final class Dumper |
| 15 |
{ |
| 16 |
private const INDENT_LENGTH = 4; |
| 17 |
/** @var int */ |
| 18 |
public $maxDepth = 50; |
| 19 |
/** @var int */ |
| 20 |
public $wrapLength = 120; |
| 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 (\is_object($var)) { |
| 37 |
if ($var instanceof Literal || $var instanceof Closure) { |
| 38 |
return \ltrim(\Packetery\Nette\Utils\Strings::indent(\trim((string) $var), $level), "\t"); |
| 39 |
} |
| 40 |
return $this->dumpObject($var, $parents, $level); |
| 41 |
} elseif (\is_resource($var)) { |
| 42 |
throw new \Packetery\Nette\InvalidArgumentException('Cannot dump resource.'); |
| 43 |
} else { |
| 44 |
return \var_export($var, \true); |
| 45 |
} |
| 46 |
} |
| 47 |
private function dumpString(string $var) : string |
| 48 |
{ |
| 49 |
if (\preg_match('#[^\\x09\\x20-\\x7E\\xA0-\\x{10FFFF}]#u', $var) || \preg_last_error()) { |
| 50 |
static $table; |
| 51 |
if ($table === null) { |
| 52 |
foreach (\array_merge(\range("\x00", "\x1f"), \range("", "\xff")) as $ch) { |
| 53 |
$table[$ch] = '\\x' . \str_pad(\dechex(\ord($ch)), 2, '0', \STR_PAD_LEFT); |
| 54 |
} |
| 55 |
$table['\\'] = '\\\\'; |
| 56 |
$table["\r"] = '\\r'; |
| 57 |
$table["\n"] = '\\n'; |
| 58 |
$table["\t"] = '\\t'; |
| 59 |
$table['$'] = '\\$'; |
| 60 |
$table['"'] = '\\"'; |
| 61 |
} |
| 62 |
return '"' . \strtr($var, $table) . '"'; |
| 63 |
} |
| 64 |
return "'" . \preg_replace('#\'|\\\\(?=[\'\\\\]|$)#D', '\\\\$0', $var) . "'"; |
| 65 |
} |
| 66 |
private function dumpArray(array &$var, array $parents, int $level, int $column) : string |
| 67 |
{ |
| 68 |
if (empty($var)) { |
| 69 |
return '[]'; |
| 70 |
} elseif ($level > $this->maxDepth || \in_array($var, $parents ?? [], \true)) { |
| 71 |
throw new \Packetery\Nette\InvalidArgumentException('Nesting level too deep or recursive dependency.'); |
| 72 |
} |
| 73 |
$space = \str_repeat("\t", $level); |
| 74 |
$outInline = ''; |
| 75 |
$outWrapped = "\n{$space}"; |
| 76 |
$parents[] = $var; |
| 77 |
$counter = 0; |
| 78 |
$hideKeys = \is_int(($tmp = \array_keys($var))[0]) && $tmp === \range($tmp[0], $tmp[0] + \count($var) - 1); |
| 79 |
foreach ($var as $k => &$v) { |
| 80 |
$keyPart = $hideKeys && $k === $counter ? '' : $this->dumpVar($k) . ' => '; |
| 81 |
$counter = \is_int($k) ? \max($k + 1, $counter) : $counter; |
| 82 |
$outInline .= ($outInline === '' ? '' : ', ') . $keyPart; |
| 83 |
$outInline .= $this->dumpVar($v, $parents, 0, $column + \strlen($outInline)); |
| 84 |
$outWrapped .= "\t" . $keyPart . $this->dumpVar($v, $parents, $level + 1, \strlen($keyPart)) . ",\n{$space}"; |
| 85 |
} |
| 86 |
\array_pop($parents); |
| 87 |
$wrap = \strpos($outInline, "\n") !== \false || $level * self::INDENT_LENGTH + $column + \strlen($outInline) + 3 > $this->wrapLength; |
| 88 |
// 3 = [], |
| 89 |
return '[' . ($wrap ? $outWrapped : $outInline) . ']'; |
| 90 |
} |
| 91 |
private function dumpObject(&$var, array $parents, int $level) : string |
| 92 |
{ |
| 93 |
if ($var instanceof \Serializable) { |
| 94 |
return 'unserialize(' . $this->dumpString(\serialize($var)) . ')'; |
| 95 |
} elseif ($var instanceof \Closure) { |
| 96 |
throw new \Packetery\Nette\InvalidArgumentException('Cannot dump closure.'); |
| 97 |
} |
| 98 |
$class = \get_class($var); |
| 99 |
if ((new \ReflectionObject($var))->isAnonymous()) { |
| 100 |
throw new \Packetery\Nette\InvalidArgumentException('Cannot dump anonymous class.'); |
| 101 |
} elseif (\in_array($class, [\DateTime::class, \DateTimeImmutable::class], \true)) { |
| 102 |
return $this->format("new \\{$class}(?, new \\DateTimeZone(?))", $var->format('Y-m-d H:i:s.u'), $var->getTimeZone()->getName()); |
| 103 |
} |
| 104 |
$arr = (array) $var; |
| 105 |
$space = \str_repeat("\t", $level); |
| 106 |
if ($level > $this->maxDepth || \in_array($var, $parents ?? [], \true)) { |
| 107 |
throw new \Packetery\Nette\InvalidArgumentException('Nesting level too deep or recursive dependency.'); |
| 108 |
} |
| 109 |
$out = "\n"; |
| 110 |
$parents[] = $var; |
| 111 |
if (\method_exists($var, '__sleep')) { |
| 112 |
foreach ($var->__sleep() as $v) { |
| 113 |
$props[$v] = $props["\x00*\x00{$v}"] = $props["\x00{$class}\x00{$v}"] = \true; |
| 114 |
} |
| 115 |
} |
| 116 |
foreach ($arr as $k => &$v) { |
| 117 |
if (!isset($props) || isset($props[$k])) { |
| 118 |
$out .= "{$space}\t" . ($keyPart = $this->dumpVar($k) . ' => ') . $this->dumpVar($v, $parents, $level + 1, \strlen($keyPart)) . ",\n"; |
| 119 |
} |
| 120 |
} |
| 121 |
\array_pop($parents); |
| 122 |
$out .= $space; |
| 123 |
return $class === \stdClass::class ? "(object) [{$out}]" : '\\' . self::class . "::createObject('{$class}', [{$out}])"; |
| 124 |
} |
| 125 |
/** |
| 126 |
* Generates PHP statement. |
| 127 |
*/ |
| 128 |
public function format(string $statement, ...$args) : string |
| 129 |
{ |
| 130 |
$tokens = \preg_split('#(\\.\\.\\.\\?:?|\\$\\?|->\\?|::\\?|\\\\\\?|\\?\\*|\\?)#', $statement, -1, \PREG_SPLIT_DELIM_CAPTURE); |
| 131 |
$res = ''; |
| 132 |
foreach ($tokens as $n => $token) { |
| 133 |
if ($n % 2 === 0) { |
| 134 |
$res .= $token; |
| 135 |
} elseif ($token === '\\?') { |
| 136 |
$res .= '?'; |
| 137 |
} elseif (!$args) { |
| 138 |
throw new \Packetery\Nette\InvalidArgumentException('Insufficient number of arguments.'); |
| 139 |
} elseif ($token === '?') { |
| 140 |
$res .= $this->dump(\array_shift($args), \strlen($res) - \strrpos($res, "\n")); |
| 141 |
} elseif ($token === '...?' || $token === '...?:' || $token === '?*') { |
| 142 |
$arg = \array_shift($args); |
| 143 |
if (!\is_array($arg)) { |
| 144 |
throw new \Packetery\Nette\InvalidArgumentException('Argument must be an array.'); |
| 145 |
} |
| 146 |
$res .= $this->dumpArguments($arg, \strlen($res) - \strrpos($res, "\n"), $token === '...?:'); |
| 147 |
} else { |
| 148 |
// $ -> :: |
| 149 |
$arg = \array_shift($args); |
| 150 |
if ($arg instanceof Literal || !Helpers::isIdentifier($arg)) { |
| 151 |
$arg = '{' . $this->dumpVar($arg) . '}'; |
| 152 |
} |
| 153 |
$res .= \substr($token, 0, -1) . $arg; |
| 154 |
} |
| 155 |
} |
| 156 |
if ($args) { |
| 157 |
throw new \Packetery\Nette\InvalidArgumentException('Insufficient number of placeholders.'); |
| 158 |
} |
| 159 |
return $res; |
| 160 |
} |
| 161 |
private function dumpArguments(array &$var, int $column, bool $named) : string |
| 162 |
{ |
| 163 |
$outInline = $outWrapped = ''; |
| 164 |
foreach ($var as $k => &$v) { |
| 165 |
$k = !$named || \is_int($k) ? '' : $k . ': '; |
| 166 |
$outInline .= $outInline === '' ? '' : ', '; |
| 167 |
$outInline .= $k . $this->dumpVar($v, [$var], 0, $column + \strlen($outInline)); |
| 168 |
$outWrapped .= ($outWrapped === '' ? '' : ',') . "\n\t" . $k . $this->dumpVar($v, [$var], 1); |
| 169 |
} |
| 170 |
return \count($var) > 1 && (\strpos($outInline, "\n") !== \false || $column + \strlen($outInline) > $this->wrapLength) ? $outWrapped . "\n" : $outInline; |
| 171 |
} |
| 172 |
/** |
| 173 |
* @return object |
| 174 |
* @internal |
| 175 |
*/ |
| 176 |
public static function createObject(string $class, array $props) |
| 177 |
{ |
| 178 |
return \unserialize('O' . \substr(\serialize($class), 1, -1) . \substr(\serialize($props), 1)); |
| 179 |
} |
| 180 |
} |
| 181 |
|