FlattenException.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\ErrorHandler\Exception;
  11. use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
  12. use Symfony\Component\HttpFoundation\Response;
  13. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  14. use Symfony\Component\VarDumper\Caster\Caster;
  15. use Symfony\Component\VarDumper\Cloner\Data;
  16. use Symfony\Component\VarDumper\Cloner\Stub;
  17. use Symfony\Component\VarDumper\Cloner\VarCloner;
  18. /**
  19. * FlattenException wraps a PHP Error or Exception to be able to serialize it.
  20. *
  21. * Basically, this class removes all objects from the trace.
  22. *
  23. * @author Fabien Potencier <fabien@symfony.com>
  24. */
  25. class FlattenException
  26. {
  27. private string $message;
  28. private string|int $code;
  29. private ?self $previous = null;
  30. private array $trace;
  31. private string $traceAsString;
  32. private string $class;
  33. private int $statusCode;
  34. private string $statusText;
  35. private array $headers;
  36. private string $file;
  37. private int $line;
  38. private ?string $asString = null;
  39. private Data $dataRepresentation;
  40. public static function create(\Exception $exception, ?int $statusCode = null, array $headers = []): static
  41. {
  42. return static::createFromThrowable($exception, $statusCode, $headers);
  43. }
  44. public static function createFromThrowable(\Throwable $exception, ?int $statusCode = null, array $headers = []): static
  45. {
  46. $e = new static();
  47. $e->setMessage($exception->getMessage());
  48. $e->setCode($exception->getCode());
  49. if ($exception instanceof HttpExceptionInterface) {
  50. $statusCode = $exception->getStatusCode();
  51. $headers = array_merge($headers, $exception->getHeaders());
  52. } elseif ($exception instanceof RequestExceptionInterface) {
  53. $statusCode = 400;
  54. }
  55. $statusCode ??= 500;
  56. if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) {
  57. $statusText = Response::$statusTexts[$statusCode];
  58. } else {
  59. $statusText = 'Whoops, looks like something went wrong.';
  60. }
  61. $e->setStatusText($statusText);
  62. $e->setStatusCode($statusCode);
  63. $e->setHeaders($headers);
  64. $e->setTraceFromThrowable($exception);
  65. $e->setClass(get_debug_type($exception));
  66. $e->setFile($exception->getFile());
  67. $e->setLine($exception->getLine());
  68. $previous = $exception->getPrevious();
  69. if ($previous instanceof \Throwable) {
  70. $e->setPrevious(static::createFromThrowable($previous));
  71. }
  72. return $e;
  73. }
  74. public static function createWithDataRepresentation(\Throwable $throwable, ?int $statusCode = null, array $headers = [], ?VarCloner $cloner = null): static
  75. {
  76. $e = static::createFromThrowable($throwable, $statusCode, $headers);
  77. static $defaultCloner;
  78. if (!$cloner ??= $defaultCloner) {
  79. $cloner = $defaultCloner = new VarCloner();
  80. $cloner->addCasters([
  81. \Throwable::class => function (\Throwable $e, array $a, Stub $s, bool $isNested): array {
  82. if (!$isNested) {
  83. unset($a[Caster::PREFIX_PROTECTED.'message']);
  84. unset($a[Caster::PREFIX_PROTECTED.'code']);
  85. unset($a[Caster::PREFIX_PROTECTED.'file']);
  86. unset($a[Caster::PREFIX_PROTECTED.'line']);
  87. unset($a["\0Error\0trace"], $a["\0Exception\0trace"]);
  88. unset($a["\0Error\0previous"], $a["\0Exception\0previous"]);
  89. }
  90. return $a;
  91. },
  92. ]);
  93. }
  94. return $e->setDataRepresentation($cloner->cloneVar($throwable));
  95. }
  96. public function toArray(): array
  97. {
  98. $exceptions = [];
  99. foreach (array_merge([$this], $this->getAllPrevious()) as $exception) {
  100. $exceptions[] = [
  101. 'message' => $exception->getMessage(),
  102. 'class' => $exception->getClass(),
  103. 'trace' => $exception->getTrace(),
  104. 'data' => $exception->getDataRepresentation(),
  105. ];
  106. }
  107. return $exceptions;
  108. }
  109. public function getStatusCode(): int
  110. {
  111. return $this->statusCode;
  112. }
  113. /**
  114. * @return $this
  115. */
  116. public function setStatusCode(int $code): static
  117. {
  118. $this->statusCode = $code;
  119. return $this;
  120. }
  121. public function getHeaders(): array
  122. {
  123. return $this->headers;
  124. }
  125. /**
  126. * @return $this
  127. */
  128. public function setHeaders(array $headers): static
  129. {
  130. $this->headers = $headers;
  131. return $this;
  132. }
  133. public function getClass(): string
  134. {
  135. return $this->class;
  136. }
  137. /**
  138. * @return $this
  139. */
  140. public function setClass(string $class): static
  141. {
  142. $this->class = str_contains($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class;
  143. return $this;
  144. }
  145. public function getFile(): string
  146. {
  147. return $this->file;
  148. }
  149. /**
  150. * @return $this
  151. */
  152. public function setFile(string $file): static
  153. {
  154. $this->file = $file;
  155. return $this;
  156. }
  157. public function getLine(): int
  158. {
  159. return $this->line;
  160. }
  161. /**
  162. * @return $this
  163. */
  164. public function setLine(int $line): static
  165. {
  166. $this->line = $line;
  167. return $this;
  168. }
  169. public function getStatusText(): string
  170. {
  171. return $this->statusText;
  172. }
  173. /**
  174. * @return $this
  175. */
  176. public function setStatusText(string $statusText): static
  177. {
  178. $this->statusText = $statusText;
  179. return $this;
  180. }
  181. public function getMessage(): string
  182. {
  183. return $this->message;
  184. }
  185. /**
  186. * @return $this
  187. */
  188. public function setMessage(string $message): static
  189. {
  190. if (str_contains($message, "@anonymous\0")) {
  191. $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message);
  192. }
  193. $this->message = $message;
  194. return $this;
  195. }
  196. /**
  197. * @return int|string int most of the time (might be a string with PDOException)
  198. */
  199. public function getCode(): int|string
  200. {
  201. return $this->code;
  202. }
  203. /**
  204. * @return $this
  205. */
  206. public function setCode(int|string $code): static
  207. {
  208. $this->code = $code;
  209. return $this;
  210. }
  211. public function getPrevious(): ?self
  212. {
  213. return $this->previous;
  214. }
  215. /**
  216. * @return $this
  217. */
  218. public function setPrevious(?self $previous): static
  219. {
  220. $this->previous = $previous;
  221. return $this;
  222. }
  223. /**
  224. * @return self[]
  225. */
  226. public function getAllPrevious(): array
  227. {
  228. $exceptions = [];
  229. $e = $this;
  230. while ($e = $e->getPrevious()) {
  231. $exceptions[] = $e;
  232. }
  233. return $exceptions;
  234. }
  235. public function getTrace(): array
  236. {
  237. return $this->trace;
  238. }
  239. /**
  240. * @return $this
  241. */
  242. public function setTraceFromThrowable(\Throwable $throwable): static
  243. {
  244. $this->traceAsString = $throwable->getTraceAsString();
  245. return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine());
  246. }
  247. /**
  248. * @return $this
  249. */
  250. public function setTrace(array $trace, ?string $file, ?int $line): static
  251. {
  252. $this->trace = [];
  253. $this->trace[] = [
  254. 'namespace' => '',
  255. 'short_class' => '',
  256. 'class' => '',
  257. 'type' => '',
  258. 'function' => '',
  259. 'file' => $file,
  260. 'line' => $line,
  261. 'args' => [],
  262. ];
  263. foreach ($trace as $entry) {
  264. $class = '';
  265. $namespace = '';
  266. if (isset($entry['class'])) {
  267. $parts = explode('\\', $entry['class']);
  268. $class = array_pop($parts);
  269. $namespace = implode('\\', $parts);
  270. }
  271. $this->trace[] = [
  272. 'namespace' => $namespace,
  273. 'short_class' => $class,
  274. 'class' => $entry['class'] ?? '',
  275. 'type' => $entry['type'] ?? '',
  276. 'function' => $entry['function'] ?? null,
  277. 'file' => $entry['file'] ?? null,
  278. 'line' => $entry['line'] ?? null,
  279. 'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : [],
  280. ];
  281. }
  282. return $this;
  283. }
  284. public function getDataRepresentation(): ?Data
  285. {
  286. return $this->dataRepresentation ?? null;
  287. }
  288. /**
  289. * @return $this
  290. */
  291. public function setDataRepresentation(Data $data): static
  292. {
  293. $this->dataRepresentation = $data;
  294. return $this;
  295. }
  296. private function flattenArgs(array $args, int $level = 0, int &$count = 0): array
  297. {
  298. $result = [];
  299. foreach ($args as $key => $value) {
  300. if (++$count > 1e4) {
  301. return ['array', '*SKIPPED over 10000 entries*'];
  302. }
  303. if ($value instanceof \__PHP_Incomplete_Class) {
  304. $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)];
  305. } elseif (\is_object($value)) {
  306. $result[$key] = ['object', get_debug_type($value)];
  307. } elseif (\is_array($value)) {
  308. if ($level > 10) {
  309. $result[$key] = ['array', '*DEEP NESTED ARRAY*'];
  310. } else {
  311. $result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)];
  312. }
  313. } elseif (null === $value) {
  314. $result[$key] = ['null', null];
  315. } elseif (\is_bool($value)) {
  316. $result[$key] = ['boolean', $value];
  317. } elseif (\is_int($value)) {
  318. $result[$key] = ['integer', $value];
  319. } elseif (\is_float($value)) {
  320. $result[$key] = ['float', $value];
  321. } elseif (\is_resource($value)) {
  322. $result[$key] = ['resource', get_resource_type($value)];
  323. } else {
  324. $result[$key] = ['string', (string) $value];
  325. }
  326. }
  327. return $result;
  328. }
  329. private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value): string
  330. {
  331. $array = new \ArrayObject($value);
  332. return $array['__PHP_Incomplete_Class_Name'];
  333. }
  334. public function getTraceAsString(): string
  335. {
  336. return $this->traceAsString;
  337. }
  338. /**
  339. * @return $this
  340. */
  341. public function setAsString(?string $asString): static
  342. {
  343. $this->asString = $asString;
  344. return $this;
  345. }
  346. public function getAsString(): string
  347. {
  348. if (null !== $this->asString) {
  349. return $this->asString;
  350. }
  351. $message = '';
  352. $next = false;
  353. foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) {
  354. if ($next) {
  355. $message .= 'Next ';
  356. } else {
  357. $next = true;
  358. }
  359. $message .= $exception->getClass();
  360. if ('' != $exception->getMessage()) {
  361. $message .= ': '.$exception->getMessage();
  362. }
  363. $message .= ' in '.$exception->getFile().':'.$exception->getLine().
  364. "\nStack trace:\n".$exception->getTraceAsString()."\n\n";
  365. }
  366. return rtrim($message);
  367. }
  368. }