353 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			PHP
		
	
	
<?php
 | 
						|
 | 
						|
declare(strict_types=1);
 | 
						|
 | 
						|
namespace NunoMaduro\Collision;
 | 
						|
 | 
						|
use Closure;
 | 
						|
use NunoMaduro\Collision\Contracts\RenderableOnCollisionEditor;
 | 
						|
use NunoMaduro\Collision\Contracts\RenderlessEditor;
 | 
						|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
 | 
						|
use NunoMaduro\Collision\Contracts\SolutionsRepository;
 | 
						|
use NunoMaduro\Collision\Exceptions\TestException;
 | 
						|
use NunoMaduro\Collision\SolutionsRepositories\NullSolutionsRepository;
 | 
						|
use Symfony\Component\Console\Output\ConsoleOutput;
 | 
						|
use Symfony\Component\Console\Output\OutputInterface;
 | 
						|
use Throwable;
 | 
						|
use Whoops\Exception\Frame;
 | 
						|
use Whoops\Exception\Inspector;
 | 
						|
 | 
						|
/**
 | 
						|
 * @internal
 | 
						|
 *
 | 
						|
 * @see \Tests\Unit\WriterTest
 | 
						|
 */
 | 
						|
final class Writer
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * The number of frames if no verbosity is specified.
 | 
						|
     */
 | 
						|
    public const VERBOSITY_NORMAL_FRAMES = 1;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Holds an instance of the solutions repository.
 | 
						|
     */
 | 
						|
    private SolutionsRepository $solutionsRepository;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Holds an instance of the Output.
 | 
						|
     */
 | 
						|
    private OutputInterface $output;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Holds an instance of the Argument Formatter.
 | 
						|
     */
 | 
						|
    private ArgumentFormatter $argumentFormatter;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Holds an instance of the Highlighter.
 | 
						|
     */
 | 
						|
    private Highlighter $highlighter;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Ignores traces where the file string matches one
 | 
						|
     * of the provided regex expressions.
 | 
						|
     *
 | 
						|
     * @var array<int, string|Closure>
 | 
						|
     */
 | 
						|
    private array $ignore = [];
 | 
						|
 | 
						|
    /**
 | 
						|
     * Declares whether or not the trace should appear.
 | 
						|
     */
 | 
						|
    private bool $showTrace = true;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Declares whether or not the title should appear.
 | 
						|
     */
 | 
						|
    private bool $showTitle = true;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Declares whether the editor should appear.
 | 
						|
     */
 | 
						|
    private bool $showEditor = true;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates an instance of the writer.
 | 
						|
     */
 | 
						|
    public function __construct(
 | 
						|
        ?SolutionsRepository $solutionsRepository = null,
 | 
						|
        ?OutputInterface $output = null,
 | 
						|
        ?ArgumentFormatter $argumentFormatter = null,
 | 
						|
        ?Highlighter $highlighter = null
 | 
						|
    ) {
 | 
						|
        $this->solutionsRepository = $solutionsRepository ?: new NullSolutionsRepository;
 | 
						|
        $this->output = $output ?: new ConsoleOutput;
 | 
						|
        $this->argumentFormatter = $argumentFormatter ?: new ArgumentFormatter;
 | 
						|
        $this->highlighter = $highlighter ?: new Highlighter;
 | 
						|
    }
 | 
						|
 | 
						|
    public function write(Inspector $inspector): void
 | 
						|
    {
 | 
						|
        $this->renderTitleAndDescription($inspector);
 | 
						|
 | 
						|
        $frames = $this->getFrames($inspector);
 | 
						|
 | 
						|
        $exception = $inspector->getException();
 | 
						|
 | 
						|
        if ($exception instanceof RenderableOnCollisionEditor) {
 | 
						|
            $editorFrame = $exception->toCollisionEditor();
 | 
						|
        } else {
 | 
						|
            $editorFrame = array_shift($frames);
 | 
						|
        }
 | 
						|
 | 
						|
        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('');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public function ignoreFilesIn(array $ignore): self
 | 
						|
    {
 | 
						|
        $this->ignore = $ignore;
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    public function showTrace(bool $show): self
 | 
						|
    {
 | 
						|
        $this->showTrace = $show;
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    public function showTitle(bool $show): self
 | 
						|
    {
 | 
						|
        $this->showTitle = $show;
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    public function showEditor(bool $show): self
 | 
						|
    {
 | 
						|
        $this->showEditor = $show;
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    public function setOutput(OutputInterface $output): self
 | 
						|
    {
 | 
						|
        $this->output = $output;
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getOutput(): OutputInterface
 | 
						|
    {
 | 
						|
        return $this->output;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns pertinent frames.
 | 
						|
     *
 | 
						|
     * @return array<int, Frame>
 | 
						|
     */
 | 
						|
    private 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) {
 | 
						|
                        if (is_string($ignore)) {
 | 
						|
                            // Ensure paths are linux-style (like the ones on $this->ignore)
 | 
						|
                            $sanitizedPath = (string) str_replace('\\', '/', $frame->getFile());
 | 
						|
                            if (preg_match($ignore, $sanitizedPath)) {
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
 | 
						|
                        if ($ignore instanceof Closure) {
 | 
						|
                            if ($ignore($frame)) {
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
 | 
						|
                    return true;
 | 
						|
                }
 | 
						|
            )
 | 
						|
            ->getArray();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Renders the title of the exception.
 | 
						|
     */
 | 
						|
    private function renderTitleAndDescription(Inspector $inspector): self
 | 
						|
    {
 | 
						|
        /** @var Throwable|TestException $exception */
 | 
						|
        $exception = $inspector->getException();
 | 
						|
        $message = rtrim($exception->getMessage());
 | 
						|
        $class = $exception instanceof TestException
 | 
						|
            ? $exception->getClassName()
 | 
						|
            : $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.
 | 
						|
     */
 | 
						|
    private function renderSolution(Inspector $inspector): self
 | 
						|
    {
 | 
						|
        $throwable = $inspector->getException();
 | 
						|
 | 
						|
        $solutions = $throwable instanceof Throwable
 | 
						|
            ? $this->solutionsRepository->getFromThrowable($throwable)
 | 
						|
            : []; // @phpstan-ignore-line
 | 
						|
 | 
						|
        foreach ($solutions as $solution) {
 | 
						|
            /** @var \Spatie\Ignition\Contracts\Solution $solution */
 | 
						|
            $title = $solution->getSolutionTitle();
 | 
						|
            $description = $solution->getSolutionDescription();
 | 
						|
            $links = $solution->getDocumentationLinks();
 | 
						|
 | 
						|
            $description = trim((string) preg_replace("/\n/", "\n    ", $description));
 | 
						|
 | 
						|
            $this->render(sprintf(
 | 
						|
                '<fg=cyan;options=bold>i</>   <fg=default;options=bold>%s</>: %s %s',
 | 
						|
                rtrim($title, '.'),
 | 
						|
                $description,
 | 
						|
                implode(', ', array_map(function (string $link) {
 | 
						|
                    return sprintf("\n      <fg=gray>%s</>", $link);
 | 
						|
                }, $links))
 | 
						|
            ));
 | 
						|
        }
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Renders the editor containing the code that was the
 | 
						|
     * origin of the exception.
 | 
						|
     */
 | 
						|
    private function renderEditor(Frame $frame): self
 | 
						|
    {
 | 
						|
        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.
 | 
						|
     */
 | 
						|
    private function renderTrace(array $frames): self
 | 
						|
    {
 | 
						|
        $vendorFrames = 0;
 | 
						|
        $userFrames = 0;
 | 
						|
 | 
						|
        if (! empty($frames)) {
 | 
						|
            $this->output->writeln(['']);
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($frames as $i => $frame) {
 | 
						|
            if ($this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE && strpos($frame->getFile(), '/vendor/') !== false) {
 | 
						|
                $vendorFrames++;
 | 
						|
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if ($userFrames > self::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->writeln(
 | 
						|
                    sprintf("      \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</>", (bool) $class && $i > 0);
 | 
						|
            if ($class) {
 | 
						|
                $this->render("<fg=gray>    $class$function($args)</>", false);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (! empty($frames)) {
 | 
						|
            $this->output->writeln(['']);
 | 
						|
        }
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Renders a message into the console.
 | 
						|
     */
 | 
						|
    private function render(string $message, bool $break = true): self
 | 
						|
    {
 | 
						|
        if ($break) {
 | 
						|
            $this->output->writeln('');
 | 
						|
        }
 | 
						|
 | 
						|
        $this->output->writeln("  $message");
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the relative path of the given file path.
 | 
						|
     */
 | 
						|
    private function getFileRelativePath(string $filePath): string
 | 
						|
    {
 | 
						|
        $cwd = (string) getcwd();
 | 
						|
 | 
						|
        if (! empty($cwd)) {
 | 
						|
            return str_replace("$cwd".DIRECTORY_SEPARATOR, '', $filePath);
 | 
						|
        }
 | 
						|
 | 
						|
        return $filePath;
 | 
						|
    }
 | 
						|
}
 |