Writer.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. /**
  3. * This file is part of Collision.
  4. *
  5. * (c) Nuno Maduro <enunomaduro@gmail.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 NunoMaduro\Collision;
  11. use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;
  12. use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;
  13. use NunoMaduro\Collision\Contracts\SolutionsRepository;
  14. use NunoMaduro\Collision\Contracts\Writer as WriterContract;
  15. use NunoMaduro\Collision\SolutionsRepositories\NullSolutionsRepository;
  16. use Symfony\Component\Console\Output\ConsoleOutput;
  17. use Symfony\Component\Console\Output\OutputInterface;
  18. use Whoops\Exception\Frame;
  19. use Whoops\Exception\Inspector;
  20. /**
  21. * This is an Collision Writer implementation.
  22. *
  23. * @author Nuno Maduro <enunomaduro@gmail.com>
  24. */
  25. class Writer implements WriterContract
  26. {
  27. /**
  28. * The number of frames if no verbosity is specified.
  29. */
  30. const VERBOSITY_NORMAL_FRAMES = 1;
  31. /**
  32. * Holds an instance of the solutions repository.
  33. *
  34. * @var \NunoMaduro\Collision\Contracts\SolutionsRepository
  35. */
  36. private $solutionsRepository;
  37. /**
  38. * Holds an instance of the Output.
  39. *
  40. * @var \Symfony\Component\Console\Output\OutputInterface
  41. */
  42. protected $output;
  43. /**
  44. * Holds an instance of the Argument Formatter.
  45. *
  46. * @var \NunoMaduro\Collision\Contracts\ArgumentFormatter
  47. */
  48. protected $argumentFormatter;
  49. /**
  50. * Holds an instance of the Highlighter.
  51. *
  52. * @var \NunoMaduro\Collision\Contracts\Highlighter
  53. */
  54. protected $highlighter;
  55. /**
  56. * Ignores traces where the file string matches one
  57. * of the provided regex expressions.
  58. *
  59. * @var string[]
  60. */
  61. protected $ignore = [];
  62. /**
  63. * Declares whether or not the trace should appear.
  64. *
  65. * @var bool
  66. */
  67. protected $showTrace = true;
  68. /**
  69. * Declares whether or not the title should appear.
  70. *
  71. * @var bool
  72. */
  73. protected $showTitle = true;
  74. /**
  75. * Declares whether or not the editor should appear.
  76. *
  77. * @var bool
  78. */
  79. protected $showEditor = true;
  80. /**
  81. * Creates an instance of the writer.
  82. */
  83. public function __construct(
  84. SolutionsRepository $solutionsRepository = null,
  85. OutputInterface $output = null,
  86. ArgumentFormatterContract $argumentFormatter = null,
  87. HighlighterContract $highlighter = null
  88. ) {
  89. $this->solutionsRepository = $solutionsRepository ?: new NullSolutionsRepository();
  90. $this->output = $output ?: new ConsoleOutput();
  91. $this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter();
  92. $this->highlighter = $highlighter ?: new Highlighter();
  93. }
  94. /**
  95. * {@inheritdoc}
  96. */
  97. public function write(Inspector $inspector): void
  98. {
  99. $this->renderTitleAndDescription($inspector);
  100. $frames = $this->getFrames($inspector);
  101. $editorFrame = array_shift($frames);
  102. if ($this->showEditor && $editorFrame !== null) {
  103. $this->renderEditor($editorFrame);
  104. }
  105. $this->renderSolution($inspector);
  106. if ($this->showTrace && !empty($frames)) {
  107. $this->renderTrace($frames);
  108. } else {
  109. $this->output->writeln('');
  110. }
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function ignoreFilesIn(array $ignore): WriterContract
  116. {
  117. $this->ignore = $ignore;
  118. return $this;
  119. }
  120. /**
  121. * {@inheritdoc}
  122. */
  123. public function showTrace(bool $show): WriterContract
  124. {
  125. $this->showTrace = $show;
  126. return $this;
  127. }
  128. /**
  129. * {@inheritdoc}
  130. */
  131. public function showTitle(bool $show): WriterContract
  132. {
  133. $this->showTitle = $show;
  134. return $this;
  135. }
  136. /**
  137. * {@inheritdoc}
  138. */
  139. public function showEditor(bool $show): WriterContract
  140. {
  141. $this->showEditor = $show;
  142. return $this;
  143. }
  144. /**
  145. * {@inheritdoc}
  146. */
  147. public function setOutput(OutputInterface $output): WriterContract
  148. {
  149. $this->output = $output;
  150. return $this;
  151. }
  152. /**
  153. * {@inheritdoc}
  154. */
  155. public function getOutput(): OutputInterface
  156. {
  157. return $this->output;
  158. }
  159. /**
  160. * Returns pertinent frames.
  161. */
  162. protected function getFrames(Inspector $inspector): array
  163. {
  164. return $inspector->getFrames()
  165. ->filter(
  166. function ($frame) {
  167. // If we are in verbose mode, we always
  168. // display the full stack trace.
  169. if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
  170. return true;
  171. }
  172. foreach ($this->ignore as $ignore) {
  173. if (preg_match($ignore, $frame->getFile())) {
  174. return false;
  175. }
  176. }
  177. return true;
  178. }
  179. )
  180. ->getArray();
  181. }
  182. /**
  183. * Renders the title of the exception.
  184. */
  185. protected function renderTitleAndDescription(Inspector $inspector): WriterContract
  186. {
  187. $exception = $inspector->getException();
  188. $message = rtrim($exception->getMessage());
  189. $class = $inspector->getExceptionName();
  190. if ($this->showTitle) {
  191. $this->render("<bg=red;options=bold> $class </>");
  192. $this->output->writeln('');
  193. }
  194. $this->output->writeln("<fg=default;options=bold> $message</>");
  195. return $this;
  196. }
  197. /**
  198. * Renders the solution of the exception, if any.
  199. */
  200. protected function renderSolution(Inspector $inspector): WriterContract
  201. {
  202. $throwable = $inspector->getException();
  203. $solutions = $this->solutionsRepository->getFromThrowable($throwable);
  204. foreach ($solutions as $solution) {
  205. /** @var \Facade\IgnitionContracts\Solution $solution */
  206. $title = $solution->getSolutionTitle();
  207. $description = $solution->getSolutionDescription();
  208. $links = $solution->getDocumentationLinks();
  209. $description = trim((string) preg_replace("/\n/", "\n ", $description));
  210. $this->render(sprintf(
  211. '<fg=blue;options=bold>• </><fg=default;options=bold>%s</>: %s %s',
  212. rtrim($title, '.'),
  213. $description,
  214. implode(', ', array_map(function (string $link) {
  215. return sprintf("\n <fg=blue>%s</>", $link);
  216. }, $links))
  217. ));
  218. }
  219. return $this;
  220. }
  221. /**
  222. * Renders the editor containing the code that was the
  223. * origin of the exception.
  224. */
  225. protected function renderEditor(Frame $frame): WriterContract
  226. {
  227. $file = $this->getFileRelativePath((string) $frame->getFile());
  228. // getLine() might return null so cast to int to get 0 instead
  229. $line = (int) $frame->getLine();
  230. $this->render('at <fg=green>' . $file . '</>' . ':<fg=green>' . $line . '</>');
  231. $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());
  232. $this->output->writeln($content);
  233. return $this;
  234. }
  235. /**
  236. * Renders the trace of the exception.
  237. */
  238. protected function renderTrace(array $frames): WriterContract
  239. {
  240. $vendorFrames = 0;
  241. $userFrames = 0;
  242. foreach ($frames as $i => $frame) {
  243. if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {
  244. $vendorFrames++;
  245. continue;
  246. }
  247. if ($userFrames > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
  248. break;
  249. }
  250. $userFrames++;
  251. $file = $this->getFileRelativePath($frame->getFile());
  252. $line = $frame->getLine();
  253. $class = empty($frame->getClass()) ? '' : $frame->getClass() . '::';
  254. $function = $frame->getFunction();
  255. $args = $this->argumentFormatter->format($frame->getArgs());
  256. $pos = str_pad((string) ((int) $i + 1), 4, ' ');
  257. if ($vendorFrames > 0) {
  258. $this->output->write(
  259. sprintf("\n \e[2m+%s vendor frames \e[22m", $vendorFrames)
  260. );
  261. $vendorFrames = 0;
  262. }
  263. $this->render("<fg=yellow>$pos</><fg=default;options=bold>$file</>:<fg=default;options=bold>$line</>");
  264. $this->render("<fg=white> $class$function($args)</>", false);
  265. }
  266. /* Let's consider add this later...
  267. * if ($vendorFrames > 0) {
  268. * $this->output->write(
  269. * sprintf("\n \e[2m+%s vendor frames \e[22m\n", $vendorFrames)
  270. * );
  271. * $vendorFrames = 0;
  272. * }.
  273. */
  274. return $this;
  275. }
  276. /**
  277. * Renders an message into the console.
  278. *
  279. * @return $this
  280. */
  281. protected function render(string $message, bool $break = true): WriterContract
  282. {
  283. if ($break) {
  284. $this->output->writeln('');
  285. }
  286. $this->output->writeln(" $message");
  287. return $this;
  288. }
  289. /**
  290. * Returns the relative path of the given file path.
  291. */
  292. protected function getFileRelativePath(string $filePath): string
  293. {
  294. $cwd = (string) getcwd();
  295. if (!empty($cwd)) {
  296. return str_replace("$cwd/", '', $filePath);
  297. }
  298. return $filePath;
  299. }
  300. }