| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 | <?phpdeclare(strict_types=1);namespace NunoMaduro\Collision;use NunoMaduro\Collision\Contracts\ArgumentFormatter as ArgumentFormatterContract;use NunoMaduro\Collision\Contracts\Highlighter as HighlighterContract;use NunoMaduro\Collision\Contracts\RenderlessEditor;use NunoMaduro\Collision\Contracts\RenderlessTrace;use NunoMaduro\Collision\Contracts\SolutionsRepository;use NunoMaduro\Collision\Contracts\Writer as WriterContract;use NunoMaduro\Collision\SolutionsRepositories\NullSolutionsRepository;use Symfony\Component\Console\Output\ConsoleOutput;use Symfony\Component\Console\Output\OutputInterface;use Whoops\Exception\Frame;use Whoops\Exception\Inspector;/** * @internal * * @see \Tests\Unit\WriterTest */final class Writer implements WriterContract{    /**     * The number of frames if no verbosity is specified.     */    public const VERBOSITY_NORMAL_FRAMES = 1;    /**     * Holds an instance of the solutions repository.     *     * @var \NunoMaduro\Collision\Contracts\SolutionsRepository     */    private $solutionsRepository;    /**     * Holds an instance of the Output.     *     * @var \Symfony\Component\Console\Output\OutputInterface     */    protected $output;    /**     * Holds an instance of the Argument Formatter.     *     * @var \NunoMaduro\Collision\Contracts\ArgumentFormatter     */    protected $argumentFormatter;    /**     * Holds an instance of the Highlighter.     *     * @var \NunoMaduro\Collision\Contracts\Highlighter     */    protected $highlighter;    /**     * Ignores traces where the file string matches one     * of the provided regex expressions.     *     * @var string[]     */    protected $ignore = [];    /**     * Declares whether or not the trace should appear.     *     * @var bool     */    protected $showTrace = true;    /**     * Declares whether or not the title should appear.     *     * @var bool     */    protected $showTitle = true;    /**     * Declares whether or not the editor should appear.     *     * @var bool     */    protected $showEditor = true;    /**     * Creates an instance of the writer.     */    public function __construct(        SolutionsRepository $solutionsRepository = null,        OutputInterface $output = null,        ArgumentFormatterContract $argumentFormatter = null,        HighlighterContract $highlighter = null    ) {        $this->solutionsRepository = $solutionsRepository ?: new NullSolutionsRepository();        $this->output              = $output ?: new ConsoleOutput();        $this->argumentFormatter   = $argumentFormatter ?: new ArgumentFormatter();        $this->highlighter         = $highlighter ?: new Highlighter();    }    /**     * {@inheritdoc}     */    public function write(Inspector $inspector): void    {        $this->renderTitleAndDescription($inspector);        $frames = $this->getFrames($inspector);        $editorFrame = array_shift($frames);        $exception = $inspector->getException();        if ($this->showEditor            && $editorFrame !== null            && !$exception instanceof RenderlessEditor        ) {            $this->renderEditor($editorFrame);        }        $this->renderSolution($inspector);        if ($this->showTrace && !empty($frames) && !$exception instanceof RenderlessTrace) {            $this->renderTrace($frames);        } elseif (!$exception instanceof RenderlessEditor) {            $this->output->writeln('');        }    }    /**     * {@inheritdoc}     */    public function ignoreFilesIn(array $ignore): WriterContract    {        $this->ignore = $ignore;        return $this;    }    /**     * {@inheritdoc}     */    public function showTrace(bool $show): WriterContract    {        $this->showTrace = $show;        return $this;    }    /**     * {@inheritdoc}     */    public function showTitle(bool $show): WriterContract    {        $this->showTitle = $show;        return $this;    }    /**     * {@inheritdoc}     */    public function showEditor(bool $show): WriterContract    {        $this->showEditor = $show;        return $this;    }    /**     * {@inheritdoc}     */    public function setOutput(OutputInterface $output): WriterContract    {        $this->output = $output;        return $this;    }    /**     * {@inheritdoc}     */    public function getOutput(): OutputInterface    {        return $this->output;    }    /**     * Returns pertinent frames.     */    protected function getFrames(Inspector $inspector): array    {        return $inspector->getFrames()            ->filter(                function ($frame) {                    // If we are in verbose mode, we always                    // display the full stack trace.                    if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {                        return true;                    }                    foreach ($this->ignore as $ignore) {                        // Ensure paths are linux-style (like the ones on $this->ignore)                        // @phpstan-ignore-next-line                        $sanitizedPath = (string) str_replace('\\', '/', $frame->getFile());                        if (preg_match($ignore, $sanitizedPath)) {                            return false;                        }                    }                    return true;                }            )            ->getArray();    }    /**     * Renders the title of the exception.     */    protected function renderTitleAndDescription(Inspector $inspector): WriterContract    {        $exception = $inspector->getException();        $message   = rtrim($exception->getMessage());        $class     = $inspector->getExceptionName();        if ($this->showTitle) {            $this->render("<bg=red;options=bold> $class </>");            $this->output->writeln('');        }        $this->output->writeln("<fg=default;options=bold>  $message</>");        return $this;    }    /**     * Renders the solution of the exception, if any.     */    protected function renderSolution(Inspector $inspector): WriterContract    {        $throwable = $inspector->getException();        $solutions = $this->solutionsRepository->getFromThrowable($throwable);        foreach ($solutions as $solution) {            /** @var \Facade\IgnitionContracts\Solution $solution */            $title       = $solution->getSolutionTitle();            $description = $solution->getSolutionDescription();            $links       = $solution->getDocumentationLinks();            $description = trim((string) preg_replace("/\n/", "\n    ", $description));            $this->render(sprintf(                '<fg=blue;options=bold>• </><fg=default;options=bold>%s</>: %s %s',                rtrim($title, '.'),                $description,                implode(', ', array_map(function (string $link) {                    return sprintf("\n    <fg=blue>%s</>", $link);                }, $links))            ));        }        return $this;    }    /**     * Renders the editor containing the code that was the     * origin of the exception.     */    protected function renderEditor(Frame $frame): WriterContract    {        if ($frame->getFile() !== 'Unknown') {            $file = $this->getFileRelativePath((string) $frame->getFile());            // getLine() might return null so cast to int to get 0 instead            $line = (int) $frame->getLine();            $this->render('at <fg=green>' . $file . '</>' . ':<fg=green>' . $line . '</>');            $content = $this->highlighter->highlight((string) $frame->getFileContents(), (int) $frame->getLine());            $this->output->writeln($content);        }        return $this;    }    /**     * Renders the trace of the exception.     */    protected function renderTrace(array $frames): WriterContract    {        $vendorFrames = 0;        $userFrames   = 0;        foreach ($frames as $i => $frame) {            if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {                $vendorFrames++;                continue;            }            if ($userFrames > static::VERBOSITY_NORMAL_FRAMES && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {                break;            }            $userFrames++;            $file     = $this->getFileRelativePath($frame->getFile());            $line     = $frame->getLine();            $class    = empty($frame->getClass()) ? '' : $frame->getClass() . '::';            $function = $frame->getFunction();            $args     = $this->argumentFormatter->format($frame->getArgs());            $pos      = str_pad((string) ((int) $i + 1), 4, ' ');            if ($vendorFrames > 0) {                $this->output->write(                    sprintf("\n      \e[2m+%s vendor frames \e[22m", $vendorFrames)                );                $vendorFrames = 0;            }            $this->render("<fg=yellow>$pos</><fg=default;options=bold>$file</>:<fg=default;options=bold>$line</>");            $this->render("<fg=white>    $class$function($args)</>", false);        }        return $this;    }    /**     * Renders an message into the console.     *     * @return $this     */    protected function render(string $message, bool $break = true): WriterContract    {        if ($break) {            $this->output->writeln('');        }        $this->output->writeln("  $message");        return $this;    }    /**     * Returns the relative path of the given file path.     */    protected function getFileRelativePath(string $filePath): string    {        $cwd = (string) getcwd();        if (!empty($cwd)) {            return str_replace("$cwd/", '', $filePath);        }        return $filePath;    }}
 |