Файловый менеджер - Редактировать - /home/easybachat/hisabat365.com/4a7891/src.tar
Ðазад
Arguments/ProvidedArgument.php 0000644 00000006111 15060222625 0012500 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use ReflectionParameter; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\TruncatedReducedArgument; class ProvidedArgument { /** @var string */ public $name; /** @var bool */ public $passedByReference = false; /** @var bool */ public $isVariadic = false; /** @var bool */ public $hasDefaultValue = false; /** @var mixed */ public $defaultValue = null; /** @var bool */ public $defaultValueUsed = false; /** @var bool */ public $truncated = false; /** @var mixed */ public $reducedValue = null; /** @var string|null */ public $originalType = null; public static function fromReflectionParameter(ReflectionParameter $parameter): self { return new self( $parameter->getName(), $parameter->isPassedByReference(), $parameter->isVariadic(), $parameter->isDefaultValueAvailable(), $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, ); } public static function fromNonReflectableParameter( int $index ): self { return new self( "arg{$index}", false, ); } public function __construct( string $name, bool $passedByReference = false, bool $isVariadic = false, bool $hasDefaultValue = false, $defaultValue = null, bool $defaultValueUsed = false, bool $truncated = false, $reducedValue = null, ?string $originalType = null ) { $this->originalType = $originalType; $this->reducedValue = $reducedValue; $this->truncated = $truncated; $this->defaultValueUsed = $defaultValueUsed; $this->defaultValue = $defaultValue; $this->hasDefaultValue = $hasDefaultValue; $this->isVariadic = $isVariadic; $this->passedByReference = $passedByReference; $this->name = $name; if ($this->isVariadic) { $this->defaultValue = []; } } public function setReducedArgument( ReducedArgument $reducedArgument ): self { $this->reducedValue = $reducedArgument->value; $this->originalType = $reducedArgument->originalType; if ($reducedArgument instanceof TruncatedReducedArgument) { $this->truncated = true; } return $this; } public function defaultValueUsed(): self { $this->defaultValueUsed = true; $this->originalType = get_debug_type($this->defaultValue); return $this; } public function toArray(): array { return [ 'name' => $this->name, 'value' => $this->defaultValueUsed ? $this->defaultValue : $this->reducedValue, 'original_type' => $this->originalType, 'passed_by_reference' => $this->passedByReference, 'is_variadic' => $this->isVariadic, 'truncated' => $this->truncated, ]; } } Arguments/Reducers/StdClassArgumentReducer.php 0000644 00000001020 15060222625 0015524 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use stdClass; class StdClassArgumentReducer extends ArrayArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof stdClass) { return UnReducedArgument::create(); } return parent::reduceArgument((array) $argument, stdClass::class); } } Arguments/Reducers/DateTimeZoneArgumentReducer.php 0000644 00000001202 15060222625 0016336 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use DateTimeZone; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class DateTimeZoneArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof DateTimeZone) { return UnReducedArgument::create(); } return new ReducedArgument( $argument->getName(), get_class($argument), ); } } Arguments/Reducers/SymphonyRequestArgumentReducer.php 0000644 00000001267 15060222625 0017220 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Symfony\Component\HttpFoundation\Request; class SymphonyRequestArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if(! $argument instanceof Request) { return UnReducedArgument::create(); } return new ReducedArgument( "{$argument->getMethod()} {$argument->getUri()}", get_class($argument), ); } } Arguments/Reducers/ArrayArgumentReducer.php 0000644 00000003170 15060222625 0015072 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ArgumentReducers; use Spatie\Backtrace\Arguments\ReduceArgumentPayloadAction; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\TruncatedReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class ArrayArgumentReducer implements ReducedArgumentContract { /** @var int */ protected $maxArraySize = 25; /** @var \Spatie\Backtrace\Arguments\ReduceArgumentPayloadAction */ protected $reduceArgumentPayloadAction; public function __construct() { $this->reduceArgumentPayloadAction = new ReduceArgumentPayloadAction(ArgumentReducers::minimal()); } public function execute($argument): ReducedArgumentContract { if (! is_array($argument)) { return UnReducedArgument::create(); } return $this->reduceArgument($argument, 'array'); } protected function reduceArgument(array $argument, string $originalType): ReducedArgumentContract { foreach ($argument as $key => $value) { $argument[$key] = $this->reduceArgumentPayloadAction->reduce( $value, true )->value; } if (count($argument) > $this->maxArraySize) { return new TruncatedReducedArgument( array_slice($argument, 0, $this->maxArraySize), 'array' ); } return new ReducedArgument($argument, $originalType); } } Arguments/Reducers/MinimalArrayArgumentReducer.php 0000644 00000001142 15060222625 0016376 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class MinimalArrayArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if(! is_array($argument)) { return UnReducedArgument::create(); } return new ReducedArgument( 'array (size='.count($argument).')', 'array' ); } } Arguments/Reducers/ArgumentReducer.php 0000644 00000000415 15060222625 0014072 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; interface ArgumentReducer { /** * @param mixed $argument */ public function execute($argument): ReducedArgumentContract; } Arguments/Reducers/SensitiveParameterArrayReducer.php 0000644 00000001312 15060222625 0017116 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use SensitiveParameterValue; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class SensitiveParameterArrayReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof SensitiveParameterValue) { return UnReducedArgument::create(); } return new ReducedArgument( 'SensitiveParameterValue('.get_debug_type($argument->getValue()).')', get_class($argument) ); } } Arguments/Reducers/BaseTypeArgumentReducer.php 0000644 00000001305 15060222625 0015526 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class BaseTypeArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (is_int($argument) || is_float($argument) || is_bool($argument) || is_string($argument) || $argument === null ) { return new ReducedArgument($argument, get_debug_type($argument)); } return UnReducedArgument::create(); } } Arguments/Reducers/EnumArgumentReducer.php 0000644 00000001207 15060222625 0014717 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use UnitEnum; class EnumArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof UnitEnum) { return UnReducedArgument::create(); } return new ReducedArgument( get_class($argument).'::'.$argument->name, get_class($argument), ); } } Arguments/Reducers/ClosureArgumentReducer.php 0000644 00000001715 15060222625 0015433 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Closure; use ReflectionFunction; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class ClosureArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof Closure) { return UnReducedArgument::create(); } $reflection = new ReflectionFunction($argument); if ($reflection->getFileName() && $reflection->getStartLine() && $reflection->getEndLine()) { return new ReducedArgument( "{$reflection->getFileName()}:{$reflection->getStartLine()}-{$reflection->getEndLine()}", 'Closure' ); } return new ReducedArgument("{$reflection->getFileName()}", 'Closure'); } } Arguments/Reducers/DateTimeArgumentReducer.php 0000644 00000001226 15060222625 0015510 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use DateTimeInterface; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class DateTimeArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof DateTimeInterface) { return UnReducedArgument::create(); } return new ReducedArgument( $argument->format('d M Y H:i:s e'), get_class($argument), ); } } Arguments/Reducers/StringableArgumentReducer.php 0000644 00000001172 15060222625 0016106 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Stringable; class StringableArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof Stringable) { return UnReducedArgument::create(); } return new ReducedArgument( (string) $argument, get_class($argument), ); } } Arguments/ArgumentReducers.php 0000644 00000005444 15060222625 0012510 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\BaseTypeArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\ClosureArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\DateTimeArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\DateTimeZoneArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\EnumArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\MinimalArrayArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\SensitiveParameterArrayReducer; use Spatie\Backtrace\Arguments\Reducers\StdClassArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\StringableArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\SymphonyRequestArgumentReducer; class ArgumentReducers { /** @var array<int, ArgumentReducer> */ public $argumentReducers = []; /** * @param array<ArgumentReducer|class-string<ArgumentReducer>> $argumentReducers */ public static function create(array $argumentReducers): self { return new self(array_map( function ($argumentReducer) { /** @var $argumentReducer ArgumentReducer|class-string<ArgumentReducer> */ return $argumentReducer instanceof ArgumentReducer ? $argumentReducer : new $argumentReducer(); }, $argumentReducers )); } public static function default(array $extra = []): self { return new self(static::defaultReducers($extra)); } public static function minimal(array $extra = []): self { return new self(static::minimalReducers($extra)); } /** * @param array<int, ArgumentReducer> $argumentReducers */ protected function __construct(array $argumentReducers) { $this->argumentReducers = $argumentReducers; } protected static function defaultReducers(array $extra = []): array { return array_merge($extra, [ new BaseTypeArgumentReducer(), new ArrayArgumentReducer(), new StdClassArgumentReducer(), new EnumArgumentReducer(), new ClosureArgumentReducer(), new SensitiveParameterArrayReducer(), new DateTimeArgumentReducer(), new DateTimeZoneArgumentReducer(), new SymphonyRequestArgumentReducer(), new StringableArgumentReducer(), ]); } protected static function minimalReducers(array $extra = []): array { return array_merge($extra, [ new BaseTypeArgumentReducer(), new MinimalArrayArgumentReducer(), new EnumArgumentReducer(), new ClosureArgumentReducer(), new SensitiveParameterArrayReducer(), ]); } } Arguments/ReducedArgument/VariadicReducedArgument.php 0000644 00000000771 15060222625 0017026 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; use Exception; class VariadicReducedArgument extends ReducedArgument { public function __construct(array $value) { foreach ($value as $key => $item) { if (! $item instanceof ReducedArgument) { throw new Exception('VariadicReducedArgument must be an array of ReducedArgument'); } $value[$key] = $item->value; } parent::__construct($value, 'array'); } } Arguments/ReducedArgument/ReducedArgumentContract.php 0000644 00000000145 15060222625 0017054 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; interface ReducedArgumentContract { } Arguments/ReducedArgument/ReducedArgument.php 0000644 00000000652 15060222625 0015361 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; class ReducedArgument implements ReducedArgumentContract { /** @var mixed */ public $value; /** @var string */ public $originalType; /** * @param mixed $value */ public function __construct( $value, string $originalType ) { $this->originalType = $originalType; $this->value = $value; } } Arguments/ReducedArgument/TruncatedReducedArgument.php 0000644 00000000172 15060222625 0017230 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; class TruncatedReducedArgument extends ReducedArgument { } Arguments/ReducedArgument/UnReducedArgument.php 0000644 00000000647 15060222625 0015670 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; class UnReducedArgument implements ReducedArgumentContract { /** @var self|null */ private static $instance = null; private function __construct() { } public static function create(): self { if (self::$instance !== null) { return self::$instance; } return self::$instance = new self(); } } Arguments/ReduceArgumentPayloadAction.php 0000644 00000002266 15060222625 0014612 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; class ReduceArgumentPayloadAction { /** @var \Spatie\Backtrace\Arguments\ArgumentReducers */ protected $argumentReducers; public function __construct( ArgumentReducers $argumentReducers ) { $this->argumentReducers = $argumentReducers; } public function reduce($argument, bool $includeObjectType = false): ReducedArgument { foreach ($this->argumentReducers->argumentReducers as $reducer) { $reduced = $reducer->execute($argument); if ($reduced instanceof ReducedArgument) { return $reduced; } } if (gettype($argument) === 'object' && $includeObjectType) { return new ReducedArgument( 'object ('.get_class($argument).')', get_debug_type($argument), ); } if (gettype($argument) === 'object') { return new ReducedArgument('object', get_debug_type($argument), ); } return new ReducedArgument( $argument, get_debug_type($argument), ); } } Arguments/ReduceArgumentsAction.php 0000644 00000007234 15060222625 0013463 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use ReflectionException; use ReflectionFunction; use ReflectionMethod; use ReflectionParameter; use Spatie\Backtrace\Arguments\ReducedArgument\VariadicReducedArgument; use Throwable; class ReduceArgumentsAction { /** @var ArgumentReducers */ protected $argumentReducers; /** @var ReduceArgumentPayloadAction */ protected $reduceArgumentPayloadAction; public function __construct( ArgumentReducers $argumentReducers ) { $this->argumentReducers = $argumentReducers; $this->reduceArgumentPayloadAction = new ReduceArgumentPayloadAction($argumentReducers); } public function execute( ?string $class, ?string $method, ?array $frameArguments ): ?array { try { if ($frameArguments === null) { return null; } $parameters = $this->getParameters($class, $method); if ($parameters === null) { $arguments = []; foreach ($frameArguments as $index => $argument) { $arguments[$index] = ProvidedArgument::fromNonReflectableParameter($index) ->setReducedArgument($this->reduceArgumentPayloadAction->reduce($argument)) ->toArray(); } return $arguments; } $arguments = array_map( function ($argument) { return $this->reduceArgumentPayloadAction->reduce($argument); }, $frameArguments, ); $argumentsCount = count($arguments); $hasVariadicParameter = false; foreach ($parameters as $index => $parameter) { if ($index + 1 > $argumentsCount) { $parameter->defaultValueUsed(); } elseif ($parameter->isVariadic) { $parameter->setReducedArgument(new VariadicReducedArgument(array_slice($arguments, $index))); $hasVariadicParameter = true; } else { $parameter->setReducedArgument($arguments[$index]); } $parameters[$index] = $parameter->toArray(); } if ($this->moreArgumentsProvidedThanParameters($arguments, $parameters, $hasVariadicParameter)) { for ($i = count($parameters); $i < count($arguments); $i++) { $parameters[$i] = ProvidedArgument::fromNonReflectableParameter(count($parameters)) ->setReducedArgument($arguments[$i]) ->toArray(); } } return $parameters; } catch (Throwable $e) { return null; } } /** @return null|Array<\Spatie\Backtrace\Arguments\ProvidedArgument> */ protected function getParameters( ?string $class, ?string $method ): ?array { try { $reflection = $class !== null ? new ReflectionMethod($class, $method) : new ReflectionFunction($method); } catch (ReflectionException $e) { return null; } return array_map( function (ReflectionParameter $reflectionParameter) { return ProvidedArgument::fromReflectionParameter($reflectionParameter); }, $reflection->getParameters(), ); } protected function moreArgumentsProvidedThanParameters( array $arguments, array $parameters, bool $hasVariadicParameter ): bool { return count($arguments) > count($parameters) && ! $hasVariadicParameter; } } Frame.php 0000644 00000004626 15060222625 0006317 0 ustar 00 <?php namespace Spatie\Backtrace; use Spatie\Backtrace\CodeSnippets\CodeSnippet; use Spatie\Backtrace\CodeSnippets\FileSnippetProvider; use Spatie\Backtrace\CodeSnippets\LaravelSerializableClosureSnippetProvider; use Spatie\Backtrace\CodeSnippets\NullSnippetProvider; use Spatie\Backtrace\CodeSnippets\SnippetProvider; class Frame { /** @var string */ public $file; /** @var int */ public $lineNumber; /** @var array|null */ public $arguments = null; /** @var bool */ public $applicationFrame; /** @var string|null */ public $method; /** @var string|null */ public $class; /** @var string|null */ protected $textSnippet; public function __construct( string $file, int $lineNumber, ?array $arguments, string $method = null, string $class = null, bool $isApplicationFrame = false, ?string $textSnippet = null ) { $this->file = $file; $this->lineNumber = $lineNumber; $this->arguments = $arguments; $this->method = $method; $this->class = $class; $this->applicationFrame = $isApplicationFrame; $this->textSnippet = $textSnippet; } public function getSnippet(int $lineCount): array { return (new CodeSnippet()) ->surroundingLine($this->lineNumber) ->snippetLineCount($lineCount) ->get($this->getCodeSnippetProvider()); } public function getSnippetAsString(int $lineCount): string { return (new CodeSnippet()) ->surroundingLine($this->lineNumber) ->snippetLineCount($lineCount) ->getAsString($this->getCodeSnippetProvider()); } public function getSnippetProperties(int $lineCount): array { $snippet = $this->getSnippet($lineCount); return array_map(function (int $lineNumber) use ($snippet) { return [ 'line_number' => $lineNumber, 'text' => $snippet[$lineNumber], ]; }, array_keys($snippet)); } protected function getCodeSnippetProvider(): SnippetProvider { if($this->textSnippet) { return new LaravelSerializableClosureSnippetProvider($this->textSnippet); } if(file_exists($this->file)) { return new FileSnippetProvider($this->file); } return new NullSnippetProvider(); } } Backtrace.php 0000644 00000016107 15060222625 0007141 0 ustar 00 <?php namespace Spatie\Backtrace; use Closure; use Laravel\SerializableClosure\Support\ClosureStream; use Spatie\Backtrace\Arguments\ArgumentReducers; use Spatie\Backtrace\Arguments\ReduceArgumentsAction; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; use Throwable; class Backtrace { /** @var bool */ protected $withArguments = false; /** @var bool */ protected $reduceArguments = false; /** @var array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null */ protected $argumentReducers = null; /** @var bool */ protected $withObject = false; /** @var string|null */ protected $applicationPath; /** @var int */ protected $offset = 0; /** @var int */ protected $limit = 0; /** @var \Closure|null */ protected $startingFromFrameClosure = null; /** @var \Throwable|null */ protected $throwable = null; public static function create(): self { return new static(); } public static function createForThrowable(Throwable $throwable): self { return (new static())->forThrowable($throwable); } protected function forThrowable(Throwable $throwable): self { $this->throwable = $throwable; return $this; } public function withArguments( bool $withArguments = true ): self { $this->withArguments = $withArguments; return $this; } /** * @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers * * @return $this */ public function reduceArguments( $argumentReducers = null ): self { $this->reduceArguments = true; $this->argumentReducers = $argumentReducers; return $this; } public function withObject(): self { $this->withObject = true; return $this; } public function applicationPath(string $applicationPath): self { $this->applicationPath = rtrim($applicationPath, '/'); return $this; } public function offset(int $offset): self { $this->offset = $offset; return $this; } public function limit(int $limit): self { $this->limit = $limit; return $this; } public function startingFromFrame(Closure $startingFromFrameClosure) { $this->startingFromFrameClosure = $startingFromFrameClosure; return $this; } /** * @return \Spatie\Backtrace\Frame[] */ public function frames(): array { $rawFrames = $this->getRawFrames(); return $this->toFrameObjects($rawFrames); } public function firstApplicationFrameIndex(): ?int { foreach ($this->frames() as $index => $frame) { if ($frame->applicationFrame) { return $index; } } return null; } protected function getRawFrames(): array { if ($this->throwable) { return $this->throwable->getTrace(); } $options = null; if (! $this->withArguments) { $options = $options | DEBUG_BACKTRACE_IGNORE_ARGS; } if ($this->withObject()) { $options = $options | DEBUG_BACKTRACE_PROVIDE_OBJECT; } $limit = $this->limit; if ($limit !== 0) { $limit += 3; } return debug_backtrace($options, $limit); } /** * @return \Spatie\Backtrace\Frame[] */ protected function toFrameObjects(array $rawFrames): array { $currentFile = $this->throwable ? $this->throwable->getFile() : ''; $currentLine = $this->throwable ? $this->throwable->getLine() : 0; $arguments = $this->withArguments ? [] : null; $frames = []; $reduceArgumentsAction = new ReduceArgumentsAction($this->resolveArgumentReducers()); foreach ($rawFrames as $rawFrame) { $textSnippet = null; if ( class_exists(ClosureStream::class) && substr($currentFile, 0, strlen(ClosureStream::STREAM_PROTO)) === ClosureStream::STREAM_PROTO ) { $textSnippet = $currentFile; $currentFile = ClosureStream::STREAM_PROTO.'://function()'; $currentLine -= 1; } $frame = new Frame( $currentFile, $currentLine, $arguments, $rawFrame['function'] ?? null, $rawFrame['class'] ?? null, $this->isApplicationFrame($currentFile), $textSnippet ); $frames[] = $frame; $arguments = $this->withArguments ? $rawFrame['args'] ?? null : null; if ($this->reduceArguments) { $arguments = $reduceArgumentsAction->execute( $rawFrame['class'] ?? null, $rawFrame['function'] ?? null, $arguments ); } $currentFile = $rawFrame['file'] ?? 'unknown'; $currentLine = $rawFrame['line'] ?? 0; } $frames[] = new Frame( $currentFile, $currentLine, [], '[top]' ); $frames = $this->removeBacktracePackageFrames($frames); if ($closure = $this->startingFromFrameClosure) { $frames = $this->startAtFrameFromClosure($frames, $closure); } $frames = array_slice($frames, $this->offset, $this->limit === 0 ? PHP_INT_MAX : $this->limit); return array_values($frames); } protected function isApplicationFrame(string $frameFilename): bool { $relativeFile = str_replace('\\', DIRECTORY_SEPARATOR, $frameFilename); if (! empty($this->applicationPath)) { $relativeFile = array_reverse(explode($this->applicationPath ?? '', $frameFilename, 2))[0]; } if (strpos($relativeFile, DIRECTORY_SEPARATOR.'vendor') === 0) { return false; } return true; } protected function removeBacktracePackageFrames(array $frames): array { return $this->startAtFrameFromClosure($frames, function (Frame $frame) { return $frame->class !== static::class; }); } /** * @param \Spatie\Backtrace\Frame[] $frames * @param \Closure $closure * * @return array */ protected function startAtFrameFromClosure(array $frames, Closure $closure): array { foreach ($frames as $i => $frame) { $foundStartingFrame = $closure($frame); if ($foundStartingFrame) { return $frames; } unset($frames[$i]); } return $frames; } protected function resolveArgumentReducers(): ArgumentReducers { if ($this->argumentReducers === null) { return ArgumentReducers::default(); } if ($this->argumentReducers instanceof ArgumentReducers) { return $this->argumentReducers; } return ArgumentReducers::create($this->argumentReducers); } } CodeSnippets/LaravelSerializableClosureSnippetProvider.php 0000644 00000003000 15060222625 0020176 0 ustar 00 <?php namespace Spatie\Backtrace\CodeSnippets; class LaravelSerializableClosureSnippetProvider implements SnippetProvider { /** @var array<string> */ protected $lines; /** @var int */ protected $counter = 0; public function __construct(string $snippet) { $this->lines = preg_split("/\r\n|\n|\r/", $snippet); $this->cleanupLines(); } public function numberOfLines(): int { return count($this->lines); } public function getLine(int $lineNumber = null): string { if (is_null($lineNumber)) { return $this->getNextLine(); } $this->counter = $lineNumber - 1; return $this->lines[$lineNumber - 1]; } public function getNextLine(): string { $this->counter++; if ($this->counter >= count($this->lines)) { return ''; } return $this->lines[$this->counter]; } protected function cleanupLines(): void { $spacesOrTabsToRemove = PHP_INT_MAX; for ($i = 1; $i < count($this->lines); $i++) { if (empty($this->lines[$i])) { continue; } $spacesOrTabsToRemove = min(strspn($this->lines[$i], " \t"), $spacesOrTabsToRemove); } if ($spacesOrTabsToRemove === PHP_INT_MAX) { $spacesOrTabsToRemove = 0; } for ($i = 1; $i < count($this->lines); $i++) { $this->lines[$i] = substr($this->lines[$i], $spacesOrTabsToRemove); } } } CodeSnippets/SnippetProvider.php 0000644 00000000343 15060222625 0013012 0 ustar 00 <?php namespace Spatie\Backtrace\CodeSnippets; interface SnippetProvider { public function numberOfLines(): int; public function getLine(int $lineNumber = null): string; public function getNextLine(): string; } CodeSnippets/NullSnippetProvider.php 0000644 00000000610 15060222625 0013642 0 ustar 00 <?php namespace Spatie\Backtrace\CodeSnippets; class NullSnippetProvider implements SnippetProvider { public function numberOfLines(): int { return 1; } public function getLine(int $lineNumber = null): string { return $this->getNextLine(); } public function getNextLine(): string { return "File not found for code snippet"; } } CodeSnippets/CodeSnippet.php 0000644 00000003666 15060222625 0012105 0 ustar 00 <?php namespace Spatie\Backtrace\CodeSnippets; use RuntimeException; class CodeSnippet { /** @var int */ protected $surroundingLine = 1; /** @var int */ protected $snippetLineCount = 9; public function surroundingLine(int $surroundingLine): self { $this->surroundingLine = $surroundingLine; return $this; } public function snippetLineCount(int $snippetLineCount): self { $this->snippetLineCount = $snippetLineCount; return $this; } public function get(SnippetProvider $provider): array { try { [$startLineNumber, $endLineNumber] = $this->getBounds($provider->numberOfLines()); $code = []; $line = $provider->getLine($startLineNumber); $currentLineNumber = $startLineNumber; while ($currentLineNumber <= $endLineNumber) { $code[$currentLineNumber] = rtrim(substr($line, 0, 250)); $line = $provider->getNextLine(); $currentLineNumber++; } return $code; } catch (RuntimeException $exception) { return []; } } public function getAsString(SnippetProvider $provider): string { $snippet = $this->get($provider); $snippetStrings = array_map(function (string $line, string $number) { return "{$number} {$line}"; }, $snippet, array_keys($snippet)); return implode(PHP_EOL, $snippetStrings); } protected function getBounds(int $totalNumberOfLineInFile): array { $startLine = max($this->surroundingLine - floor($this->snippetLineCount / 2), 1); $endLine = $startLine + ($this->snippetLineCount - 1); if ($endLine > $totalNumberOfLineInFile) { $endLine = $totalNumberOfLineInFile; $startLine = max($endLine - ($this->snippetLineCount - 1), 1); } return [$startLine, $endLine]; } } CodeSnippets/FileSnippetProvider.php 0000644 00000001424 15060222625 0013613 0 ustar 00 <?php namespace Spatie\Backtrace\CodeSnippets; use SplFileObject; class FileSnippetProvider implements SnippetProvider { /** @var \SplFileObject */ protected $file; public function __construct(string $path) { $this->file = new SplFileObject($path); } public function numberOfLines(): int { $this->file->seek(PHP_INT_MAX); return $this->file->key() + 1; } public function getLine(int $lineNumber = null): string { if (is_null($lineNumber)) { return $this->getNextLine(); } $this->file->seek($lineNumber - 1); return $this->file->current(); } public function getNextLine(): string { $this->file->next(); return $this->file->current(); } } Support/Laravel/LivewireComponentParser.php 0000644 00000006225 15060250363 0015152 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support\Laravel; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Livewire\Component; use Livewire\LivewireManager; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; class LivewireComponentParser { /** @var class-string<Component> */ protected string $componentClass; /** @var ReflectionClass<Component> */ protected ReflectionClass $reflectionClass; public static function create(string $componentAlias): self { return new self($componentAlias); } public function __construct(protected string $componentAlias) { $this->componentClass = app(LivewireManager::class)->getClass($this->componentAlias); $this->reflectionClass = new ReflectionClass($this->componentClass); } public function getComponentClass(): string { return $this->componentClass; } /** @return Collection<int, string> */ public function getPropertyNamesLike(string $similar): Collection { $properties = collect($this->reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC)) ->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->class !== $this->reflectionClass->name) ->map(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->name); $computedProperties = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) ->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name) ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'get') && str_ends_with($reflectionMethod->name, 'Property')) ->map(fn (ReflectionMethod $reflectionMethod) => lcfirst(Str::of($reflectionMethod->name)->after('get')->before('Property'))); return $this->filterItemsBySimilarity( $properties->merge($computedProperties), $similar ); } /** @return Collection<int, string> */ public function getMethodNamesLike(string $similar): Collection { $methods = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) ->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name) ->map(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->name); return $this->filterItemsBySimilarity($methods, $similar); } /** * @param Collection<int, string> $items * * @return Collection<int, string> */ protected function filterItemsBySimilarity(Collection $items, string $similar): Collection { return $items ->map(function (string $name) use ($similar) { similar_text($similar, $name, $percentage); return ['match' => $percentage, 'value' => $name]; }) ->sortByDesc('match') ->filter(function (array $item) { return $item['match'] > 40; }) ->map(function (array $item) { return $item['value']; }) ->values(); } } Support/Laravel/StringComparator.php 0000644 00000003050 15060250363 0013613 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support\Laravel; use Illuminate\Support\Collection; class StringComparator { /** * @param array<int|string, string> $strings * @param string $input * @param int $sensitivity * * @return string|null */ public static function findClosestMatch(array $strings, string $input, int $sensitivity = 4): ?string { $closestDistance = -1; $closestMatch = null; foreach ($strings as $string) { $levenshteinDistance = levenshtein($input, $string); if ($levenshteinDistance === 0) { $closestMatch = $string; $closestDistance = 0; break; } if ($levenshteinDistance <= $closestDistance || $closestDistance < 0) { $closestMatch = $string; $closestDistance = $levenshteinDistance; } } if ($closestDistance <= $sensitivity) { return $closestMatch; } return null; } /** * @param array<int, string> $strings * @param string $input * * @return string|null */ public static function findSimilarText(array $strings, string $input): ?string { if (empty($strings)) { return null; } return Collection::make($strings) ->sortByDesc(function (string $string) use ($input) { similar_text($input, $string, $percentage); return $percentage; }) ->first(); } } Support/Laravel/Composer/FakeComposer.php 0000644 00000000703 15060250363 0014464 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support\Laravel\Composer; class FakeComposer implements Composer { /** @return array<string, mixed> */ public function getClassMap(): array { return []; } /** @return array<string, mixed> */ public function getPrefixes(): array { return []; } /** @return array<string, mixed> */ public function getPrefixesPsr4(): array { return []; } } Support/Laravel/Composer/Composer.php 0000644 00000000525 15060250363 0013677 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support\Laravel\Composer; interface Composer { /** @return array<string, mixed> */ public function getClassMap(): array; /** @return array<string, mixed> */ public function getPrefixes(): array; /** @return array<string, mixed> */ public function getPrefixesPsr4(): array; } Support/Laravel/Composer/ComposerClassMap.php 0000644 00000007373 15060250363 0015333 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support\Laravel\Composer; use function app_path; use function base_path; use Illuminate\Support\Str; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; class ComposerClassMap { /** @var \Spatie\ErrorSolutions\Support\Laravel\Composer\Composer */ protected object $composer; protected string $basePath; public function __construct(?string $autoloaderPath = null) { $autoloaderPath = $autoloaderPath ?? base_path('/vendor/autoload.php'); $this->composer = file_exists($autoloaderPath) ? require $autoloaderPath : new FakeComposer(); $this->basePath = app_path(); } /** @return array<string, string> */ public function listClasses(): array { $classes = $this->composer->getClassMap(); return array_merge($classes, $this->listClassesInPsrMaps()); } public function searchClassMap(string $missingClass): ?string { foreach ($this->composer->getClassMap() as $fqcn => $file) { $basename = basename($file, '.php'); if ($basename === $missingClass) { return $fqcn; } } return null; } /** @return array<string, mixed> */ public function listClassesInPsrMaps(): array { // TODO: This is incorrect. Doesnt list all fqcns. Need to parse namespace? e.g. App\LoginController is wrong $prefixes = array_merge( $this->composer->getPrefixes(), $this->composer->getPrefixesPsr4() ); $classes = []; foreach ($prefixes as $namespace => $directories) { foreach ($directories as $directory) { if (file_exists($directory)) { $files = (new Finder) ->in($directory) ->files() ->name('*.php'); foreach ($files as $file) { if ($file instanceof SplFileInfo) { $fqcn = $this->getFullyQualifiedClassNameFromFile($namespace, $file); $classes[$fqcn] = $file->getRelativePathname(); } } } } } return $classes; } public function searchPsrMaps(string $missingClass): ?string { $prefixes = array_merge( $this->composer->getPrefixes(), $this->composer->getPrefixesPsr4() ); foreach ($prefixes as $namespace => $directories) { foreach ($directories as $directory) { if (file_exists($directory)) { $files = (new Finder) ->in($directory) ->files() ->name('*.php'); foreach ($files as $file) { if ($file instanceof SplFileInfo) { $basename = basename($file->getRelativePathname(), '.php'); if ($basename === $missingClass) { return $namespace . basename($file->getRelativePathname(), '.php'); } } } } } } return null; } protected function getFullyQualifiedClassNameFromFile(string $rootNamespace, SplFileInfo $file): string { $class = trim(str_replace($this->basePath, '', (string)$file->getRealPath()), DIRECTORY_SEPARATOR); $class = str_replace( [DIRECTORY_SEPARATOR, 'App\\'], ['\\', app()->getNamespace()], ucfirst(Str::replaceLast('.php', '', $class)) ); return $rootNamespace . $class; } } Support/Laravel/LaravelVersion.php 0000644 00000000273 15060250363 0013255 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support\Laravel; class LaravelVersion { public static function major(): string { return explode('.', app()->version())[0]; } } Support/AiPromptRenderer.php 0000644 00000001076 15060250363 0012157 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Support; class AiPromptRenderer { /** * @param array<string, mixed> $data * * @return void */ public function render(array $data, string $viewPath): void { $viewFile = $viewPath; extract($data, EXTR_OVERWRITE); include $viewFile; } /** * @param array<string, mixed> $data */ public function renderAsString(array $data, string $viewPath): string { ob_start(); $this->render($data, $viewPath); return ob_get_clean(); } } SolutionProviders/Laravel/IncorrectValetDbCredentialsSolutionProvider.php 0000644 00000003154 15060250363 0023200 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\QueryException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\UseDefaultValetDbCredentialsSolution; use Throwable; class IncorrectValetDbCredentialsSolutionProvider implements HasSolutionsForThrowable { const MYSQL_ACCESS_DENIED_CODE = 1045; public function canSolve(Throwable $throwable): bool { if (PHP_OS !== 'Darwin') { return false; } if (! $throwable instanceof QueryException) { return false; } if (! $this->isAccessDeniedCode($throwable->getCode())) { return false; } if (! $this->envFileExists()) { return false; } if (! $this->isValetInstalled()) { return false; } if ($this->usingCorrectDefaultCredentials()) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [new UseDefaultValetDbCredentialsSolution()]; } protected function envFileExists(): bool { return file_exists(base_path('.env')); } protected function isAccessDeniedCode(string $code): bool { return $code === static::MYSQL_ACCESS_DENIED_CODE; } protected function isValetInstalled(): bool { return file_exists('/usr/local/bin/valet'); } protected function usingCorrectDefaultCredentials(): bool { return env('DB_USERNAME') === 'root' && env('DB_PASSWORD') === ''; } } SolutionProviders/Laravel/UndefinedLivewirePropertySolutionProvider.php 0000644 00000003234 15060250363 0023004 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Livewire\Exceptions\PropertyNotFoundException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\SuggestLivewirePropertyNameSolution; use Spatie\ErrorSolutions\Support\Laravel\LivewireComponentParser; use Throwable; class UndefinedLivewirePropertySolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return $throwable instanceof PropertyNotFoundException; } public function getSolutions(Throwable $throwable): array { ['variable' => $variable, 'component' => $component] = $this->getMethodAndComponent($throwable); if ($variable === null || $component === null) { return []; } $parsed = LivewireComponentParser::create($component); return $parsed->getPropertyNamesLike($variable) ->map(function (string $suggested) use ($parsed, $variable) { return new SuggestLivewirePropertyNameSolution( $variable, $parsed->getComponentClass(), '$'.$suggested ); }) ->toArray(); } /** * @param \Throwable $throwable * * @return array<string, string|null> */ protected function getMethodAndComponent(Throwable $throwable): array { preg_match_all('/\[([\d\w\-_\$]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER, 0); return [ 'variable' => $matches[0][1] ?? null, 'component' => $matches[1][1] ?? null, ]; } } SolutionProviders/Laravel/ViewNotFoundSolutionProvider.php 0000644 00000007466 15060250363 0020231 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Support\Arr; use Illuminate\Support\Facades\View; use InvalidArgumentException; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Support\Laravel\StringComparator; use Spatie\Ignition\Exceptions\ViewException as IgnitionViewException; use Spatie\LaravelFlare\Exceptions\ViewException as FlareViewException; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Throwable; class ViewNotFoundSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/View \[(.*)\] not found/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof InvalidArgumentException && (! $throwable instanceof IgnitionViewException || ! $throwable instanceof FlareViewException)) { return false; } return (bool)preg_match(self::REGEX, $throwable->getMessage(), $matches); } public function getSolutions(Throwable $throwable): array { preg_match(self::REGEX, $throwable->getMessage(), $matches); $missingView = $matches[1] ?? null; $suggestedView = $this->findRelatedView($missingView); if ($suggestedView) { return [ BaseSolution::create() ->setSolutionTitle("{$missingView} was not found.") ->setSolutionDescription("Did you mean `{$suggestedView}`?"), ]; } return [ BaseSolution::create() ->setSolutionTitle("{$missingView} was not found.") ->setSolutionDescription('Are you sure the view exists and is a `.blade.php` file?'), ]; } protected function findRelatedView(string $missingView): ?string { $views = $this->getAllViews(); return StringComparator::findClosestMatch($views, $missingView); } /** @return array<int, string> */ protected function getAllViews(): array { /** @var \Illuminate\View\FileViewFinder $fileViewFinder */ $fileViewFinder = View::getFinder(); $extensions = $fileViewFinder->getExtensions(); $viewsForHints = collect($fileViewFinder->getHints()) ->flatMap(function ($paths, string $namespace) use ($extensions) { $paths = Arr::wrap($paths); return collect($paths) ->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions)) ->map(fn (string $view) => "{$namespace}::{$view}") ->toArray(); }); $viewsForViewPaths = collect($fileViewFinder->getPaths()) ->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions)); return $viewsForHints->merge($viewsForViewPaths)->toArray(); } /** * @param string $path * @param array<int, string> $extensions * * @return array<int, string> */ protected function getViewsInPath(string $path, array $extensions): array { $filePatterns = array_map(fn (string $extension) => "*.{$extension}", $extensions); $extensionsWithDots = array_map(fn (string $extension) => ".{$extension}", $extensions); $files = (new Finder()) ->in($path) ->files(); foreach ($filePatterns as $filePattern) { $files->name($filePattern); } $views = []; foreach ($files as $file) { if ($file instanceof SplFileInfo) { $view = $file->getRelativePathname(); $view = str_replace($extensionsWithDots, '', $view); $view = str_replace('/', '.', $view); $views[] = $view; } } return $views; } } SolutionProviders/Laravel/MissingAppKeySolutionProvider.php 0000644 00000001264 15060250363 0020353 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use RuntimeException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\GenerateAppKeySolution; use Throwable; class MissingAppKeySolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof RuntimeException) { return false; } return $throwable->getMessage() === 'No application encryption key has been specified.'; } public function getSolutions(Throwable $throwable): array { return [new GenerateAppKeySolution()]; } } SolutionProviders/Laravel/MissingLivewireComponentSolutionProvider.php 0000644 00000002130 15060250363 0022624 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Livewire\Exceptions\ComponentNotFoundException; use Livewire\LivewireComponentsFinder; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\LivewireDiscoverSolution; use Throwable; class MissingLivewireComponentSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! $this->livewireIsInstalled()) { return false; } if (! $throwable instanceof ComponentNotFoundException) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [new LivewireDiscoverSolution('A Livewire component was not found')]; } public function livewireIsInstalled(): bool { if (! class_exists(ComponentNotFoundException::class)) { return false; } if (! class_exists(LivewireComponentsFinder::class)) { return false; } return true; } } SolutionProviders/Laravel/OpenAiSolutionProvider.php 0000644 00000002127 15060250363 0017002 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Support\Str; use OpenAI\Client; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\OpenAi\OpenAiSolutionProvider as BaseOpenAiSolutionProvider; use Throwable; class OpenAiSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! class_exists(Client::class)) { return false; } if (config('ErrorSolutions.open_ai_key') === null) { return false; } return true; } public function getSolutions(Throwable $throwable): array { $solutionProvider = new BaseOpenAiSolutionProvider( openAiKey: config('ErrorSolutions.open_ai_key'), cache: cache()->store(config('cache.default')), cacheTtlInSeconds: 60, applicationType: 'Laravel ' . Str::before(app()->version(), '.'), applicationPath: base_path(), ); return $solutionProvider->getSolutions($throwable); } } SolutionProviders/Laravel/UndefinedLivewireMethodSolutionProvider.php 0000644 00000003136 15060250363 0022401 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Livewire\Exceptions\MethodNotFoundException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\SuggestLivewireMethodNameSolution; use Spatie\ErrorSolutions\Support\Laravel\LivewireComponentParser; use Throwable; class UndefinedLivewireMethodSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return $throwable instanceof MethodNotFoundException; } public function getSolutions(Throwable $throwable): array { ['methodName' => $methodName, 'component' => $component] = $this->getMethodAndComponent($throwable); if ($methodName === null || $component === null) { return []; } $parsed = LivewireComponentParser::create($component); return $parsed->getMethodNamesLike($methodName) ->map(function (string $suggested) use ($parsed, $methodName) { return new SuggestLivewireMethodNameSolution( $methodName, $parsed->getComponentClass(), $suggested ); }) ->toArray(); } /** @return array<string, string|null> */ protected function getMethodAndComponent(Throwable $throwable): array { preg_match_all('/\[([\d\w\-_]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER); return [ 'methodName' => $matches[0][1] ?? null, 'component' => $matches[1][1] ?? null, ]; } } SolutionProviders/Laravel/LazyLoadingViolationSolutionProvider.php 0000644 00000002613 15060250363 0021731 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\LazyLoadingViolationException; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Support\Laravel\LaravelVersion; use Throwable; class LazyLoadingViolationSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if ($throwable instanceof LazyLoadingViolationException) { return true; } if (! $previous = $throwable->getPrevious()) { return false; } return $previous instanceof LazyLoadingViolationException; } public function getSolutions(Throwable $throwable): array { $majorVersion = LaravelVersion::major(); return [BaseSolution::create( 'Lazy loading was disabled to detect N+1 problems' ) ->setSolutionDescription( 'Either avoid lazy loading the relation or allow lazy loading.' ) ->setDocumentationLinks([ 'Read the docs on preventing lazy loading' => "https://laravel.com/docs/{$majorVersion}.x/eloquent-relationships#preventing-lazy-loading", 'Watch a video on how to deal with the N+1 problem' => 'https://www.youtube.com/watch?v=ZE7KBeraVpc', ]),]; } } SolutionProviders/Laravel/RunningLaravelDuskInProductionProvider.php 0000644 00000002133 15060250363 0022203 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Exception; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class RunningLaravelDuskInProductionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof Exception) { return false; } return $throwable->getMessage() === 'It is unsafe to run Dusk in production.'; } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create() ->setSolutionTitle('Laravel Dusk should not be run in production.') ->setSolutionDescription('Install the dependencies with the `--no-dev` flag.'), BaseSolution::create() ->setSolutionTitle('Laravel Dusk can be run in other environments.') ->setSolutionDescription('Consider setting the `APP_ENV` to something other than `production` like `local` for example.'), ]; } } SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 0000644 00000007275 15060250363 0022042 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Contracts\Solution; use Spatie\ErrorSolutions\Solutions\Laravel\MakeViewVariableOptionalSolution; use Spatie\ErrorSolutions\Solutions\Laravel\SuggestCorrectVariableNameSolution; use Spatie\LaravelFlare\Exceptions\ViewException as FlareViewException; use Spatie\LaravelIgnition\Exceptions\ViewException as IgnitionViewException; use Throwable; class UndefinedViewVariableSolutionProvider implements HasSolutionsForThrowable { protected string $variableName; protected string $viewFile; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof IgnitionViewException && ! $throwable instanceof FlareViewException) { return false; } return $this->getNameAndView($throwable) !== null; } public function getSolutions(Throwable $throwable): array { $solutions = []; /** @phpstan-ignore-next-line */ extract($this->getNameAndView($throwable)); if (! isset($variableName)) { return []; } if (isset($viewFile)) { /** @phpstan-ignore-next-line */ $solutions = $this->findCorrectVariableSolutions($throwable, $variableName, $viewFile); $solutions[] = $this->findOptionalVariableSolution($variableName, $viewFile); } return $solutions; } /** * @param IgnitionViewException|FlareViewException $throwable * @param string $variableName * @param string $viewFile * * @return array<int, \Spatie\ErrorSolutions\Contracts\Solution> */ protected function findCorrectVariableSolutions( IgnitionViewException|FlareViewException $throwable, string $variableName, string $viewFile ): array { return collect($throwable->getViewData()) ->map(function ($value, $key) use ($variableName) { similar_text($variableName, $key, $percentage); return ['match' => $percentage, 'value' => $value]; }) ->sortByDesc('match') ->filter(fn ($var) => $var['match'] > 40) ->keys() ->map(fn ($suggestion) => new SuggestCorrectVariableNameSolution($variableName, $viewFile, $suggestion)) ->map(function ($solution) { return $solution->isRunnable() ? $solution : BaseSolution::create($solution->getSolutionTitle()) ->setSolutionDescription($solution->getSolutionDescription()); }) ->toArray(); } protected function findOptionalVariableSolution(string $variableName, string $viewFile): Solution { $optionalSolution = new MakeViewVariableOptionalSolution($variableName, $viewFile); return $optionalSolution->isRunnable() ? $optionalSolution : BaseSolution::create($optionalSolution->getSolutionTitle()) ->setSolutionDescription($optionalSolution->getSolutionDescription()); } /** * @param \Throwable $throwable * * @return array<string, string>|null */ protected function getNameAndView(Throwable $throwable): ?array { $pattern = '/Undefined variable:? (.*?) \(View: (.*?)\)/'; preg_match($pattern, $throwable->getMessage(), $matches); if (count($matches) === 3) { [, $variableName, $viewFile] = $matches; $variableName = ltrim($variableName, '$'); return compact('variableName', 'viewFile'); } return null; } } SolutionProviders/Laravel/UnknownMariadbCollationSolutionProvider.php 0000644 00000001661 15060250363 0022415 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\QueryException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\SuggestUsingMariadbDatabaseSolution; use Throwable; class UnknownMariadbCollationSolutionProvider implements HasSolutionsForThrowable { const MYSQL_UNKNOWN_COLLATION_CODE = 1273; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } if ($throwable->getCode() !== self::MYSQL_UNKNOWN_COLLATION_CODE) { return false; } return str_contains( $throwable->getMessage(), 'Unknown collation: \'utf8mb4_uca1400_ai_ci\'' ); } public function getSolutions(Throwable $throwable): array { return [new SuggestUsingMariadbDatabaseSolution()]; } } SolutionProviders/Laravel/InvalidRouteActionSolutionProvider.php 0000644 00000005760 15060250363 0021400 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Support\Str; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Support\Laravel\Composer\ComposerClassMap; use Spatie\ErrorSolutions\Support\Laravel\StringComparator; use Throwable; use UnexpectedValueException; class InvalidRouteActionSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/\[([a-zA-Z\\\\]+)\]/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof UnexpectedValueException) { return false; } if (! preg_match(self::REGEX, $throwable->getMessage(), $matches)) { return false; } return Str::startsWith($throwable->getMessage(), 'Invalid route action: '); } public function getSolutions(Throwable $throwable): array { preg_match(self::REGEX, $throwable->getMessage(), $matches); $invalidController = $matches[1] ?? null; if (! $invalidController) { return []; } $suggestedController = $this->findRelatedController($invalidController); if ($suggestedController === $invalidController) { return [ BaseSolution::create("`{$invalidController}` is not invokable.") ->setSolutionDescription("The controller class `{$invalidController}` is not invokable. Did you forget to add the `__invoke` method or is the controller's method missing in your routes file?"), ]; } if ($suggestedController) { return [ BaseSolution::create("`{$invalidController}` was not found.") ->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Did you mean `{$suggestedController}`?"), ]; } return [ BaseSolution::create("`{$invalidController}` was not found.") ->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Are you sure this controller exists and is imported correctly?"), ]; } protected function findRelatedController(string $invalidController): ?string { $composerClassMap = app(ComposerClassMap::class); $controllers = collect($composerClassMap->listClasses()) ->filter(function (string $file, string $fqcn) { return Str::endsWith($fqcn, 'Controller'); }) ->mapWithKeys(function (string $file, string $fqcn) { return [$fqcn => class_basename($fqcn)]; }) ->toArray(); $basenameMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4); $controllers = array_flip($controllers); $fqcnMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4); return $fqcnMatch ?? $basenameMatch; } } SolutionProviders/Laravel/SailNetworkSolutionProvider.php 0000644 00000002030 15060250363 0020062 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class SailNetworkSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return app()->runningInConsole() && str_contains($throwable->getMessage(), 'php_network_getaddresses') && file_exists(base_path('vendor/bin/sail')) && file_exists(base_path('docker-compose.yml')) && env('LARAVEL_SAIL') === null; } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create('Network address not found') ->setSolutionDescription('Did you mean to use `sail artisan`?') ->setDocumentationLinks([ 'Sail: Executing Artisan Commands' => 'https://laravel.com/docs/sail#executing-artisan-commands', ]), ]; } } SolutionProviders/Laravel/RouteNotDefinedSolutionProvider.php 0000644 00000003206 15060250363 0020664 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Support\Facades\Route; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Support\Laravel\StringComparator; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Throwable; class RouteNotDefinedSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/Route \[(.*)\] not defined/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof RouteNotFoundException) { return false; } return (bool)preg_match(self::REGEX, $throwable->getMessage(), $matches); } public function getSolutions(Throwable $throwable): array { preg_match(self::REGEX, $throwable->getMessage(), $matches); $missingRoute = $matches[1] ?? ''; $suggestedRoute = $this->findRelatedRoute($missingRoute); if ($suggestedRoute) { return [ BaseSolution::create("{$missingRoute} was not defined.") ->setSolutionDescription("Did you mean `{$suggestedRoute}`?"), ]; } return [ BaseSolution::create("{$missingRoute} was not defined.") ->setSolutionDescription('Are you sure that the route is defined'), ]; } protected function findRelatedRoute(string $missingRoute): ?string { Route::getRoutes()->refreshNameLookups(); return StringComparator::findClosestMatch(array_keys(Route::getRoutes()->getRoutesByName()), $missingRoute); } } SolutionProviders/Laravel/UnknownMysql8CollationSolutionProvider.php 0000644 00000001653 15060250363 0022254 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\QueryException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\SuggestUsingMysql8DatabaseSolution; use Throwable; class UnknownMysql8CollationSolutionProvider implements HasSolutionsForThrowable { const MYSQL_UNKNOWN_COLLATION_CODE = 1273; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } if ($throwable->getCode() !== self::MYSQL_UNKNOWN_COLLATION_CODE) { return false; } return str_contains( $throwable->getMessage(), 'Unknown collation: \'utf8mb4_0900_ai_ci\'' ); } public function getSolutions(Throwable $throwable): array { return [new SuggestUsingMysql8DatabaseSolution()]; } } SolutionProviders/Laravel/MissingColumnSolutionProvider.php 0000644 00000001743 15060250363 0020421 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\QueryException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\RunMigrationsSolution; use Throwable; class MissingColumnSolutionProvider implements HasSolutionsForThrowable { /** * See https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_bad_field_error. */ const MYSQL_BAD_FIELD_CODE = '42S22'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } return $this->isBadTableErrorCode($throwable->getCode()); } protected function isBadTableErrorCode(string $code): bool { return $code === static::MYSQL_BAD_FIELD_CODE; } public function getSolutions(Throwable $throwable): array { return [new RunMigrationsSolution('A column was not found')]; } } SolutionProviders/Laravel/GenericLaravelExceptionSolutionProvider.php 0000644 00000003447 15060250363 0022377 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Broadcasting\BroadcastException; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Support\Laravel\LaravelVersion; use Throwable; class GenericLaravelExceptionSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return ! is_null($this->getSolutionTexts($throwable)); } public function getSolutions(Throwable $throwable): array { if (! $texts = $this->getSolutionTexts($throwable)) { return []; } $solution = BaseSolution::create($texts['title']) ->setSolutionDescription($texts['description']) ->setDocumentationLinks($texts['links']); return ([$solution]); } /** * @param \Throwable $throwable * * @return array<string, mixed>|null */ protected function getSolutionTexts(Throwable $throwable) : ?array { foreach ($this->getSupportedExceptions() as $supportedClass => $texts) { if ($throwable instanceof $supportedClass) { return $texts; } } return null; } /** @return array<string, mixed> */ protected function getSupportedExceptions(): array { $majorVersion = LaravelVersion::major(); return [ BroadcastException::class => [ 'title' => 'Here are some links that might help solve this problem', 'description' => '', 'links' => [ 'Laravel docs on authentication' => "https://laravel.com/docs/{$majorVersion}.x/authentication", ], ], ]; } } SolutionProviders/Laravel/UnknownValidationSolutionProvider.php 0000644 00000004754 15060250363 0021311 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use BadMethodCallException; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Validation\Validator; use ReflectionClass; use ReflectionMethod; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Support\Laravel\StringComparator; use Throwable; class UnknownValidationSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/Illuminate\\\\Validation\\\\Validator::(?P<method>validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof BadMethodCallException) { return false; } return ! is_null($this->getMethodFromExceptionMessage($throwable->getMessage())); } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create() ->setSolutionTitle('Unknown Validation Rule') ->setSolutionDescription($this->getSolutionDescription($throwable)), ]; } protected function getSolutionDescription(Throwable $throwable): string { $method = (string)$this->getMethodFromExceptionMessage($throwable->getMessage()); $possibleMethod = StringComparator::findSimilarText( $this->getAvailableMethods()->toArray(), $method ); if (empty($possibleMethod)) { return ''; } $rule = Str::snake(str_replace('validate', '', $possibleMethod)); return "Did you mean `{$rule}` ?"; } protected function getMethodFromExceptionMessage(string $message): ?string { if (! preg_match(self::REGEX, $message, $matches)) { return null; } return $matches['method']; } protected function getAvailableMethods(): Collection { $class = new ReflectionClass(Validator::class); $extensions = Collection::make((app('validator')->make([], []))->extensions) ->keys() ->map(fn (string $extension) => 'validate'.Str::studly($extension)); return Collection::make($class->getMethods()) ->filter(fn (ReflectionMethod $method) => preg_match('/(validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/', $method->name)) ->map(fn (ReflectionMethod $method) => $method->name) ->merge($extensions); } } SolutionProviders/Laravel/MissingImportSolutionProvider.php 0000644 00000002657 15060250363 0020443 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\SuggestImportSolution; use Spatie\ErrorSolutions\Support\Laravel\Composer\ComposerClassMap; use Throwable; class MissingImportSolutionProvider implements HasSolutionsForThrowable { protected ?string $foundClass; protected ComposerClassMap $composerClassMap; public function canSolve(Throwable $throwable): bool { $pattern = '/Class \"([^\s]+)\" not found/m'; if (! preg_match($pattern, $throwable->getMessage(), $matches)) { return false; } $class = $matches[1]; $this->composerClassMap = new ComposerClassMap(); $this->search($class); return ! is_null($this->foundClass); } /** * @param \Throwable $throwable * * @return array<int, SuggestImportSolution> */ public function getSolutions(Throwable $throwable): array { if (is_null($this->foundClass)) { return []; } return [new SuggestImportSolution($this->foundClass)]; } protected function search(string $missingClass): void { $this->foundClass = $this->composerClassMap->searchClassMap($missingClass); if (is_null($this->foundClass)) { $this->foundClass = $this->composerClassMap->searchPsrMaps($missingClass); } } } SolutionProviders/Laravel/DefaultDbNameSolutionProvider.php 0000644 00000001632 15060250363 0020262 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\QueryException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\SuggestUsingCorrectDbNameSolution; use Throwable; class DefaultDbNameSolutionProvider implements HasSolutionsForThrowable { const MYSQL_UNKNOWN_DATABASE_CODE = 1049; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } if ($throwable->getCode() !== self::MYSQL_UNKNOWN_DATABASE_CODE) { return false; } if (! in_array(env('DB_DATABASE'), ['homestead', 'laravel'])) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [new SuggestUsingCorrectDbNameSolution()]; } } SolutionProviders/Laravel/MissingMixManifestSolutionProvider.php 0000644 00000001320 15060250363 0021377 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Support\Str; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class MissingMixManifestSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return Str::startsWith($throwable->getMessage(), 'Mix manifest not found'); } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create('Missing Mix Manifest File') ->setSolutionDescription('Did you forget to run `npm install && npm run dev`?'), ]; } } SolutionProviders/Laravel/MissingViteManifestSolutionProvider.php 0000644 00000003470 15060250363 0021561 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Support\Str; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Contracts\Solution; use Throwable; class MissingViteManifestSolutionProvider implements HasSolutionsForThrowable { /** @var array<string, string> */ protected array $links = [ 'Asset bundling with Vite' => 'https://laravel.com/docs/9.x/vite#running-vite', ]; public function canSolve(Throwable $throwable): bool { return Str::startsWith($throwable->getMessage(), 'Vite manifest not found'); } public function getSolutions(Throwable $throwable): array { return [ $this->getSolution(), ]; } public function getSolution(): Solution { /** @var string */ $baseCommand = collect([ 'pnpm-lock.yaml' => 'pnpm', 'yarn.lock' => 'yarn', ])->first(fn ($_, $lockfile) => file_exists(base_path($lockfile)), 'npm run'); return app()->environment('local') ? $this->getLocalSolution($baseCommand) : $this->getProductionSolution($baseCommand); } protected function getLocalSolution(string $baseCommand): Solution { return BaseSolution::create('Start the development server') ->setSolutionDescription("Run `{$baseCommand} dev` in your terminal and refresh the page.") ->setDocumentationLinks($this->links); } protected function getProductionSolution(string $baseCommand): Solution { return BaseSolution::create('Build the production assets') ->setSolutionDescription("Run `{$baseCommand} build` in your deployment script.") ->setDocumentationLinks($this->links); } } SolutionProviders/Laravel/TableNotFoundSolutionProvider.php 0000644 00000001742 15060250363 0020335 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders\Laravel; use Illuminate\Database\QueryException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Solutions\Laravel\RunMigrationsSolution; use Throwable; class TableNotFoundSolutionProvider implements HasSolutionsForThrowable { /** * See https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_bad_table_error. */ const MYSQL_BAD_TABLE_CODE = '42S02'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } return $this->isBadTableErrorCode($throwable->getCode()); } protected function isBadTableErrorCode(string $code): bool { return $code === static::MYSQL_BAD_TABLE_CODE; } public function getSolutions(Throwable $throwable): array { return [new RunMigrationsSolution('A table was not found')]; } } SolutionProviders/UndefinedPropertySolutionProvider.php 0000644 00000006736 15060250363 0017721 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders; use ErrorException; use Illuminate\Support\Collection; use ReflectionClass; use ReflectionProperty; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class UndefinedPropertySolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/([a-zA-Z\\\\]+)::\$([a-zA-Z]+)/m'; protected const MINIMUM_SIMILARITY = 80; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof ErrorException) { return false; } if (is_null($this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()))) { return false; } if (! $this->similarPropertyExists($throwable)) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create('Unknown Property') ->setSolutionDescription($this->getSolutionDescription($throwable)), ]; } public function getSolutionDescription(Throwable $throwable): string { if (! $this->canSolve($throwable) || ! $this->similarPropertyExists($throwable)) { return ''; } extract( /** @phpstan-ignore-next-line */ $this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE, ); $possibleProperty = $this->findPossibleProperty($class ?? '', $property ?? ''); $class = $class ?? ''; return "Did you mean {$class}::\${$possibleProperty->name} ?"; } protected function similarPropertyExists(Throwable $throwable): bool { /** @phpstan-ignore-next-line */ extract($this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE); $possibleProperty = $this->findPossibleProperty($class ?? '', $property ?? ''); return $possibleProperty !== null; } /** * @param string $message * * @return null|array<string, string> */ protected function getClassAndPropertyFromExceptionMessage(string $message): ?array { if (! preg_match(self::REGEX, $message, $matches)) { return null; } return [ 'class' => $matches[1], 'property' => $matches[2], ]; } /** * @param class-string $class * @param string $invalidPropertyName * * @return mixed */ protected function findPossibleProperty(string $class, string $invalidPropertyName): mixed { return $this->getAvailableProperties($class) ->sortByDesc(function (ReflectionProperty $property) use ($invalidPropertyName) { similar_text($invalidPropertyName, $property->name, $percentage); return $percentage; }) ->filter(function (ReflectionProperty $property) use ($invalidPropertyName) { similar_text($invalidPropertyName, $property->name, $percentage); return $percentage >= self::MINIMUM_SIMILARITY; })->first(); } /** * @param class-string $class * * @return Collection<int, ReflectionProperty> */ protected function getAvailableProperties(string $class): Collection { $class = new ReflectionClass($class); return Collection::make($class->getProperties()); } } SolutionProviders/MergeConflictSolutionProvider.php 0000644 00000004100 15060250363 0016753 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders; use Illuminate\Support\Str; use ParseError; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class MergeConflictSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! ($throwable instanceof ParseError)) { return false; } if (! $this->hasMergeConflictExceptionMessage($throwable)) { return false; } $file = (string)file_get_contents($throwable->getFile()); if (! str_contains($file, '=======')) { return false; } if (! str_contains($file, '>>>>>>>')) { return false; } return true; } public function getSolutions(Throwable $throwable): array { $file = (string)file_get_contents($throwable->getFile()); preg_match('/\>\>\>\>\>\>\> (.*?)\n/', $file, $matches); $source = $matches[1]; $target = $this->getCurrentBranch(basename($throwable->getFile())); return [ BaseSolution::create("Merge conflict from branch '$source' into $target") ->setSolutionDescription('You have a Git merge conflict. To undo your merge do `git reset --hard HEAD`'), ]; } protected function getCurrentBranch(string $directory): string { $branch = "'".trim((string)shell_exec("cd {$directory}; git branch | grep \\* | cut -d ' ' -f2"))."'"; if ($branch === "''") { $branch = 'current branch'; } return $branch; } protected function hasMergeConflictExceptionMessage(Throwable $throwable): bool { // For PHP 7.x and below if (Str::startsWith($throwable->getMessage(), 'syntax error, unexpected \'<<\'')) { return true; } // For PHP 8+ if (Str::startsWith($throwable->getMessage(), 'syntax error, unexpected token "<<"')) { return true; } return false; } } SolutionProviders/BadMethodCallSolutionProvider.php 0000644 00000005243 15060250363 0016666 0 ustar 00 <?php namespace Spatie\ErrorSolutions\SolutionProviders; use BadMethodCallException; use Illuminate\Support\Collection; use ReflectionClass; use ReflectionMethod; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class BadMethodCallSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/([a-zA-Z\\\\]+)::([a-zA-Z]+)/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof BadMethodCallException) { return false; } if (is_null($this->getClassAndMethodFromExceptionMessage($throwable->getMessage()))) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create('Bad Method Call') ->setSolutionDescription($this->getSolutionDescription($throwable)), ]; } public function getSolutionDescription(Throwable $throwable): string { if (! $this->canSolve($throwable)) { return ''; } /** @phpstan-ignore-next-line */ extract($this->getClassAndMethodFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE); $possibleMethod = $this->findPossibleMethod($class ?? '', $method ?? ''); $class ??= 'UnknownClass'; return "Did you mean {$class}::{$possibleMethod?->name}() ?"; } /** * @param string $message * * @return null|array<string, mixed> */ protected function getClassAndMethodFromExceptionMessage(string $message): ?array { if (! preg_match(self::REGEX, $message, $matches)) { return null; } return [ 'class' => $matches[1], 'method' => $matches[2], ]; } /** * @param class-string $class * @param string $invalidMethodName * * @return \ReflectionMethod|null */ protected function findPossibleMethod(string $class, string $invalidMethodName): ?ReflectionMethod { return $this->getAvailableMethods($class) ->sortByDesc(function (ReflectionMethod $method) use ($invalidMethodName) { similar_text($invalidMethodName, $method->name, $percentage); return $percentage; })->first(); } /** * @param class-string $class * * @return \Illuminate\Support\Collection<int, ReflectionMethod> */ protected function getAvailableMethods(string $class): Collection { $class = new ReflectionClass($class); return Collection::make($class->getMethods()); } } Solutions/Laravel/UseDefaultValetDbCredentialsSolution.php 0000644 00000003165 15060250363 0020065 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Illuminate\Support\Str; use Spatie\ErrorSolutions\Contracts\RunnableSolution; class UseDefaultValetDbCredentialsSolution implements RunnableSolution { public function getSolutionActionDescription(): string { return 'Pressing the button will change `DB_USER` and `DB_PASSWORD` in your `.env` file.'; } public function getRunButtonText(): string { return 'Use default Valet credentials'; } public function getSolutionTitle(): string { return 'Could not connect to database'; } public function run(array $parameters = []): void { if (! file_exists(base_path('.env'))) { return; } $this->ensureLineExists('DB_USERNAME', 'root'); $this->ensureLineExists('DB_PASSWORD', ''); } protected function ensureLineExists(string $key, string $value): void { $envPath = base_path('.env'); $envLines = array_map(fn (string $envLine) => Str::startsWith($envLine, $key) ? "{$key}={$value}".PHP_EOL : $envLine, file($envPath) ?: []); file_put_contents($envPath, implode('', $envLines)); } public function getRunParameters(): array { return []; } public function getDocumentationLinks(): array { return [ 'Valet documentation' => 'https://laravel.com/docs/master/valet', ]; } public function getSolutionDescription(): string { return 'You seem to be using Valet, but the .env file does not contain the right default database credentials.'; } } Solutions/Laravel/SuggestLivewireMethodNameSolution.php 0000644 00000001405 15060250363 0017471 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestLivewireMethodNameSolution implements Solution { public function __construct( protected string $methodName, protected string $componentClass, protected string $suggested ) { } public function getSolutionTitle(): string { return "Possible typo `{$this->componentClass}::{$this->methodName}`"; } public function getDocumentationLinks(): array { return []; } public function getSolutionDescription(): string { return "Did you mean `{$this->componentClass}::{$this->suggested}`?"; } public function isRunnable(): bool { return false; } } Solutions/Laravel/SuggestUsingMysql8DatabaseSolution.php 0000644 00000001537 15060250363 0017577 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestUsingMysql8DatabaseSolution implements Solution { public function getSolutionTitle(): string { return 'Database is not a MySQL 8 database'; } public function getSolutionDescription(): string { return "Laravel 11 changed the default collation for MySQL and MariaDB. It seems you are trying to use the MySQL 8 collation `utf8mb4_0900_ai_ci` with a MariaDB or MySQL 5.7 database.\n\nEdit the `.env` file and use the correct database in the `DB_CONNECTION` key."; } /** @return array<string, string> */ public function getDocumentationLinks(): array { return [ 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', ]; } } Solutions/Laravel/LivewireDiscoverSolution.php 0000644 00000002370 15060250363 0015666 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Livewire\LivewireComponentsFinder; use Spatie\ErrorSolutions\Contracts\RunnableSolution; class LivewireDiscoverSolution implements RunnableSolution { protected string $customTitle; public function __construct(string $customTitle = '') { $this->customTitle = $customTitle; } public function getSolutionTitle(): string { return $this->customTitle; } public function getSolutionDescription(): string { return 'You might have forgotten to discover your Livewire components.'; } public function getDocumentationLinks(): array { return [ 'Livewire: Artisan Commands' => 'https://laravel-livewire.com/docs/2.x/artisan-commands', ]; } public function getRunParameters(): array { return []; } public function getSolutionActionDescription(): string { return 'You can discover your Livewire components using `php artisan livewire:discover`.'; } public function getRunButtonText(): string { return 'Run livewire:discover'; } public function run(array $parameters = []): void { app(LivewireComponentsFinder::class)->build(); } } Solutions/Laravel/SuggestUsingCorrectDbNameSolution.php 0000644 00000001475 15060250363 0017426 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestUsingCorrectDbNameSolution implements Solution { public function getSolutionTitle(): string { return 'Database name seems incorrect'; } public function getSolutionDescription(): string { $defaultDatabaseName = env('DB_DATABASE'); return "You're using the default database name `$defaultDatabaseName`. This database does not exist.\n\nEdit the `.env` file and use the correct database name in the `DB_DATABASE` key."; } /** @return array<string, string> */ public function getDocumentationLinks(): array { return [ 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', ]; } } Solutions/Laravel/MakeViewVariableOptionalSolution.php 0000644 00000007442 15060250363 0017272 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Str; use Spatie\ErrorSolutions\Contracts\RunnableSolution; class MakeViewVariableOptionalSolution implements RunnableSolution { protected ?string $variableName; protected ?string $viewFile; public function __construct(string $variableName = null, string $viewFile = null) { $this->variableName = $variableName; $this->viewFile = $viewFile; } public function getSolutionTitle(): string { return "$$this->variableName is undefined"; } public function getDocumentationLinks(): array { return []; } public function getSolutionActionDescription(): string { $output = [ 'Make the variable optional in the blade template.', "Replace `{{ $$this->variableName }}` with `{{ $$this->variableName ?? '' }}`", ]; return implode(PHP_EOL, $output); } public function getRunButtonText(): string { return 'Make variable optional'; } public function getSolutionDescription(): string { return $this->getSolutionActionDescription(); } public function getRunParameters(): array { return [ 'variableName' => $this->variableName, 'viewFile' => $this->viewFile, ]; } /** * @param array<string, mixed> $parameters * * @return bool */ public function isRunnable(array $parameters = []): bool { return $this->makeOptional($this->getRunParameters()) !== false; } /** * @param array<string, string> $parameters * * @return void */ public function run(array $parameters = []): void { $output = $this->makeOptional($parameters); if ($output !== false) { file_put_contents($parameters['viewFile'], $output); } } protected function isSafePath(string $path): bool { if (! Str::startsWith($path, ['/', './'])) { return false; } if (! Str::endsWith($path, '.blade.php')) { return false; } return true; } /** * @param array<string, string> $parameters * * @return bool|string */ public function makeOptional(array $parameters = []): bool|string { if (! $this->isSafePath($parameters['viewFile'])) { return false; } $originalContents = (string)file_get_contents($parameters['viewFile']); $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents); $originalTokens = token_get_all(Blade::compileString($originalContents)); $newTokens = token_get_all(Blade::compileString($newContents)); $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']); if ($expectedTokens !== $newTokens) { return false; } return $newContents; } /** * @param array<int, mixed> $originalTokens * @param string $variableName * * @return array<int, mixed> */ protected function generateExpectedTokens(array $originalTokens, string $variableName): array { $expectedTokens = []; foreach ($originalTokens as $token) { $expectedTokens[] = $token; if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) { $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]]; $expectedTokens[] = [T_COALESCE, '??', $token[2]]; $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]]; $expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]]; } } return $expectedTokens; } } Solutions/Laravel/GenerateAppKeySolution.php 0000644 00000002060 15060250363 0015241 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Illuminate\Support\Facades\Artisan; use Spatie\ErrorSolutions\Contracts\RunnableSolution; class GenerateAppKeySolution implements RunnableSolution { public function getSolutionTitle(): string { return 'Your app key is missing'; } public function getDocumentationLinks(): array { return [ 'Laravel installation' => 'https://laravel.com/docs/master/installation#configuration', ]; } public function getSolutionActionDescription(): string { return 'Generate your application encryption key using `php artisan key:generate`.'; } public function getRunButtonText(): string { return 'Generate app key'; } public function getSolutionDescription(): string { return $this->getSolutionActionDescription(); } public function getRunParameters(): array { return []; } public function run(array $parameters = []): void { Artisan::call('key:generate'); } } Solutions/Laravel/RunMigrationsSolution.php 0000644 00000002322 15060250363 0015177 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Illuminate\Support\Facades\Artisan; use Spatie\ErrorSolutions\Contracts\RunnableSolution; class RunMigrationsSolution implements RunnableSolution { protected string $customTitle; public function __construct(string $customTitle = '') { $this->customTitle = $customTitle; } public function getSolutionTitle(): string { return $this->customTitle; } public function getSolutionDescription(): string { return 'You might have forgotten to run your database migrations.'; } public function getDocumentationLinks(): array { return [ 'Database: Running Migrations docs' => 'https://laravel.com/docs/master/migrations#running-migrations', ]; } public function getRunParameters(): array { return []; } public function getSolutionActionDescription(): string { return 'You can try to run your migrations using `php artisan migrate`.'; } public function getRunButtonText(): string { return 'Run migrations'; } public function run(array $parameters = []): void { Artisan::call('migrate'); } } Solutions/Laravel/SuggestLivewirePropertyNameSolution.php 0000644 00000001326 15060250363 0020077 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestLivewirePropertyNameSolution implements Solution { public function __construct( protected string $variableName, protected string $componentClass, protected string $suggested, ) { } public function getSolutionTitle(): string { return "Possible typo {$this->variableName}"; } public function getDocumentationLinks(): array { return []; } public function getSolutionDescription(): string { return "Did you mean `$this->suggested`?"; } public function isRunnable(): bool { return false; } } Solutions/Laravel/SuggestUsingMariadbDatabaseSolution.php 0000644 00000001524 15060250363 0017735 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\Laravel; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestUsingMariadbDatabaseSolution implements Solution { public function getSolutionTitle(): string { return 'Database is not a MariaDB database'; } public function getSolutionDescription(): string { return "Laravel 11 changed the default collation for MySQL and MariaDB. It seems you are trying to use the MariaDB collation `utf8mb4_uca1400_ai_ci` with a MySQL database.\n\nEdit the `.env` file and use the correct database in the `DB_CONNECTION` key."; } /** @return array<string, string> */ public function getDocumentationLinks(): array { return [ 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', ]; } } Solutions/OpenAi/OpenAiPromptViewModel.php 0000644 00000001616 15060250363 0014624 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\OpenAi; class OpenAiPromptViewModel { public function __construct( protected string $file, protected string $exceptionMessage, protected string $exceptionClass, protected string $snippet, protected string $line, protected string|null $applicationType = null, ) { } public function file(): string { return $this->file; } public function line(): string { return $this->line; } public function snippet(): string { return $this->snippet; } public function exceptionMessage(): string { return $this->exceptionMessage; } public function exceptionClass(): string { return $this->exceptionClass; } public function applicationType(): string|null { return $this->applicationType; } } Solutions/OpenAi/DummyCache.php 0000644 00000001635 15060250363 0012453 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\OpenAi; use Psr\SimpleCache\CacheInterface; class DummyCache implements CacheInterface { public function get(string $key, mixed $default = null): mixed { return null; } public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool { return true; } public function delete(string $key): bool { return true; } public function clear(): bool { return true; } public function getMultiple(iterable $keys, mixed $default = null): iterable { return []; } public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool { return true; } public function deleteMultiple(iterable $keys): bool { return true; } public function has(string $key): bool { return false; } } Solutions/OpenAi/OpenAiSolutionProvider.php 0000644 00000003025 15060250363 0015052 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\OpenAi; use Psr\SimpleCache\CacheInterface; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class OpenAiSolutionProvider implements HasSolutionsForThrowable { public function __construct( protected string $openAiKey, protected ?CacheInterface $cache = null, protected int $cacheTtlInSeconds = 60 * 60, protected string|null $applicationType = null, protected string|null $applicationPath = null, ) { $this->cache ??= new DummyCache(); } public function canSolve(Throwable $throwable): bool { return true; } public function getSolutions(Throwable $throwable): array { return [ new OpenAiSolution( $throwable, $this->openAiKey, $this->cache, $this->cacheTtlInSeconds, $this->applicationType, $this->applicationPath, ), ]; } public function applicationType(string $applicationType): self { $this->applicationType = $applicationType; return $this; } public function applicationPath(string $applicationPath): self { $this->applicationPath = $applicationPath; return $this; } public function useCache(CacheInterface $cache, int $cacheTtlInSeconds = 60 * 60): self { $this->cache = $cache; $this->cacheTtlInSeconds = $cacheTtlInSeconds; return $this; } } Solutions/OpenAi/OpenAiSolution.php 0000644 00000006371 15060250363 0013346 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\OpenAi; use OpenAI; use Psr\SimpleCache\CacheInterface; use Spatie\Backtrace\Backtrace; use Spatie\Backtrace\Frame; use Spatie\ErrorSolutions\Contracts\Solution; use Spatie\ErrorSolutions\Support\AiPromptRenderer; use Throwable; class OpenAiSolution implements Solution { public bool $aiGenerated = true; protected string $prompt; protected OpenAiSolutionResponse $openAiSolutionResponse; public function __construct( protected Throwable $throwable, protected string $openAiKey, protected CacheInterface|null $cache = null, protected int|null $cacheTtlInSeconds = 60, protected string|null $applicationType = null, protected string|null $applicationPath = null, ) { $this->prompt = $this->generatePrompt(); $this->openAiSolutionResponse = $this->getAiSolution(); } public function getSolutionTitle(): string { return 'AI Generated Solution'; } public function getSolutionDescription(): string { return $this->openAiSolutionResponse->description(); } public function getDocumentationLinks(): array { return $this->openAiSolutionResponse->links(); } public function getAiSolution(): ?OpenAiSolutionResponse { $solution = $this->cache->get($this->getCacheKey()); if ($solution) { return new OpenAiSolutionResponse($solution); } $solutionText = OpenAI::client($this->openAiKey) ->chat() ->create([ 'model' => $this->getModel(), 'messages' => [['role' => 'user', 'content' => $this->prompt]], 'max_tokens' => 1000, 'temperature' => 0, ])->choices[0]->message->content; $this->cache->set($this->getCacheKey(), $solutionText, $this->cacheTtlInSeconds); return new OpenAiSolutionResponse($solutionText); } protected function getCacheKey(): string { $hash = sha1($this->prompt); return "ignition-solution-{$hash}"; } protected function generatePrompt(): string { $viewPath = __DIR__.'/../../../resources/views/aiPrompt.php'; $viewModel = new OpenAiPromptViewModel( file: $this->throwable->getFile(), exceptionMessage: $this->throwable->getMessage(), exceptionClass: get_class($this->throwable), snippet: $this->getApplicationFrame($this->throwable)->getSnippetAsString(15), line: $this->throwable->getLine(), applicationType: $this->applicationType, ); return (new AiPromptRenderer())->renderAsString( ['viewModel' => $viewModel], $viewPath, ); } protected function getModel(): string { return 'gpt-3.5-turbo'; } protected function getApplicationFrame(Throwable $throwable): ?Frame { $backtrace = Backtrace::createForThrowable($throwable); if ($this->applicationPath) { $backtrace->applicationPath($this->applicationPath); } $frames = $backtrace->frames(); return $frames[$backtrace->firstApplicationFrameIndex()] ?? null; } } Solutions/OpenAi/OpenAiSolutionResponse.php 0000644 00000002643 15060250363 0015063 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions\OpenAi; class OpenAiSolutionResponse { protected string $rawText; public function __construct(string $rawText) { $this->rawText = trim($rawText); } public function description(): string { return $this->between('FIX', 'ENDFIX', $this->rawText); } public function links(): array { $textLinks = $this->between('LINKS', 'ENDLINKS', $this->rawText); $textLinks = explode(PHP_EOL, $textLinks); $textLinks = array_map(function ($textLink) { $textLink = str_replace('\\', '\\\\', $textLink); $textLink = str_replace('\\\\\\', '\\\\', $textLink); return json_decode($textLink, true); }, $textLinks); array_filter($textLinks); $links = []; foreach ($textLinks as $textLink) { $links[$textLink['title']] = $textLink['url']; } return $links; } protected function between(string $start, string $end, string $text): string { $startPosition = strpos($text, $start); if ($startPosition === false) { return ""; } $startPosition += strlen($start); $endPosition = strpos($text, $end, $startPosition); if ($endPosition === false) { return ""; } return trim(substr($text, $startPosition, $endPosition - $startPosition)); } } Solutions/SuggestCorrectVariableNameSolution.php 0000644 00000001612 15060250363 0016223 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestCorrectVariableNameSolution implements Solution { protected ?string $variableName; protected ?string $viewFile; protected ?string $suggested; public function __construct(string $variableName = null, string $viewFile = null, string $suggested = null) { $this->variableName = $variableName; $this->viewFile = $viewFile; $this->suggested = $suggested; } public function getSolutionTitle(): string { return 'Possible typo $'.$this->variableName; } public function getDocumentationLinks(): array { return []; } public function getSolutionDescription(): string { return "Did you mean `$$this->suggested`?"; } public function isRunnable(): bool { return false; } } Solutions/SolutionTransformer.php 0000644 00000001604 15060250363 0013314 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions; use Illuminate\Contracts\Support\Arrayable; use Spatie\ErrorSolutions\Contracts\Solution; /** @implements Arrayable<string, array<string,string>|string|false> */ class SolutionTransformer implements Arrayable { protected Solution $solution; public function __construct(Solution $solution) { $this->solution = $solution; } /** @return array<string, array<string,string>|string|false> */ public function toArray(): array { return [ 'class' => get_class($this->solution), 'title' => $this->solution->getSolutionTitle(), 'links' => $this->solution->getDocumentationLinks(), 'description' => $this->solution->getSolutionDescription(), 'is_runnable' => false, 'ai_generated' => $this->solution->aiGenerated ?? false, ]; } } Solutions/SuggestImportSolution.php 0000644 00000001154 15060250363 0013626 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Solutions; use Spatie\ErrorSolutions\Contracts\Solution; class SuggestImportSolution implements Solution { protected string $class; public function __construct(string $class) { $this->class = $class; } public function getSolutionTitle(): string { return 'A class import is missing'; } public function getSolutionDescription(): string { return 'You have a missing class import. Try importing this class: `'.$this->class.'`.'; } public function getDocumentationLinks(): array { return []; } } DiscoverSolutionProviders.php 0000644 00000004312 15060250363 0012466 0 ustar 00 <?php namespace Spatie\ErrorSolutions; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; class DiscoverSolutionProviders { /** @var array<string, string> */ protected array $config = [ 'ai' => 'SolutionProviders/OpenAi', 'php' => 'SolutionProviders', 'laravel' => 'SolutionProviders/Laravel', ]; /** * @param array<string> $types * * @return array<HasSolutionsForThrowable> */ public static function for(array $types): array { if (in_array('php', $types)) { $types[] = 'ai'; } return (new self($types))->get(); } /** * @param array<string> $types */ public function __construct(protected array $types) { } /** @return array<HasSolutionsForThrowable> */ public function get(): array { $providers = []; foreach ($this->types as $type) { $providers = array_merge($providers, $this->getProviderClassesForType($type)); } return $providers; } /** @return array<HasSolutionsForThrowable> */ protected function getProviderClassesForType(string $type): array { $relativePath = $this->config[$type] ?? null; if (! $relativePath) { return []; } $namespace = $this->getNamespaceForPath($relativePath); $globPattern = __DIR__ . '/' . $relativePath . '/*.php'; $files = glob($globPattern); if (! $files) { return []; } $solutionProviders = array_map(function (string $solutionProviderFilePath) use ($namespace) { $fileName = pathinfo($solutionProviderFilePath, PATHINFO_FILENAME); $fqcn = $namespace . '\\' . $fileName; $validClass = in_array(HasSolutionsForThrowable::class, class_implements($fqcn) ?: []); return $validClass ? $fqcn : null; }, $files); return array_values(array_filter($solutionProviders)); } protected function getNamespaceForPath(string $relativePath): string { $namespacePath = str_replace('/', '\\', $relativePath); $namespace = 'Spatie\\ErrorSolutions\\' . $namespacePath; return $namespace; } } Contracts/BaseSolution.php 0000644 00000002564 15060250363 0011633 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Contracts; class BaseSolution implements Solution { protected string $title; protected string $description = ''; /** @var array<string, string> */ protected array $links = []; public static function create(string $title = ''): static { // It's important to keep the return type as static because // the old Facade Ignition contracts extend from this method. /** @phpstan-ignore-next-line */ return new static($title); } public function __construct(string $title = '') { $this->title = $title; } public function getSolutionTitle(): string { return $this->title; } public function setSolutionTitle(string $title): self { $this->title = $title; return $this; } public function getSolutionDescription(): string { return $this->description; } public function setSolutionDescription(string $description): self { $this->description = $description; return $this; } /** @return array<string, string> */ public function getDocumentationLinks(): array { return $this->links; } /** @param array<string, string> $links */ public function setDocumentationLinks(array $links): self { $this->links = $links; return $this; } } Contracts/Solution.php 0000644 00000000417 15060250363 0011033 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Contracts; interface Solution { public function getSolutionTitle(): string; public function getSolutionDescription(): string; /** @return array<string, string> */ public function getDocumentationLinks(): array; } Contracts/RunnableSolution.php 0000644 00000000623 15060250363 0012521 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Contracts; interface RunnableSolution extends Solution { public function getSolutionActionDescription(): string; public function getRunButtonText(): string; /** @param array<string, mixed> $parameters */ public function run(array $parameters = []): void; /** @return array<string, mixed> */ public function getRunParameters(): array; } Contracts/HasSolutionsForThrowable.php 0000644 00000000475 15060250363 0014175 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Contracts; use Throwable; /** * Interface used for SolutionProviders. */ interface HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool; /** @return array<int, Solution> */ public function getSolutions(Throwable $throwable): array; } Contracts/ProvidesSolution.php 0000644 00000000316 15060250363 0012545 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Contracts; /** * Interface to be used on exceptions that provide their own solution. */ interface ProvidesSolution { public function getSolution(): Solution; } Contracts/SolutionProviderRepository.php 0000644 00000001610 15060250363 0014642 0 ustar 00 <?php namespace Spatie\ErrorSolutions\Contracts; use Throwable; interface SolutionProviderRepository { /** * @param class-string<HasSolutionsForThrowable>|HasSolutionsForThrowable $solutionProvider * * @return $this */ public function registerSolutionProvider(string $solutionProvider): self; /** * @param array<class-string<HasSolutionsForThrowable>|HasSolutionsForThrowable> $solutionProviders * * @return $this */ public function registerSolutionProviders(array $solutionProviders): self; /** * @param Throwable $throwable * * @return array<int, Solution> */ public function getSolutionsForThrowable(Throwable $throwable): array; /** * @param class-string<Solution> $solutionClass * * @return null|Solution */ public function getSolutionForClass(string $solutionClass): ?Solution; } SolutionProviderRepository.php 0000644 00000007235 15060250363 0012713 0 ustar 00 <?php namespace Spatie\ErrorSolutions; use Illuminate\Support\Collection; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Contracts\ProvidesSolution; use Spatie\ErrorSolutions\Contracts\Solution; use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract; use Throwable; class SolutionProviderRepository implements SolutionProviderRepositoryContract { /** @var Collection<int, class-string<HasSolutionsForThrowable>|HasSolutionsForThrowable> */ protected Collection $solutionProviders; /** @param array<int, class-string<HasSolutionsForThrowable>|HasSolutionsForThrowable> $solutionProviders */ public function __construct(array $solutionProviders = []) { $this->solutionProviders = Collection::make($solutionProviders); } public function registerSolutionProvider(string|HasSolutionsForThrowable $solutionProvider): SolutionProviderRepositoryContract { $this->solutionProviders->push($solutionProvider); return $this; } public function registerSolutionProviders(array $solutionProviderClasses): SolutionProviderRepositoryContract { $this->solutionProviders = $this->solutionProviders->merge($solutionProviderClasses); return $this; } public function getSolutionsForThrowable(Throwable $throwable): array { $solutions = []; if ($throwable instanceof Solution) { $solutions[] = $throwable; } if ($throwable instanceof ProvidesSolution) { $solutions[] = $throwable->getSolution(); } $providedSolutions = $this ->initialiseSolutionProviderRepositories() ->filter(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) { try { return $solutionProvider->canSolve($throwable); } catch (Throwable $exception) { return false; } }) ->map(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) { try { return $solutionProvider->getSolutions($throwable); } catch (Throwable $exception) { return []; } }) ->flatten() ->toArray(); return array_merge($solutions, $providedSolutions); } public function getSolutionForClass(string $solutionClass): ?Solution { if (! class_exists($solutionClass)) { return null; } if (! in_array(Solution::class, class_implements($solutionClass) ?: [])) { return null; } if (! function_exists('app')) { return null; } return app($solutionClass); } /** @return Collection<int, HasSolutionsForThrowable> */ protected function initialiseSolutionProviderRepositories(): Collection { return $this->solutionProviders ->filter(function (HasSolutionsForThrowable|string $provider) { if (! in_array(HasSolutionsForThrowable::class, class_implements($provider) ?: [])) { return false; } if (function_exists('config') && in_array($provider, config('ErrorSolutions.ignored_solution_providers', []))) { return false; } return true; }) ->map(function (string|HasSolutionsForThrowable $provider): HasSolutionsForThrowable { if (is_string($provider)) { return new $provider; } return $provider; }); } } Renderers/ErrorPageRenderer.php 0000644 00000002754 15060250375 0012576 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Renderers; use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository; use Spatie\FlareClient\Flare; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\Ignition\Ignition; use Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector; use Spatie\LaravelIgnition\Solutions\SolutionTransformers\LaravelSolutionTransformer; use Spatie\LaravelIgnition\Support\LaravelDocumentationLinkFinder; use Throwable; class ErrorPageRenderer { public function render(Throwable $throwable): void { $viteJsAutoRefresh = ''; if (class_exists('Illuminate\Foundation\Vite')) { $vite = app(\Illuminate\Foundation\Vite::class); if (is_file($vite->hotFile())) { $viteJsAutoRefresh = $vite->__invoke([]); } } app(Ignition::class) ->resolveDocumentationLink( fn (Throwable $throwable) => (new LaravelDocumentationLinkFinder())->findLinkForThrowable($throwable) ) ->setFlare(app(Flare::class)) ->setConfig(app(IgnitionConfig::class)) ->setSolutionProviderRepository(app(SolutionProviderRepository::class)) ->setContextProviderDetector(new LaravelContextProviderDetector()) ->setSolutionTransformerClass(LaravelSolutionTransformer::class) ->applicationPath(base_path()) ->addCustomHtmlToHead($viteJsAutoRefresh) ->renderException($throwable); } } Renderers/IgnitionExceptionRenderer.php 0000644 00000001005 15060250375 0014333 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Renderers; use Illuminate\Contracts\Foundation\ExceptionRenderer; class IgnitionExceptionRenderer implements ExceptionRenderer { protected ErrorPageRenderer $errorPageHandler; public function __construct(ErrorPageRenderer $errorPageHandler) { $this->errorPageHandler = $errorPageHandler; } public function render($throwable) { ob_start(); $this->errorPageHandler->render($throwable); return ob_get_clean(); } } ArgumentReducers/CollectionArgumentReducer.php 0000644 00000001170 15060250375 0015646 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ArgumentReducers; use Illuminate\Support\Collection; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer; class CollectionArgumentReducer extends ArrayArgumentReducer { public function execute(mixed $argument): ReducedArgumentContract { if (! $argument instanceof Collection) { return UnReducedArgument::create(); } return $this->reduceArgument($argument->toArray(), get_class($argument)); } } ArgumentReducers/ModelArgumentReducer.php 0000644 00000001351 15060250375 0014614 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ArgumentReducers; use Illuminate\Database\Eloquent\Model; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; class ModelArgumentReducer implements ArgumentReducer { public function execute(mixed $argument): ReducedArgumentContract { if (! $argument instanceof Model) { return UnReducedArgument::create(); } return new ReducedArgument( "{$argument->getKeyName()}:{$argument->getKey()}", get_class($argument) ); } } Views/ViewExceptionMapper.php 0000644 00000014565 15060250375 0012326 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Views; use Illuminate\Contracts\View\Engine; use Illuminate\Foundation\Application; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\View\Engines\PhpEngine; use Illuminate\View\ViewException; use ReflectionClass; use ReflectionProperty; use Spatie\ErrorSolutions\Contracts\ProvidesSolution; use Spatie\LaravelIgnition\Exceptions\ViewException as IgnitionViewException; use Spatie\LaravelIgnition\Exceptions\ViewExceptionWithSolution; use Throwable; class ViewExceptionMapper { protected Engine $compilerEngine; protected BladeSourceMapCompiler $bladeSourceMapCompiler; protected array $knownPaths; public function __construct(BladeSourceMapCompiler $bladeSourceMapCompiler) { $resolver = app('view.engine.resolver'); $this->compilerEngine = $resolver->resolve('blade'); $this->bladeSourceMapCompiler = $bladeSourceMapCompiler; } public function map(ViewException $viewException): IgnitionViewException { $baseException = $this->getRealException($viewException); if ($baseException instanceof IgnitionViewException) { return $baseException; } preg_match('/\(View: (?P<path>.*?)\)/', $viewException->getMessage(), $matches); $compiledViewPath = $matches['path']; $exception = $this->createException($baseException); if ($baseException instanceof ProvidesSolution) { /** @var ViewExceptionWithSolution $exception */ $exception->setSolution($baseException->getSolution()); } $this->modifyViewsInTrace($exception); $exception->setView($compiledViewPath); $exception->setViewData($this->getViewData($exception)); return $exception; } protected function createException(Throwable $baseException): IgnitionViewException { $viewExceptionClass = $baseException instanceof ProvidesSolution ? ViewExceptionWithSolution::class : IgnitionViewException::class; $viewFile = $this->findCompiledView($baseException->getFile()); $file = $viewFile ?? $baseException->getFile(); $line = $viewFile ? $this->getBladeLineNumber($file, $baseException->getLine()) : $baseException->getLine(); return new $viewExceptionClass( $baseException->getMessage(), 0, 1, $file, $line, $baseException ); } protected function modifyViewsInTrace(IgnitionViewException $exception): void { $viewIndex = null; $trace = Collection::make($exception->getPrevious()->getTrace()) ->map(function ($trace, $index) use (&$viewIndex) { if ($originalPath = $this->findCompiledView(Arr::get($trace, 'file', ''))) { $trace['file'] = $originalPath; $trace['line'] = $this->getBladeLineNumber($trace['file'], $trace['line']); if ($viewIndex === null) { $viewIndex = $index; } } return $trace; }) ->when( $viewIndex !== null && str_ends_with($exception->getFile(), '.blade.php'), fn (Collection $trace) => $trace->slice($viewIndex + 1) // Remove all traces before the view ) ->toArray(); $traceProperty = new ReflectionProperty('Exception', 'trace'); $traceProperty->setAccessible(true); $traceProperty->setValue($exception, $trace); } /** * Look at the previous exceptions to find the original exception. * This is usually the first Exception that is not a ViewException. */ protected function getRealException(Throwable $exception): Throwable { $rootException = $exception->getPrevious() ?? $exception; while ($rootException instanceof ViewException && $rootException->getPrevious()) { $rootException = $rootException->getPrevious(); } return $rootException; } protected function findCompiledView(string $compiledPath): ?string { $this->knownPaths ??= $this->getKnownPaths(); return $this->knownPaths[$compiledPath] ?? null; } protected function getKnownPaths(): array { $compilerEngineReflection = new ReflectionClass($this->compilerEngine); if (! $compilerEngineReflection->hasProperty('lastCompiled') && $compilerEngineReflection->hasProperty('engine')) { $compilerEngine = $compilerEngineReflection->getProperty('engine'); $compilerEngine->setAccessible(true); $compilerEngine = $compilerEngine->getValue($this->compilerEngine); $lastCompiled = new ReflectionProperty($compilerEngine, 'lastCompiled'); $lastCompiled->setAccessible(true); $lastCompiled = $lastCompiled->getValue($compilerEngine); } else { $lastCompiled = $compilerEngineReflection->getProperty('lastCompiled'); $lastCompiled->setAccessible(true); $lastCompiled = $lastCompiled->getValue($this->compilerEngine); } $knownPaths = []; foreach ($lastCompiled as $lastCompiledPath) { $compiledPath = $this->compilerEngine->getCompiler()->getCompiledPath($lastCompiledPath); $knownPaths[realpath($compiledPath ?? $lastCompiledPath)] = realpath($lastCompiledPath); } return $knownPaths; } protected function getBladeLineNumber(string $view, int $compiledLineNumber): int { return $this->bladeSourceMapCompiler->detectLineNumber($view, $compiledLineNumber); } protected function getViewData(Throwable $exception): array { foreach ($exception->getTrace() as $frame) { if (Arr::get($frame, 'class') === PhpEngine::class) { $data = Arr::get($frame, 'args.1', []); return $this->filterViewData($data); } } return []; } protected function filterViewData(array $data): array { // By default, Laravel views get two data keys: // __env and app. We try to filter them out. return array_filter($data, function ($value, $key) { if ($key === 'app') { return ! $value instanceof Application; } return $key !== '__env'; }, ARRAY_FILTER_USE_BOTH); } } Views/BladeSourceMapCompiler.php 0000644 00000010660 15060250375 0012701 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Views; use Illuminate\View\Compilers\BladeCompiler; use Throwable; class BladeSourceMapCompiler { protected BladeCompiler $bladeCompiler; public function __construct() { $this->bladeCompiler = app('blade.compiler'); } public function detectLineNumber(string $filename, int $compiledLineNumber): int { $map = $this->compileSourcemap((string)file_get_contents($filename)); return $this->findClosestLineNumberMapping($map, $compiledLineNumber); } protected function compileSourcemap(string $value): string { try { $value = $this->addEchoLineNumbers($value); $value = $this->addStatementLineNumbers($value); $value = $this->addBladeComponentLineNumbers($value); $value = $this->bladeCompiler->compileString($value); return $this->trimEmptyLines($value); } catch (Throwable $e) { report($e); return $value; } } protected function addEchoLineNumbers(string $value): string { $echoPairs = [['{{', '}}'], ['{{{', '}}}'], ['{!!', '!!}']]; foreach ($echoPairs as $pair) { // Matches {{ $value }}, {!! $value !!} and {{{ $value }}} depending on $pair $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $pair[0], $pair[1]); if (preg_match_all($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) { foreach (array_reverse($matches[0]) as $match) { $position = mb_strlen(substr($value, 0, $match[1])); $value = $this->insertLineNumberAtPosition($position, $value); } } } return $value; } protected function addStatementLineNumbers(string $value): string { // Matches @bladeStatements() like @if, @component(...), @etc; $shouldInsertLineNumbers = preg_match_all( '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', $value, $matches, PREG_OFFSET_CAPTURE ); if ($shouldInsertLineNumbers) { foreach (array_reverse($matches[0]) as $match) { $position = mb_strlen(substr($value, 0, $match[1])); $value = $this->insertLineNumberAtPosition($position, $value); } } return $value; } protected function addBladeComponentLineNumbers(string $value): string { // Matches the start of `<x-blade-component` $shouldInsertLineNumbers = preg_match_all( '/<\s*x[-:]([\w\-:.]*)/mx', $value, $matches, PREG_OFFSET_CAPTURE ); if ($shouldInsertLineNumbers) { foreach (array_reverse($matches[0]) as $match) { $position = mb_strlen(substr($value, 0, $match[1])); $value = $this->insertLineNumberAtPosition($position, $value); } } return $value; } protected function insertLineNumberAtPosition(int $position, string $value): string { $before = mb_substr($value, 0, $position); $lineNumber = count(explode("\n", $before)); return mb_substr($value, 0, $position)."|---LINE:{$lineNumber}---|".mb_substr($value, $position); } protected function trimEmptyLines(string $value): string { $value = preg_replace('/^\|---LINE:([0-9]+)---\|$/m', '', $value); return ltrim((string)$value, PHP_EOL); } protected function findClosestLineNumberMapping(string $map, int $compiledLineNumber): int { $map = explode("\n", $map); // Max 20 lines between compiled and source line number. // Blade components can span multiple lines and the compiled line number is often // a couple lines below the source-mapped `<x-component>` code. $maxDistance = 20; $pattern = '/\|---LINE:(?P<line>[0-9]+)---\|/m'; $lineNumberToCheck = $compiledLineNumber - 1; while (true) { if ($lineNumberToCheck < $compiledLineNumber - $maxDistance) { // Something wrong. Return the $compiledLineNumber (unless it's out of range) return min($compiledLineNumber, count($map)); } if (preg_match($pattern, $map[$lineNumberToCheck] ?? '', $matches)) { return (int)$matches['line']; } $lineNumberToCheck--; } } } Support/FlareLogHandler.php 0000644 00000005423 15060250375 0011731 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use InvalidArgumentException; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Level; use Monolog\Logger; use Monolog\LogRecord; use Spatie\FlareClient\Flare; use Spatie\FlareClient\Report; use Throwable; class FlareLogHandler extends AbstractProcessingHandler { protected Flare $flare; protected SentReports $sentReports; protected int $minimumReportLogLevel; public function __construct(Flare $flare, SentReports $sentReports, $level = Level::Debug, $bubble = true) { $this->flare = $flare; $this->minimumReportLogLevel = Level::Error->value; $this->sentReports = $sentReports; parent::__construct($level, $bubble); } public function setMinimumReportLogLevel(int $level): void { if (! in_array($level, Level::VALUES)) { throw new InvalidArgumentException('The given minimum log level is not supported.'); } $this->minimumReportLogLevel = $level; } protected function write(LogRecord $record): void { if (! $this->shouldReport($record->toArray())) { return; } if ($this->hasException($record->toArray())) { $report = $this->flare->report($record['context']['exception']); if ($report) { $this->sentReports->add($report); } return; } if (config('flare.send_logs_as_events')) { if ($this->hasValidLogLevel($record->toArray())) { $this->flare->reportMessage( $record['message'], 'Log ' . Logger::toMonologLevel($record['level'])->getName(), function (Report $flareReport) use ($record) { foreach ($record['context'] as $key => $value) { $flareReport->context($key, $value); } } ); } } } /** * @param array<string, mixed> $report * * @return bool */ protected function shouldReport(array $report): bool { if (! config('flare.key')) { return false; } return $this->hasException($report) || $this->hasValidLogLevel($report); } /** * @param array<string, mixed> $report * * @return bool */ protected function hasException(array $report): bool { $context = $report['context']; return isset($context['exception']) && $context['exception'] instanceof Throwable; } /** * @param array<string, mixed> $report * * @return bool */ protected function hasValidLogLevel(array $report): bool { return $report['level'] >= $this->minimumReportLogLevel; } } Support/LaravelVersion.php 0000644 00000000264 15060250375 0011672 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; class LaravelVersion { public static function major(): string { return explode('.', app()->version())[0]; } } Support/LaravelDocumentationLinkFinder.php 0000644 00000004114 15060250375 0015022 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\LaravelIgnition\Exceptions\ViewException; use Throwable; class LaravelDocumentationLinkFinder { public function findLinkForThrowable(Throwable $throwable): ?string { if ($throwable instanceof ViewException) { $throwable = $throwable->getPrevious(); } $majorVersion = LaravelVersion::major(); if (str_contains($throwable->getMessage(), Collection::class)) { return "https://laravel.com/docs/{$majorVersion}.x/collections#available-methods"; } $type = $this->getType($throwable); if (! $type) { return null; } return match ($type) { 'Auth' => "https://laravel.com/docs/{$majorVersion}.x/authentication", 'Broadcasting' => "https://laravel.com/docs/{$majorVersion}.x/broadcasting", 'Container' => "https://laravel.com/docs/{$majorVersion}.x/container", 'Database' => "https://laravel.com/docs/{$majorVersion}.x/eloquent", 'Pagination' => "https://laravel.com/docs/{$majorVersion}.x/pagination", 'Queue' => "https://laravel.com/docs/{$majorVersion}.x/queues", 'Routing' => "https://laravel.com/docs/{$majorVersion}.x/routing", 'Session' => "https://laravel.com/docs/{$majorVersion}.x/session", 'Validation' => "https://laravel.com/docs/{$majorVersion}.x/validation", 'View' => "https://laravel.com/docs/{$majorVersion}.x/views", default => null, }; } protected function getType(?Throwable $throwable): ?string { if (! $throwable) { return null; } if (str_contains($throwable::class, 'Illuminate')) { return Str::between($throwable::class, 'Illuminate\\', '\\'); } if (str_contains($throwable->getMessage(), 'Illuminate')) { return explode('\\', Str::between($throwable->getMessage(), 'Illuminate\\', '\\'))[0]; } return null; } } Support/SentReports.php 0000644 00000002172 15060250375 0011226 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use Illuminate\Support\Arr; use Spatie\FlareClient\Report; class SentReports { /** @var array<int, Report> */ protected array $reports = []; public function add(Report $report): self { $this->reports[] = $report; return $this; } /** @return array<int, Report> */ public function all(): array { return $this->reports; } /** @return array<int, string> */ public function uuids(): array { return array_map(fn (Report $report) => $report->trackingUuid(), $this->reports); } /** @return array<int, string> */ public function urls(): array { return array_map(function (string $trackingUuid) { return "https://flareapp.io/tracked-occurrence/{$trackingUuid}"; }, $this->uuids()); } public function latestUuid(): ?string { return Arr::last($this->reports)?->trackingUuid(); } public function latestUrl(): ?string { return Arr::last($this->urls()); } public function clear(): void { $this->reports = []; } } Support/RunnableSolutionsGuard.php 0000644 00000002053 15060250375 0013405 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; class RunnableSolutionsGuard { /** * Check if runnable solutions are allowed based on the current * environment and config. * * @return bool */ public static function check(): bool { if (! config('app.debug')) { // Never run solutions in when debug mode is not enabled. return false; } if (config('ignition.enable_runnable_solutions') !== null) { // Allow enabling or disabling runnable solutions regardless of environment // if the IGNITION_ENABLE_RUNNABLE_SOLUTIONS env var is explicitly set. return config('ignition.enable_runnable_solutions'); } if (! app()->environment('local') && ! app()->environment('development')) { // Never run solutions on non-local environments. This avoids exposing // applications that are somehow APP_ENV=production with APP_DEBUG=true. return false; } return config('app.debug'); } } IgnitionServiceProvider.php 0000644 00000026746 15060250375 0012113 0 ustar 00 <?php namespace Spatie\LaravelIgnition; use Exception; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; use Illuminate\View\ViewException; use Laravel\Octane\Events\RequestReceived; use Laravel\Octane\Events\RequestTerminated; use Laravel\Octane\Events\TaskReceived; use Laravel\Octane\Events\TickReceived; use Monolog\Level; use Monolog\Logger; use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract; use Spatie\ErrorSolutions\SolutionProviderRepository; use Spatie\FlareClient\Flare; use Spatie\FlareClient\FlareMiddleware\AddSolutions; use Spatie\Ignition\Config\FileConfigManager; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\Ignition\Contracts\ConfigManager; use Spatie\Ignition\Ignition; use Spatie\LaravelIgnition\Commands\SolutionMakeCommand; use Spatie\LaravelIgnition\Commands\SolutionProviderMakeCommand; use Spatie\LaravelIgnition\Commands\TestCommand; use Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector; use Spatie\LaravelIgnition\Exceptions\InvalidConfig; use Spatie\LaravelIgnition\FlareMiddleware\AddJobs; use Spatie\LaravelIgnition\FlareMiddleware\AddLogs; use Spatie\LaravelIgnition\FlareMiddleware\AddQueries; use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder; use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder; use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder; use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder; use Spatie\LaravelIgnition\Renderers\IgnitionExceptionRenderer; use Spatie\LaravelIgnition\Support\FlareLogHandler; use Spatie\LaravelIgnition\Support\SentReports; use Spatie\LaravelIgnition\Views\ViewExceptionMapper; class IgnitionServiceProvider extends ServiceProvider { public function register(): void { $this->registerConfig(); $this->registerFlare(); $this->registerIgnition(); $this->registerRenderer(); $this->registerRecorders(); $this->registerLogHandler(); } public function boot() { if ($this->app->runningInConsole()) { $this->registerCommands(); $this->publishConfigs(); } $this->registerRoutes(); $this->configureTinker(); $this->configureOctane(); $this->registerViewExceptionMapper(); $this->startRecorders(); $this->configureQueue(); } protected function registerConfig(): void { $this->mergeConfigFrom(__DIR__ . '/../config/flare.php', 'flare'); $this->mergeConfigFrom(__DIR__ . '/../config/ignition.php', 'ignition'); } protected function registerCommands(): void { if ($this->app['config']->get('flare.key')) { $this->commands([ TestCommand::class, ]); } if ($this->app['config']->get('ignition.register_commands')) { $this->commands([ SolutionMakeCommand::class, SolutionProviderMakeCommand::class, ]); } } protected function publishConfigs(): void { $this->publishes([ __DIR__ . '/../config/ignition.php' => config_path('ignition.php'), ], 'ignition-config'); $this->publishes([ __DIR__ . '/../config/flare.php' => config_path('flare.php'), ], 'flare-config'); } protected function registerRenderer(): void { $this->app->bind( 'Illuminate\Contracts\Foundation\ExceptionRenderer', fn (Application $app) => $app->make(IgnitionExceptionRenderer::class) ); } protected function registerFlare(): void { $this->app->singleton(Flare::class, function () { return Flare::make() ->setApiToken(config('flare.key') ?? '') ->setBaseUrl(config('flare.base_url', 'https://flareapp.io/api')) ->applicationPath(base_path()) ->setStage(app()->environment()) ->setContextProviderDetector(new LaravelContextProviderDetector()) ->registerMiddleware($this->getFlareMiddleware()) ->registerMiddleware(new AddSolutions(new SolutionProviderRepository($this->getSolutionProviders()))) ->argumentReducers(config('ignition.argument_reducers', [])) ->withStackFrameArguments(config('ignition.with_stack_frame_arguments', true)); }); $this->app->singleton(SentReports::class); } protected function registerIgnition(): void { $this->app->singleton( ConfigManager::class, fn () => new FileConfigManager(config('ignition.settings_file_path', '')) ); $ignitionConfig = (new IgnitionConfig()) ->merge(config('ignition', [])) ->loadConfigFile(); $solutionProviders = $this->getSolutionProviders(); $solutionProviderRepository = new SolutionProviderRepository($solutionProviders); $this->app->singleton(IgnitionConfig::class, fn () => $ignitionConfig); $this->app->singleton(SolutionProviderRepositoryContract::class, fn () => $solutionProviderRepository); $this->app->singleton( Ignition::class, fn () => (new Ignition($this->app->make(Flare::class)))->applicationPath(base_path()) ); } protected function registerRecorders(): void { $this->app->singleton(DumpRecorder::class); $this->app->singleton(LogRecorder::class, function (Application $app): LogRecorder { return new LogRecorder( $app, config()->get('flare.flare_middleware.' . AddLogs::class . '.maximum_number_of_collected_logs') ); }); $this->app->singleton( QueryRecorder::class, function (Application $app): QueryRecorder { return new QueryRecorder( $app, config('flare.flare_middleware.' . AddQueries::class . '.report_query_bindings', true), config('flare.flare_middleware.' . AddQueries::class . '.maximum_number_of_collected_queries', 200) ); } ); $this->app->singleton(JobRecorder::class, function (Application $app): JobRecorder { return new JobRecorder( $app, config('flare.flare_middleware.' . AddJobs::class . '.max_chained_job_reporting_depth', 5) ); }); } public function configureTinker(): void { if ($this->app->runningInConsole()) { if (isset($_SERVER['argv']) && ['artisan', 'tinker'] === $_SERVER['argv']) { app(Flare::class)->sendReportsImmediately(); } } } protected function configureOctane(): void { if (isset($_SERVER['LARAVEL_OCTANE'])) { $this->setupOctane(); } } protected function registerViewExceptionMapper(): void { $handler = $this->app->make(ExceptionHandler::class); if (! method_exists($handler, 'map')) { return; } $handler->map(function (ViewException $viewException) { return $this->app->make(ViewExceptionMapper::class)->map($viewException); }); } protected function registerRoutes(): void { $this->loadRoutesFrom(realpath(__DIR__ . '/ignition-routes.php')); } protected function registerLogHandler(): void { $this->app->singleton('flare.logger', function ($app) { $handler = new FlareLogHandler( $app->make(Flare::class), $app->make(SentReports::class), ); $logLevelString = config('logging.channels.flare.level', 'error'); $logLevel = $this->getLogLevel($logLevelString); $handler->setMinimumReportLogLevel($logLevel); return tap( new Logger('Flare'), fn (Logger $logger) => $logger->pushHandler($handler) ); }); Log::extend('flare', fn ($app) => $app['flare.logger']); } protected function startRecorders(): void { foreach ($this->app->config['ignition.recorders'] ?? [] as $recorder) { $this->app->make($recorder)->start(); } } protected function configureQueue(): void { if (! $this->app->bound('queue')) { return; } $queue = $this->app->get('queue'); // Reset before executing a queue job to make sure the job's log/query/dump recorders are empty. // When using a sync queue this also reports the queued reports from previous exceptions. $queue->before(function () { $this->resetFlareAndLaravelIgnition(); app(Flare::class)->sendReportsImmediately(); }); // Send queued reports (and reset) after executing a queue job. $queue->after(function () { $this->resetFlareAndLaravelIgnition(); }); // Note: the $queue->looping() event can't be used because it's not triggered on Vapor } protected function getLogLevel(string $logLevelString): int { try { $logLevel = Level::fromName($logLevelString); } catch (Exception $exception) { $logLevel = null; } if (! $logLevel) { throw InvalidConfig::invalidLogLevel($logLevelString); } return $logLevel->value; } protected function getFlareMiddleware(): array { return collect(config('flare.flare_middleware')) ->map(function ($value, $key) { if (is_string($key)) { $middlewareClass = $key; $parameters = $value ?? []; } else { $middlewareClass = $value; $parameters = []; } return new $middlewareClass(...array_values($parameters)); }) ->values() ->toArray(); } protected function getSolutionProviders(): array { return collect(config('ignition.solution_providers')) ->reject( fn (string $class) => in_array($class, config('ignition.ignored_solution_providers')) ) ->toArray(); } protected function setupOctane(): void { $this->app['events']->listen(RequestReceived::class, function () { $this->resetFlareAndLaravelIgnition(); }); $this->app['events']->listen(TaskReceived::class, function () { $this->resetFlareAndLaravelIgnition(); }); $this->app['events']->listen(TickReceived::class, function () { $this->resetFlareAndLaravelIgnition(); }); $this->app['events']->listen(RequestTerminated::class, function () { $this->resetFlareAndLaravelIgnition(); }); } protected function resetFlareAndLaravelIgnition(): void { $this->app->get(SentReports::class)->clear(); $this->app->get(Ignition::class)->reset(); if (config('flare.flare_middleware.' . AddLogs::class)) { $this->app->make(LogRecorder::class)->reset(); } if (config('flare.flare_middleware.' . AddQueries::class)) { $this->app->make(QueryRecorder::class)->reset(); } if (config('flare.flare_middleware.' . AddJobs::class)) { $this->app->make(JobRecorder::class)->reset(); } $this->app->make(DumpRecorder::class)->reset(); } } ignition-routes.php 0000644 00000001413 15060250375 0010416 0 ustar 00 <?php use Illuminate\Support\Facades\Route; use Spatie\LaravelIgnition\Http\Controllers\ExecuteSolutionController; use Spatie\LaravelIgnition\Http\Controllers\HealthCheckController; use Spatie\LaravelIgnition\Http\Controllers\UpdateConfigController; use Spatie\LaravelIgnition\Http\Middleware\RunnableSolutionsEnabled; Route::group([ 'as' => 'ignition.', 'prefix' => config('ignition.housekeeping_endpoint_prefix'), 'middleware' => [RunnableSolutionsEnabled::class], ], function () { Route::get('health-check', HealthCheckController::class)->name('healthCheck'); Route::post('execute-solution', ExecuteSolutionController::class) ->name('executeSolution'); Route::post('update-config', UpdateConfigController::class)->name('updateConfig'); }); Commands/SolutionMakeCommand.php 0000644 00000001554 15060250375 0012737 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Commands; use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Input\InputOption; class SolutionMakeCommand extends GeneratorCommand { protected $name = 'ignition:make-solution'; protected $description = 'Create a new custom Ignition solution class'; protected $type = 'Solution'; protected function getStub(): string { return $this->option('runnable') ? __DIR__.'/stubs/runnable-solution.stub' : __DIR__.'/stubs/solution.stub'; } protected function getDefaultNamespace($rootNamespace) { return "{$rootNamespace}\\Solutions"; } /** @return array<int, mixed> */ protected function getOptions(): array { return [ ['runnable', null, InputOption::VALUE_NONE, 'Create runnable solution'], ]; } } Commands/stubs/runnable-solution.stub 0000644 00000001300 15060250375 0014021 0 ustar 00 <?php namespace DummyNamespace; use Spatie\ErrorSolutions\Contracts\RunnableSolution; class DummyClass implements RunnableSolution { public function getSolutionTitle(): string { return ''; } public function getDocumentationLinks(): array { return []; } public function getSolutionActionDescription(): string { return ''; } public function getRunButtonText(): string { return ''; } public function getSolutionDescription(): string { return ''; } public function getRunParameters(): array { return []; } public function run(array $parameters = []) { // } } Commands/stubs/solution.stub 0000644 00000000560 15060250375 0012224 0 ustar 00 <?php namespace DummyNamespace; use Spatie\ErrorSolutions\Contracts\Solution; class DummyClass implements Solution { public function getSolutionTitle(): string { return ''; } public function getSolutionDescription(): string { return ''; } public function getDocumentationLinks(): array { return []; } } Commands/stubs/solution-provider.stub 0000644 00000000542 15060250375 0014054 0 ustar 00 <?php namespace DummyNamespace; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Throwable; class DummyClass implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return false; } public function getSolutions(Throwable $throwable): array { return []; } } Commands/TestCommand.php 0000644 00000011250 15060250375 0011236 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Commands; use Composer\InstalledVersions; use Exception; use Illuminate\Config\Repository; use Illuminate\Console\Command; use Illuminate\Log\LogManager; use Spatie\FlareClient\Flare; use Spatie\FlareClient\Http\Exceptions\BadResponseCode; class TestCommand extends Command { protected $signature = 'flare:test'; protected $description = 'Send a test notification to Flare'; protected Repository $config; public function handle(Repository $config): void { $this->config = $config; $this->checkFlareKey(); if (app()->make('log') instanceof LogManager) { $this->checkFlareLogger(); } $this->sendTestException(); } protected function checkFlareKey(): self { $message = empty($this->config->get('flare.key')) ? '❌ Flare key not specified. Make sure you specify a value in the `key` key of the `flare` config file.' : '✅ Flare key specified'; $this->info($message); return $this; } public function checkFlareLogger(): self { $defaultLogChannel = $this->config->get('logging.default'); $activeStack = $this->config->get("logging.channels.{$defaultLogChannel}"); if (is_null($activeStack)) { $this->info("❌ The default logging channel `{$defaultLogChannel}` is not configured in the `logging` config file"); } if (! isset($activeStack['channels']) || ! in_array('flare', $activeStack['channels'])) { $this->info("❌ The logging channel `{$defaultLogChannel}` does not contain the 'flare' channel"); } if (is_null($this->config->get('logging.channels.flare'))) { $this->info('❌ There is no logging channel named `flare` in the `logging` config file'); } if ($this->config->get('logging.channels.flare.driver') !== 'flare') { $this->info('❌ The `flare` logging channel defined in the `logging` config file is not set to `flare`.'); } if ($this->config->get('ignition.with_stack_frame_arguments') && ini_get('zend.exception_ignore_args')) { $this->info('⚠️ The `zend.exception_ignore_args` php ini setting is enabled. This will prevent Flare from showing stack trace arguments.'); } $this->info('✅ The Flare logging driver was configured correctly.'); return $this; } protected function sendTestException(): void { $testException = new Exception('This is an exception to test if the integration with Flare works.'); try { app(Flare::class)->sendTestReport($testException); $this->info(''); } catch (Exception $exception) { $this->warn('❌ We were unable to send an exception to Flare. '); if ($exception instanceof BadResponseCode) { $this->info(''); $message = 'Unknown error'; $body = $exception->response->getBody(); if (is_array($body) && isset($body['message'])) { $message = $body['message']; } $this->warn("{$exception->response->getHttpResponseCode()} - {$message}"); } else { $this->warn($exception->getMessage()); } $this->warn('Make sure that your key is correct and that you have a valid subscription.'); $this->info(''); $this->info('For more info visit the docs on https://flareapp.io/docs/ignition-for-laravel/introduction'); $this->info('You can see the status page of Flare at https://status.flareapp.io'); $this->info('Flare support can be reached at support@flareapp.io'); $this->line(''); $this->line('Extra info'); $this->table([], [ ['Platform', PHP_OS], ['PHP', phpversion()], ['Laravel', app()->version()], ['spatie/ignition', InstalledVersions::getVersion('spatie/ignition')], ['spatie/laravel-ignition', InstalledVersions::getVersion('spatie/laravel-ignition')], ['spatie/flare-client-php', InstalledVersions::getVersion('spatie/flare-client-php')], /** @phpstan-ignore-next-line */ ['Curl', curl_version()['version'] ?? 'Unknown'], /** @phpstan-ignore-next-line */ ['SSL', curl_version()['ssl_version'] ?? 'Unknown'], ]); if ($this->output->isVerbose()) { throw $exception; } return; } $this->info('We tried to send an exception to Flare. Please check if it arrived!'); } } Commands/SolutionProviderMakeCommand.php 0000644 00000001103 15060250375 0014440 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Commands; use Illuminate\Console\GeneratorCommand; class SolutionProviderMakeCommand extends GeneratorCommand { protected $name = 'ignition:make-solution-provider'; protected $description = 'Create a new custom Ignition solution provider class'; protected $type = 'Solution Provider'; protected function getStub(): string { return __DIR__.'/stubs/solution-provider.stub'; } protected function getDefaultNamespace($rootNamespace) { return "{$rootNamespace}\\SolutionProviders"; } } FlareMiddleware/AddContext.php 0000644 00000001162 15060250375 0012344 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Closure; use Illuminate\Log\Context\Repository; use Illuminate\Support\Facades\Context; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddContext implements FlareMiddleware { public function handle(Report $report, Closure $next) { if (! class_exists(Repository::class)) { return $next($report); } $allContext = Context::all(); if (count($allContext)) { $report->group('laravel_context', $allContext); } return $next($report); } } FlareMiddleware/AddExceptionHandledStatus.php 0000644 00000002517 15060250375 0015347 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Closure; use Spatie\Backtrace\Backtrace; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Throwable; class AddExceptionHandledStatus implements FlareMiddleware { public function handle(Report $report, Closure $next) { $frames = Backtrace::create()->limit(40)->frames(); $frameCount = count($frames); try { foreach ($frames as $i => $frame) { // Check first frame, probably Illuminate\Foundation\Exceptions\Handler::report() // Next frame should be: Illuminate/Foundation/helpers.php::report() if ($frame->method !== 'report') { continue; } if ($frame->class === null) { continue; } if ($i === $frameCount - 1) { continue; } if ($frames[$i + 1]->class !== null) { continue; } if ($frames[$i + 1]->method !== 'report') { continue; } $report->handled(); break; } } catch (Throwable) { // Do nothing } return $next($report); } } FlareMiddleware/AddEnvironmentInformation.php 0000644 00000001333 15060250375 0015432 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Closure; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddEnvironmentInformation implements FlareMiddleware { public function handle(Report $report, Closure $next) { $report->frameworkVersion(app()->version()); $report->group('env', [ 'laravel_version' => app()->version(), 'laravel_locale' => app()->getLocale(), 'laravel_config_cached' => app()->configurationIsCached(), 'app_debug' => config('app.debug'), 'app_env' => config('app.env'), 'php_version' => phpversion(), ]); return $next($report); } } FlareMiddleware/AddDumps.php 0000644 00000001107 15060250375 0012007 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Closure; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder; class AddDumps implements FlareMiddleware { protected DumpRecorder $dumpRecorder; public function __construct() { $this->dumpRecorder = app(DumpRecorder::class); } public function handle(Report $report, Closure $next) { $report->group('dumps', $this->dumpRecorder->getDumps()); return $next($report); } } FlareMiddleware/AddLogs.php 0000644 00000001057 15060250375 0011627 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder; class AddLogs implements FlareMiddleware { protected LogRecorder $logRecorder; public function __construct() { $this->logRecorder = app(LogRecorder::class); } public function handle(Report $report, $next) { $report->group('logs', $this->logRecorder->getLogMessages()); return $next($report); } } FlareMiddleware/AddQueries.php 0000644 00000000754 15060250375 0012343 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder; class AddQueries { protected QueryRecorder $queryRecorder; public function __construct() { $this->queryRecorder = app(QueryRecorder::class); } public function handle(Report $report, $next) { $report->group('queries', $this->queryRecorder->getQueries()); return $next($report); } } FlareMiddleware/AddNotifierName.php 0000644 00000000617 15060250375 0013304 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddNotifierName implements FlareMiddleware { public const NOTIFIER_NAME = 'Laravel Client'; public function handle(Report $report, $next) { $report->notifierName(static::NOTIFIER_NAME); return $next($report); } } FlareMiddleware/AddJobs.php 0000644 00000001117 15060250375 0011615 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder; class AddJobs implements FlareMiddleware { protected JobRecorder $jobRecorder; public function __construct() { $this->jobRecorder = app(JobRecorder::class); } public function handle(Report $report, $next) { if ($job = $this->jobRecorder->getJob()) { $report->group('job', $job); } return $next($report); } } FlareMiddleware/AddExceptionInformation.php 0000644 00000002717 15060250375 0015073 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Illuminate\Database\QueryException; use Spatie\FlareClient\Contracts\ProvidesFlareContext; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddExceptionInformation implements FlareMiddleware { public function handle(Report $report, $next) { $throwable = $report->getThrowable(); $this->addUserDefinedContext($report); if (! $throwable instanceof QueryException) { return $next($report); } $report->group('exception', [ 'raw_sql' => $throwable->getSql(), ]); return $next($report); } private function addUserDefinedContext(Report $report): void { $throwable = $report->getThrowable(); if ($throwable === null) { return; } if ($throwable instanceof ProvidesFlareContext) { // ProvidesFlareContext writes directly to context groups and is handled in the flare-client-php package. return; } if (! method_exists($throwable, 'context')) { return; } $context = $throwable->context(); if (! is_array($context)) { return; } $exceptionContextGroup = []; foreach ($context as $key => $value) { $exceptionContextGroup[$key] = $value; } $report->group('exception', $exceptionContextGroup); } } Http/Middleware/RunnableSolutionsEnabled.php 0000644 00000000532 15060250375 0015215 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Middleware; use Closure; use Spatie\LaravelIgnition\Support\RunnableSolutionsGuard; class RunnableSolutionsEnabled { public function handle($request, Closure $next) { if (! RunnableSolutionsGuard::check()) { abort(404); } return $next($request); } } Http/Controllers/HealthCheckController.php 0000644 00000000767 15060250375 0014726 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Controllers; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Str; class HealthCheckController { public function __invoke() { return [ 'can_execute_commands' => $this->canExecuteCommands(), ]; } protected function canExecuteCommands(): bool { Artisan::call('help', ['--version']); $output = Artisan::output(); return Str::contains($output, app()->version()); } } Http/Controllers/ExecuteSolutionController.php 0000644 00000002653 15060250375 0015716 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Controllers; use Illuminate\Foundation\Validation\ValidatesRequests; use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository; use Spatie\LaravelIgnition\Exceptions\CannotExecuteSolutionForNonLocalIp; use Spatie\LaravelIgnition\Http\Requests\ExecuteSolutionRequest; use Spatie\LaravelIgnition\Support\RunnableSolutionsGuard; class ExecuteSolutionController { use ValidatesRequests; public function __invoke( ExecuteSolutionRequest $request, SolutionProviderRepository $solutionProviderRepository ) { $this ->ensureRunnableSolutionsEnabled() ->ensureLocalRequest(); $solution = $request->getRunnableSolution(); $solution->run($request->get('parameters', [])); return response()->noContent(); } public function ensureRunnableSolutionsEnabled(): self { // Should already be checked in middleware but we want to be 100% certain. abort_unless(RunnableSolutionsGuard::check(), 400); return $this; } public function ensureLocalRequest(): self { $ipIsPublic = filter_var( request()->ip(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); if ($ipIsPublic) { throw CannotExecuteSolutionForNonLocalIp::make(); } return $this; } } Http/Controllers/UpdateConfigController.php 0000644 00000000605 15060250375 0015122 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Controllers; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\LaravelIgnition\Http\Requests\UpdateConfigRequest; class UpdateConfigController { public function __invoke(UpdateConfigRequest $request) { $result = (new IgnitionConfig())->saveValues($request->validated()); return response()->json($result); } } Http/Requests/UpdateConfigRequest.php 0000644 00000000654 15060250375 0013740 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; class UpdateConfigRequest extends FormRequest { public function rules(): array { return [ 'theme' => ['required', Rule::in(['light', 'dark', 'auto'])], 'editor' => ['required'], 'hide_solutions' => ['required', 'boolean'], ]; } } Http/Requests/ExecuteSolutionRequest.php 0000644 00000001771 15060250375 0014530 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Spatie\ErrorSolutions\Contracts\RunnableSolution; use Spatie\ErrorSolutions\Contracts\Solution; use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository; class ExecuteSolutionRequest extends FormRequest { public function rules(): array { return [ 'solution' => 'required', 'parameters' => 'array', ]; } public function getSolution(): Solution { $solution = app(SolutionProviderRepository::class) ->getSolutionForClass($this->get('solution')); abort_if(is_null($solution), 404, 'Solution could not be found'); return $solution; } public function getRunnableSolution(): RunnableSolution { $solution = $this->getSolution(); if (! $solution instanceof RunnableSolution) { abort(404, 'Runnable solution could not be found'); } return $solution; } } Facades/Flare.php 0000644 00000001246 15060250375 0007642 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Facades; use Illuminate\Support\Facades\Facade; use Spatie\LaravelIgnition\Support\SentReports; /** * @method static void glow(string $name, string $messageLevel = \Spatie\FlareClient\Enums\MessageLevels::INFO, array $metaData = []) * @method static void context($key, $value) * @method static void group(string $groupName, array $properties) * * @see \Spatie\FlareClient\Flare */ class Flare extends Facade { protected static function getFacadeAccessor() { return \Spatie\FlareClient\Flare::class; } public static function sentReports(): SentReports { return app(SentReports::class); } } Solutions/SolutionTransformers/LaravelSolutionTransformer.php 0000644 00000003322 15060250375 0021047 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionTransformers; use Spatie\ErrorSolutions\Contracts\RunnableSolution; use Spatie\ErrorSolutions\Solutions\SolutionTransformer; use Spatie\LaravelIgnition\Http\Controllers\ExecuteSolutionController; use Throwable; class LaravelSolutionTransformer extends SolutionTransformer { /** @return array<string|mixed> */ public function toArray(): array { $baseProperties = parent::toArray(); if (! $this->isRunnable()) { return $baseProperties; } /** @var RunnableSolution $solution Type shenanigans */ $solution = $this->solution; $runnableProperties = [ 'is_runnable' => true, 'action_description' => $solution->getSolutionActionDescription(), 'run_button_text' => $solution->getRunButtonText(), 'execute_endpoint' => $this->executeEndpoint(), 'run_parameters' => $solution->getRunParameters(), ]; return array_merge($baseProperties, $runnableProperties); } protected function isRunnable(): bool { if (! $this->solution instanceof RunnableSolution) { return false; } if (! $this->executeEndpoint()) { return false; } return true; } protected function executeEndpoint(): ?string { try { // The action class needs to be prefixed with a `\` to Laravel from trying // to add its own global namespace from RouteServiceProvider::$namespace. return action('\\'.ExecuteSolutionController::class); } catch (Throwable $exception) { report($exception); return null; } } } ContextProviders/LaravelRequestContextProvider.php 0000644 00000005314 15060250375 0016624 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request as LaravelRequest; use Spatie\FlareClient\Context\RequestContextProvider; use Symfony\Component\HttpFoundation\Request as SymphonyRequest; use Throwable; class LaravelRequestContextProvider extends RequestContextProvider { protected LaravelRequest|SymphonyRequest|null $request; public function __construct(LaravelRequest $request) { $this->request = $request; } /** @return null|array<string, mixed> */ public function getUser(): array|null { try { /** @var object|null $user */ /** @phpstan-ignore-next-line */ $user = $this->request?->user(); if (! $user) { return null; } } catch (Throwable) { return null; } try { if (method_exists($user, 'toFlare')) { return $user->toFlare(); } if (method_exists($user, 'toArray')) { return $user->toArray(); } } catch (Throwable $e) { return null; } return null; } /** @return null|array<string, mixed> */ public function getRoute(): array|null { /** * @phpstan-ignore-next-line * @var \Illuminate\Routing\Route|null $route */ $route = $this->request->route(); if (! $route) { return null; } return [ 'route' => $route->getName(), 'routeParameters' => $this->getRouteParameters(), 'controllerAction' => $route->getActionName(), 'middleware' => array_values($route->gatherMiddleware() ?? []), ]; } /** @return array<int, mixed> */ protected function getRouteParameters(): array { try { /** @phpstan-ignore-next-line */ return collect(optional($this->request->route())->parameters ?? []) ->map(fn ($parameter) => $parameter instanceof Model ? $parameter->withoutRelations() : $parameter) ->map(function ($parameter) { return method_exists($parameter, 'toFlare') ? $parameter->toFlare() : $parameter; }) ->toArray(); } catch (Throwable) { return []; } } /** @return array<int, mixed> */ public function toArray(): array { $properties = parent::toArray(); if ($route = $this->getRoute()) { $properties['route'] = $route; } if ($user = $this->getUser()) { $properties['user'] = $user; } return $properties; } } ContextProviders/LaravelLivewireRequestContextProvider.php 0000644 00000007352 15060250375 0020337 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Livewire\LivewireManager; use Livewire\Mechanisms\ComponentRegistry; class LaravelLivewireRequestContextProvider extends LaravelRequestContextProvider { public function __construct( Request $request, protected LivewireManager $livewireManager ) { parent::__construct($request); } /** @return array<string, string> */ public function getRequest(): array { $properties = parent::getRequest(); $properties['method'] = $this->livewireManager->originalMethod(); $properties['url'] = $this->livewireManager->originalUrl(); return $properties; } /** @return array<int|string, mixed> */ public function toArray(): array { $properties = parent::toArray(); $properties['livewire'] = $this->getLivewireInformation(); return $properties; } /** @return array<int, mixed> */ protected function getLivewireInformation(): array { if ($this->request->has('components')) { $data = []; foreach ($this->request->get('components') as $component) { $snapshot = json_decode($component['snapshot'], true); $class = app(ComponentRegistry::class)->getClass($snapshot['memo']['name']); $data[] = [ 'component_class' => $class ?? null, 'data' => $snapshot['data'], 'memo' => $snapshot['memo'], 'updates' => $this->resolveUpdates($component['updates']), 'calls' => $component['calls'], ]; } return $data; } /** @phpstan-ignore-next-line */ $componentId = $this->request->input('fingerprint.id'); /** @phpstan-ignore-next-line */ $componentAlias = $this->request->input('fingerprint.name'); if ($componentAlias === null) { return []; } try { $componentClass = $this->livewireManager->getClass($componentAlias); } catch (Exception $e) { $componentClass = null; } /** @phpstan-ignore-next-line */ $updates = $this->request->input('updates') ?? []; /** @phpstan-ignore-next-line */ $updates = $this->request->input('updates') ?? []; return [ [ 'component_class' => $componentClass, 'component_alias' => $componentAlias, 'component_id' => $componentId, 'data' => $this->resolveData(), 'updates' => $this->resolveUpdates($updates), ], ]; } /** @return array<string, mixed> */ protected function resolveData(): array { /** @phpstan-ignore-next-line */ $data = $this->request->input('serverMemo.data') ?? []; /** @phpstan-ignore-next-line */ $dataMeta = $this->request->input('serverMemo.dataMeta') ?? []; foreach ($dataMeta['modelCollections'] ?? [] as $key => $value) { $data[$key] = array_merge($data[$key] ?? [], $value); } foreach ($dataMeta['models'] ?? [] as $key => $value) { $data[$key] = array_merge($data[$key] ?? [], $value); } return $data; } /** @return array<string, mixed> */ protected function resolveUpdates(array $updates): array { /** @phpstan-ignore-next-line */ $updates = $this->request->input('updates') ?? []; return array_map(function (array $update) { $update['payload'] = Arr::except($update['payload'] ?? [], ['id']); return $update; }, $updates); } } ContextProviders/LaravelConsoleContextProvider.php 0000644 00000000272 15060250375 0016574 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Spatie\FlareClient\Context\ConsoleContextProvider; class LaravelConsoleContextProvider extends ConsoleContextProvider { } ContextProviders/LaravelContextProviderDetector.php 0000644 00000001636 15060250375 0016750 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Illuminate\Http\Request; use Livewire\LivewireManager; use Spatie\FlareClient\Context\ContextProvider; use Spatie\FlareClient\Context\ContextProviderDetector; class LaravelContextProviderDetector implements ContextProviderDetector { public function detectCurrentContext(): ContextProvider { if (app()->runningInConsole()) { return new LaravelConsoleContextProvider($_SERVER['argv'] ?? []); } $request = app(Request::class); if ($this->isRunningLiveWire($request)) { return new LaravelLivewireRequestContextProvider($request, app(LivewireManager::class)); } return new LaravelRequestContextProvider($request); } protected function isRunningLiveWire(Request $request): bool { return $request->hasHeader('x-livewire') && $request->hasHeader('referer'); } } helpers.php 0000644 00000000773 15060250375 0006731 0 ustar 00 <?php use Spatie\LaravelIgnition\Renderers\ErrorPageRenderer; if (! function_exists('ddd')) { function ddd() { $args = func_get_args(); if (count($args) === 0) { throw new Exception('You should pass at least 1 argument to `ddd`'); } call_user_func_array('dump', $args); $renderer = app()->make(ErrorPageRenderer::class); $exception = new Exception('Dump, Die, Debug'); $renderer->render($exception); die(); } } Exceptions/ViewException.php 0000644 00000002307 15060250375 0012174 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use ErrorException; use Spatie\FlareClient\Contracts\ProvidesFlareContext; use Spatie\LaravelIgnition\Recorders\DumpRecorder\HtmlDumper; class ViewException extends ErrorException implements ProvidesFlareContext { /** @var array<string, mixed> */ protected array $viewData = []; protected string $view = ''; /** * @param array<string, mixed> $data * * @return void */ public function setViewData(array $data): void { $this->viewData = $data; } /** @return array<string, mixed> */ public function getViewData(): array { return $this->viewData; } public function setView(string $path): void { $this->view = $path; } protected function dumpViewData(mixed $variable): string { return (new HtmlDumper())->dumpVariable($variable); } /** @return array<string, mixed> */ public function context(): array { $context = [ 'view' => [ 'view' => $this->view, ], ]; $context['view']['data'] = array_map([$this, 'dumpViewData'], $this->viewData); return $context; } } Exceptions/InvalidConfig.php 0000644 00000001720 15060250375 0012115 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use Exception; use Monolog\Level; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\ProvidesSolution; use Spatie\ErrorSolutions\Contracts\Solution; class InvalidConfig extends Exception implements ProvidesSolution { public static function invalidLogLevel(string $logLevel): self { return new self("Invalid log level `{$logLevel}` specified."); } public function getSolution(): Solution { $validLogLevels = array_map( fn (string $level) => strtolower($level), array_keys(Level::VALUES) ); $validLogLevelsString = implode(',', $validLogLevels); return BaseSolution::create() ->setSolutionTitle('You provided an invalid log level') ->setSolutionDescription("Please change the log level in your `config/logging.php` file. Valid log levels are {$validLogLevelsString}."); } } Exceptions/ViewExceptionWithSolution.php 0000644 00000000727 15060250375 0014571 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use Spatie\ErrorSolutions\Contracts\ProvidesSolution; use Spatie\ErrorSolutions\Contracts\Solution; class ViewExceptionWithSolution extends ViewException implements ProvidesSolution { protected Solution $solution; public function setSolution(Solution $solution): void { $this->solution = $solution; } public function getSolution(): Solution { return $this->solution; } } Exceptions/CannotExecuteSolutionForNonLocalIp.php 0000644 00000001522 15060250375 0016271 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use Spatie\ErrorSolutions\Contracts\BaseSolution; use Spatie\ErrorSolutions\Contracts\ProvidesSolution; use Spatie\ErrorSolutions\Contracts\Solution; use Symfony\Component\HttpKernel\Exception\HttpException; class CannotExecuteSolutionForNonLocalIp extends HttpException implements ProvidesSolution { public static function make(): self { return new self(403, 'Solutions cannot be run from your current IP address.'); } public function getSolution(): Solution { return BaseSolution::create() ->setSolutionTitle('Checking your environment settings') ->setSolutionDescription("Solutions can only be executed by requests from a local IP address. Keep in mind that `APP_DEBUG` should set to false on any production environment."); } } Recorders/QueryRecorder/Query.php 0000644 00000003157 15060250375 0013116 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\QueryRecorder; use Illuminate\Database\Events\QueryExecuted; class Query { protected string $sql; protected float $time; protected string $connectionName; /** @var array<string, string>|null */ protected ?array $bindings; protected float $microtime; public static function fromQueryExecutedEvent(QueryExecuted $queryExecuted, bool $reportBindings = false): self { return new self( $queryExecuted->sql, $queryExecuted->time, /** @phpstan-ignore-next-line */ $queryExecuted->connectionName ?? '', $reportBindings ? $queryExecuted->bindings : null ); } /** * @param string $sql * @param float $time * @param string $connectionName * @param array<string, string>|null $bindings * @param float|null $microtime */ protected function __construct( string $sql, float $time, string $connectionName, ?array $bindings = null, ?float $microtime = null ) { $this->sql = $sql; $this->time = $time; $this->connectionName = $connectionName; $this->bindings = $bindings; $this->microtime = $microtime ?? microtime(true); } /** * @return array<string, mixed> */ public function toArray(): array { return [ 'sql' => $this->sql, 'time' => $this->time, 'connection_name' => $this->connectionName, 'bindings' => $this->bindings, 'microtime' => $this->microtime, ]; } } Recorders/QueryRecorder/QueryRecorder.php 0000644 00000003662 15060250375 0014605 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\QueryRecorder; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Events\QueryExecuted; class QueryRecorder { /** @var \Spatie\LaravelIgnition\Recorders\QueryRecorder\Query[] */ protected array $queries = []; protected Application $app; protected bool $reportBindings = true; protected ?int $maxQueries; public function __construct( Application $app, bool $reportBindings = true, ?int $maxQueries = 200 ) { $this->app = $app; $this->reportBindings = $reportBindings; $this->maxQueries = $maxQueries; } public function start(): self { /** @phpstan-ignore-next-line */ $this->app['events']->listen(QueryExecuted::class, [$this, 'record']); return $this; } public function record(QueryExecuted $queryExecuted): void { $this->queries[] = Query::fromQueryExecutedEvent($queryExecuted, $this->reportBindings); if (is_int($this->maxQueries)) { $this->queries = array_slice($this->queries, -$this->maxQueries); } } /** * @return array<int, array<string, mixed>> */ public function getQueries(): array { $queries = []; foreach ($this->queries as $query) { $queries[] = $query->toArray(); } return $queries; } public function reset(): void { $this->queries = []; } public function getReportBindings(): bool { return $this->reportBindings; } public function setReportBindings(bool $reportBindings): self { $this->reportBindings = $reportBindings; return $this; } public function getMaxQueries(): ?int { return $this->maxQueries; } public function setMaxQueries(?int $maxQueries): self { $this->maxQueries = $maxQueries; return $this; } } Recorders/JobRecorder/JobRecorder.php 0000644 00000012144 15060250375 0013612 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\JobRecorder; use DateTime; use Error; use Exception; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Queue\Job; use Illuminate\Queue\CallQueuedClosure; use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Jobs\RedisJob; use Illuminate\Support\Str; use ReflectionClass; use ReflectionProperty; use RuntimeException; class JobRecorder { protected ?Job $job = null; public function __construct( protected Application $app, protected int $maxChainedJobReportingDepth = 5, ) { } public function start(): self { /** @phpstan-ignore-next-line */ $this->app['events']->listen(JobExceptionOccurred::class, [$this, 'record']); return $this; } public function record(JobExceptionOccurred $event): void { $this->job = $event->job; } /** * @return array<string, mixed>|null */ public function getJob(): ?array { if ($this->job === null) { return null; } return array_merge( $this->getJobProperties(), [ 'name' => $this->job->resolveName(), 'connection' => $this->job->getConnectionName(), 'queue' => $this->job->getQueue(), ] ); } public function reset(): void { $this->job = null; } protected function getJobProperties(): array { $payload = collect($this->resolveJobPayload()); $properties = []; foreach ($payload as $key => $value) { if (! in_array($key, ['job', 'data', 'displayName'])) { $properties[$key] = $value; } } try { if (is_string($payload['data'])) { $properties['data'] = json_decode($payload['data'], true, 512, JSON_THROW_ON_ERROR); } } catch (Exception $exception) { } if ($pushedAt = DateTime::createFromFormat('U.u', $payload->get('pushedAt', ''))) { $properties['pushedAt'] = $pushedAt->format(DATE_ATOM); } try { $properties['data'] = $this->resolveCommandProperties( $this->resolveObjectFromCommand($payload['data']['command']), $this->maxChainedJobReportingDepth ); } catch (Exception $exception) { } return $properties; } protected function resolveJobPayload(): array { if (! $this->job instanceof RedisJob) { return $this->job->payload(); } try { return json_decode($this->job->getReservedJob(), true, 512, JSON_THROW_ON_ERROR); } catch (Exception $e) { return $this->job->payload(); } } protected function resolveCommandProperties(object $command, int $maxChainDepth): array { $propertiesToIgnore = ['job', 'closure']; $properties = collect((new ReflectionClass($command))->getProperties()) ->reject(function (ReflectionProperty $property) use ($propertiesToIgnore) { return in_array($property->name, $propertiesToIgnore); }) ->mapWithKeys(function (ReflectionProperty $property) use ($command) { try { $property->setAccessible(true); return [$property->name => $property->getValue($command)]; } catch (Error $error) { return [$property->name => 'uninitialized']; } }); if ($properties->has('chained')) { $properties['chained'] = $this->resolveJobChain($properties->get('chained'), $maxChainDepth); } return $properties->all(); } /** * @param array<string, mixed> $chainedCommands * @param int $maxDepth * * @return array */ protected function resolveJobChain(array $chainedCommands, int $maxDepth): array { if ($maxDepth === 0) { return ['Ignition stopped recording jobs after this point since the max chain depth was reached']; } return array_map( function (string $command) use ($maxDepth) { $commandObject = $this->resolveObjectFromCommand($command); return [ 'name' => $commandObject instanceof CallQueuedClosure ? $commandObject->displayName() : get_class($commandObject), 'data' => $this->resolveCommandProperties($commandObject, $maxDepth - 1), ]; }, $chainedCommands ); } // Taken from Illuminate\Queue\CallQueuedHandler protected function resolveObjectFromCommand(string $command): object { if (Str::startsWith($command, 'O:')) { return unserialize($command); } if ($this->app->bound(Encrypter::class)) { /** @phpstan-ignore-next-line */ return unserialize($this->app[Encrypter::class]->decrypt($command)); } throw new RuntimeException('Unable to extract job payload.'); } } Recorders/LogRecorder/LogRecorder.php 0000644 00000003730 15060250375 0013631 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\LogRecorder; use Illuminate\Contracts\Foundation\Application; use Illuminate\Log\Events\MessageLogged; use Throwable; class LogRecorder { /** @var \Spatie\LaravelIgnition\Recorders\LogRecorder\LogMessage[] */ protected array $logMessages = []; protected Application $app; protected ?int $maxLogs; public function __construct(Application $app, ?int $maxLogs = null) { $this->app = $app; $this->maxLogs = $maxLogs; } public function start(): self { /** @phpstan-ignore-next-line */ $this->app['events']->listen(MessageLogged::class, [$this, 'record']); return $this; } public function record(MessageLogged $event): void { if ($this->shouldIgnore($event)) { return; } $this->logMessages[] = LogMessage::fromMessageLoggedEvent($event); if (is_int($this->maxLogs)) { $this->logMessages = array_slice($this->logMessages, -$this->maxLogs); } } /** @return array<array<int,string>> */ public function getLogMessages(): array { return $this->toArray(); } /** @return array<int, mixed> */ public function toArray(): array { $logMessages = []; foreach ($this->logMessages as $log) { $logMessages[] = $log->toArray(); } return $logMessages; } protected function shouldIgnore(mixed $event): bool { if (! isset($event->context['exception'])) { return false; } if (! $event->context['exception'] instanceof Throwable) { return false; } return true; } public function reset(): void { $this->logMessages = []; } public function getMaxLogs(): ?int { return $this->maxLogs; } public function setMaxLogs(?int $maxLogs): self { $this->maxLogs = $maxLogs; return $this; } } Recorders/LogRecorder/LogMessage.php 0000644 00000002364 15060250375 0013452 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\LogRecorder; use Illuminate\Log\Events\MessageLogged; class LogMessage { protected ?string $message; protected string $level; /** @var array<string, string> */ protected array $context = []; protected ?float $microtime; /** * @param string|null $message * @param string $level * @param array<string, string> $context * @param float|null $microtime */ public function __construct( ?string $message, string $level, array $context = [], ?float $microtime = null ) { $this->message = $message; $this->level = $level; $this->context = $context; $this->microtime = $microtime ?? microtime(true); } public static function fromMessageLoggedEvent(MessageLogged $event): self { return new self( $event->message, $event->level, $event->context ); } /** @return array<string, mixed> */ public function toArray(): array { return [ 'message' => $this->message, 'level' => $this->level, 'context' => $this->context, 'microtime' => $this->microtime, ]; } } Recorders/DumpRecorder/HtmlDumper.php 0000644 00000001622 15060250375 0013665 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper as BaseHtmlDumper; class HtmlDumper extends BaseHtmlDumper { public function __construct($output = null, string $charset = null, int $flags = 0) { parent::__construct($output, $charset, $flags); $this->setDumpHeader(''); } public function dumpVariable($variable): string { $cloner = new VarCloner(); $clonedData = $cloner->cloneVar($variable)->withMaxDepth(3); return $this->dump($clonedData); } public function dump(Data $data, $output = null, array $extraDisplayOptions = []): string { return (string)parent::dump($data, true, [ 'maxDepth' => 3, 'maxStringLength' => 160, ]); } } Recorders/DumpRecorder/DumpRecorder.php 0000644 00000007424 15060250375 0014205 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Arr; use ReflectionMethod; use ReflectionProperty; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\VarDumper; class DumpRecorder { /** @var array<array<int,mixed>> */ protected array $dumps = []; protected Application $app; protected static bool $registeredHandler = false; public function __construct(Application $app) { $this->app = $app; } public function start(): self { $multiDumpHandler = new MultiDumpHandler(); $this->app->singleton(MultiDumpHandler::class, fn () => $multiDumpHandler); if (! self::$registeredHandler) { static::$registeredHandler = true; $this->ensureOriginalHandlerExists(); $originalHandler = VarDumper::setHandler(fn ($dumpedVariable) => $multiDumpHandler->dump($dumpedVariable)); $multiDumpHandler?->addHandler($originalHandler); $multiDumpHandler->addHandler(fn ($var) => (new DumpHandler($this))->dump($var)); } return $this; } public function record(Data $data): void { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 11); $sourceFrame = $this->findSourceFrame($backtrace); $file = (string) Arr::get($sourceFrame, 'file'); $lineNumber = (int) Arr::get($sourceFrame, 'line'); $htmlDump = (new HtmlDumper())->dump($data); $this->dumps[] = new Dump($htmlDump, $file, $lineNumber); } public function getDumps(): array { return $this->toArray(); } public function reset() { $this->dumps = []; } public function toArray(): array { $dumps = []; foreach ($this->dumps as $dump) { $dumps[] = $dump->toArray(); } return $dumps; } /* * Only the `VarDumper` knows how to create the orignal HTML or CLI VarDumper. * Using reflection and the private VarDumper::register() method we can force it * to create and register a new VarDumper::$handler before we'll overwrite it. * Of course, we only need to do this if there isn't a registered VarDumper::$handler. * * @throws \ReflectionException */ protected function ensureOriginalHandlerExists(): void { $reflectionProperty = new ReflectionProperty(VarDumper::class, 'handler'); $reflectionProperty->setAccessible(true); $handler = $reflectionProperty->getValue(); if (! $handler) { // No handler registered yet, so we'll force VarDumper to create one. $reflectionMethod = new ReflectionMethod(VarDumper::class, 'register'); $reflectionMethod->setAccessible(true); $reflectionMethod->invoke(null); } } /** * Find the first meaningful stack frame that is not the `DumpRecorder` itself. * * @template T of array{class?: class-string, function?: string, line?: int, file?: string} * * @param array<T> $stacktrace * * @return null|T */ protected function findSourceFrame(array $stacktrace): ?array { $seenVarDumper = false; foreach ($stacktrace as $frame) { // Keep looping until we're past the VarDumper::dump() call in Symfony's helper functions file. if (Arr::get($frame, 'class') === VarDumper::class && Arr::get($frame, 'function') === 'dump') { $seenVarDumper = true; continue; } if (! $seenVarDumper) { continue; } // Return the next frame in the stack after the VarDumper::dump() call: return $frame; } return null; } } Recorders/DumpRecorder/Dump.php 0000644 00000001421 15060250375 0012506 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; class Dump { protected string $htmlDump; protected ?string $file; protected ?int $lineNumber; protected float $microtime; public function __construct(string $htmlDump, ?string $file, ?int $lineNumber, ?float $microtime = null) { $this->htmlDump = $htmlDump; $this->file = $file; $this->lineNumber = $lineNumber; $this->microtime = $microtime ?? microtime(true); } /** @return array<string, mixed> */ public function toArray(): array { return [ 'html_dump' => $this->htmlDump, 'file' => $this->file, 'line_number' => $this->lineNumber, 'microtime' => $this->microtime, ]; } } Recorders/DumpRecorder/MultiDumpHandler.php 0000644 00000000765 15060250375 0015031 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; class MultiDumpHandler { /** @var array<int, callable|null> */ protected array $handlers = []; public function dump(mixed $value): void { foreach ($this->handlers as $handler) { if ($handler) { $handler($value); } } } public function addHandler(callable $callable = null): self { $this->handlers[] = $callable; return $this; } } Recorders/DumpRecorder/DumpHandler.php 0000644 00000000704 15060250375 0014007 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; use Symfony\Component\VarDumper\Cloner\VarCloner; class DumpHandler { protected DumpRecorder $dumpRecorder; public function __construct(DumpRecorder $dumpRecorder) { $this->dumpRecorder = $dumpRecorder; } public function dump(mixed $value): void { $data = (new VarCloner)->cloneVar($value); $this->dumpRecorder->record($data); } } Config/IgnitionConfig.php 0000644 00000015536 15060250720 0011377 0 ustar 00 <?php namespace Spatie\Ignition\Config; use Illuminate\Contracts\Support\Arrayable; use Spatie\Ignition\Contracts\ConfigManager; use Throwable; /** @implements Arrayable<string, string|null|bool|array<string, mixed>> */ class IgnitionConfig implements Arrayable { private ConfigManager $manager; public static function loadFromConfigFile(): self { return (new self())->loadConfigFile(); } /** * @param array<string, mixed> $options */ public function __construct(protected array $options = []) { $defaultOptions = $this->getDefaultOptions(); $this->options = array_merge($defaultOptions, $options); $this->manager = $this->initConfigManager(); } public function setOption(string $name, string $value): self { $this->options[$name] = $value; return $this; } private function initConfigManager(): ConfigManager { try { /** @phpstan-ignore-next-line */ return app(ConfigManager::class); } catch (Throwable) { return new FileConfigManager(); } } /** @param array<string, string> $options */ public function merge(array $options): self { $this->options = array_merge($this->options, $options); return $this; } public function loadConfigFile(): self { $this->merge($this->getConfigOptions()); return $this; } /** @return array<string, mixed> */ public function getConfigOptions(): array { return $this->manager->load(); } /** * @param array<string, mixed> $options * @return bool */ public function saveValues(array $options): bool { return $this->manager->save($options); } public function hideSolutions(): bool { return $this->options['hide_solutions'] ?? false; } public function editor(): ?string { return $this->options['editor'] ?? null; } /** * @return array<string, mixed> $options */ public function editorOptions(): array { return $this->options['editor_options'] ?? []; } public function remoteSitesPath(): ?string { return $this->options['remote_sites_path'] ?? null; } public function localSitesPath(): ?string { return $this->options['local_sites_path'] ?? null; } public function theme(): ?string { return $this->options['theme'] ?? null; } public function shareButtonEnabled(): bool { return (bool)($this->options['enable_share_button'] ?? false); } public function shareEndpoint(): string { return $this->options['share_endpoint'] ?? $this->getDefaultOptions()['share_endpoint']; } public function runnableSolutionsEnabled(): bool { return (bool)($this->options['enable_runnable_solutions'] ?? false); } /** @return array<string, string|null|bool|array<string, mixed>> */ public function toArray(): array { return [ 'editor' => $this->editor(), 'theme' => $this->theme(), 'hideSolutions' => $this->hideSolutions(), 'remoteSitesPath' => $this->remoteSitesPath(), 'localSitesPath' => $this->localSitesPath(), 'enableShareButton' => $this->shareButtonEnabled(), 'enableRunnableSolutions' => $this->runnableSolutionsEnabled(), 'directorySeparator' => DIRECTORY_SEPARATOR, 'editorOptions' => $this->editorOptions(), 'shareEndpoint' => $this->shareEndpoint(), ]; } /** * @return array<string, mixed> $options */ protected function getDefaultOptions(): array { return [ 'share_endpoint' => 'https://flareapp.io/api/public-reports', 'theme' => 'light', 'editor' => 'vscode', 'editor_options' => [ 'clipboard' => [ 'label' => 'Clipboard', 'url' => '%path:%line', 'clipboard' => true, ], 'sublime' => [ 'label' => 'Sublime', 'url' => 'subl://open?url=file://%path&line=%line', ], 'textmate' => [ 'label' => 'TextMate', 'url' => 'txmt://open?url=file://%path&line=%line', ], 'emacs' => [ 'label' => 'Emacs', 'url' => 'emacs://open?url=file://%path&line=%line', ], 'macvim' => [ 'label' => 'MacVim', 'url' => 'mvim://open/?url=file://%path&line=%line', ], 'phpstorm' => [ 'label' => 'PhpStorm', 'url' => 'phpstorm://open?file=%path&line=%line', ], 'phpstorm-remote' => [ 'label' => 'PHPStorm Remote', 'url' => 'javascript:r = new XMLHttpRequest;r.open("get", "http://localhost:63342/api/file/%path:%line");r.send()', ], 'idea' => [ 'label' => 'Idea', 'url' => 'idea://open?file=%path&line=%line', ], 'vscode' => [ 'label' => 'VS Code', 'url' => 'vscode://file/%path:%line', ], 'vscode-insiders' => [ 'label' => 'VS Code Insiders', 'url' => 'vscode-insiders://file/%path:%line', ], 'vscode-remote' => [ 'label' => 'VS Code Remote', 'url' => 'vscode://vscode-remote/%path:%line', ], 'vscode-insiders-remote' => [ 'label' => 'VS Code Insiders Remote', 'url' => 'vscode-insiders://vscode-remote/%path:%line', ], 'vscodium' => [ 'label' => 'VS Codium', 'url' => 'vscodium://file/%path:%line', ], 'cursor' => [ 'label' => 'Cursor', 'url' => 'cursor://file/%path:%line', ], 'atom' => [ 'label' => 'Atom', 'url' => 'atom://core/open/file?filename=%path&line=%line', ], 'nova' => [ 'label' => 'Nova', 'url' => 'nova://open?path=%path&line=%line', ], 'netbeans' => [ 'label' => 'NetBeans', 'url' => 'netbeans://open/?f=%path:%line', ], 'xdebug' => [ 'label' => 'Xdebug', 'url' => 'xdebug://%path@%line', ], ], ]; } } Config/FileConfigManager.php 0000644 00000006677 15060250720 0011777 0 ustar 00 <?php namespace Spatie\Ignition\Config; use Spatie\Ignition\Contracts\ConfigManager; use Throwable; class FileConfigManager implements ConfigManager { private const SETTINGS_FILE_NAME = '.ignition.json'; private string $path; private string $file; public function __construct(string $path = '') { $this->path = $this->initPath($path); $this->file = $this->initFile(); } protected function initPath(string $path): string { $path = $this->retrievePath($path); if (! $this->isValidWritablePath($path)) { return ''; } return $this->preparePath($path); } protected function retrievePath(string $path): string { if ($path !== '') { return $path; } return $this->initPathFromEnvironment(); } protected function isValidWritablePath(string $path): bool { return @file_exists($path) && @is_writable($path); } protected function preparePath(string $path): string { return rtrim($path, DIRECTORY_SEPARATOR); } protected function initPathFromEnvironment(): string { if (! empty($_SERVER['HOMEDRIVE']) && ! empty($_SERVER['HOMEPATH'])) { return $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH']; } if (! empty(getenv('HOME'))) { return getenv('HOME'); } return ''; } protected function initFile(): string { return $this->path . DIRECTORY_SEPARATOR . self::SETTINGS_FILE_NAME; } /** {@inheritDoc} */ public function load(): array { return $this->readFromFile(); } /** @return array<string, mixed> */ protected function readFromFile(): array { if (! $this->isValidFile()) { return []; } $content = (string)file_get_contents($this->file); $settings = json_decode($content, true) ?? []; return $settings; } protected function isValidFile(): bool { return $this->isValidPath() && @file_exists($this->file) && @is_writable($this->file); } protected function isValidPath(): bool { return trim($this->path) !== ''; } /** {@inheritDoc} */ public function save(array $options): bool { if (! $this->createFile()) { return false; } return $this->saveToFile($options); } protected function createFile(): bool { if (! $this->isValidPath()) { return false; } if (@file_exists($this->file)) { return true; } return (file_put_contents($this->file, '') !== false); } /** * @param array<string, mixed> $options * * @return bool */ protected function saveToFile(array $options): bool { try { $content = json_encode($options, JSON_THROW_ON_ERROR); } catch (Throwable) { return false; } return $this->writeToFile($content); } protected function writeToFile(string $content): bool { if (! $this->isValidFile()) { return false; } return (file_put_contents($this->file, $content) !== false); } /** {@inheritDoc} */ public function getPersistentInfo(): array { return [ 'name' => self::SETTINGS_FILE_NAME, 'path' => $this->path, 'file' => $this->file, ]; } } Ignition.php 0000644 00000024652 15060250720 0007043 0 ustar 00 <?php namespace Spatie\Ignition; use ArrayObject; use ErrorException; use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable; use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract; use Spatie\ErrorSolutions\SolutionProviderRepository; use Spatie\ErrorSolutions\SolutionProviders\BadMethodCallSolutionProvider; use Spatie\ErrorSolutions\SolutionProviders\MergeConflictSolutionProvider; use Spatie\ErrorSolutions\SolutionProviders\UndefinedPropertySolutionProvider; use Spatie\FlareClient\Context\BaseContextProviderDetector; use Spatie\FlareClient\Context\ContextProviderDetector; use Spatie\FlareClient\Enums\MessageLevels; use Spatie\FlareClient\Flare; use Spatie\FlareClient\FlareMiddleware\AddDocumentationLinks; use Spatie\FlareClient\FlareMiddleware\AddSolutions; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\Ignition\ErrorPage\ErrorPageViewModel; use Spatie\Ignition\ErrorPage\Renderer; use Throwable; class Ignition { protected Flare $flare; protected bool $shouldDisplayException = true; protected string $flareApiKey = ''; protected string $applicationPath = ''; /** @var array<int, FlareMiddleware> */ protected array $middleware = []; protected IgnitionConfig $ignitionConfig; protected ContextProviderDetector $contextProviderDetector; protected SolutionProviderRepositoryContract $solutionProviderRepository; protected ?bool $inProductionEnvironment = null; protected ?string $solutionTransformerClass = null; /** @var ArrayObject<int, callable(Throwable): mixed> */ protected ArrayObject $documentationLinkResolvers; protected string $customHtmlHead = ''; protected string $customHtmlBody = ''; public static function make(): self { return new self(); } public function __construct( ?Flare $flare = null, ) { $this->flare = $flare ?? Flare::make(); $this->ignitionConfig = IgnitionConfig::loadFromConfigFile(); $this->solutionProviderRepository = new SolutionProviderRepository($this->getDefaultSolutionProviders()); $this->documentationLinkResolvers = new ArrayObject(); $this->contextProviderDetector = new BaseContextProviderDetector(); $this->middleware[] = new AddSolutions($this->solutionProviderRepository); $this->middleware[] = new AddDocumentationLinks($this->documentationLinkResolvers); } public function setSolutionTransformerClass(string $solutionTransformerClass): self { $this->solutionTransformerClass = $solutionTransformerClass; return $this; } /** @param callable(Throwable): mixed $callable */ public function resolveDocumentationLink(callable $callable): self { $this->documentationLinkResolvers[] = $callable; return $this; } public function setConfig(IgnitionConfig $ignitionConfig): self { $this->ignitionConfig = $ignitionConfig; return $this; } public function runningInProductionEnvironment(bool $boolean = true): self { $this->inProductionEnvironment = $boolean; return $this; } public function getFlare(): Flare { return $this->flare; } public function setFlare(Flare $flare): self { $this->flare = $flare; return $this; } public function setSolutionProviderRepository(SolutionProviderRepositoryContract $solutionProviderRepository): self { $this->solutionProviderRepository = $solutionProviderRepository; return $this; } public function shouldDisplayException(bool $shouldDisplayException): self { $this->shouldDisplayException = $shouldDisplayException; return $this; } public function applicationPath(string $applicationPath): self { $this->applicationPath = $applicationPath; return $this; } /** * @param string $name * @param string $messageLevel * @param array<int, mixed> $metaData * * @return $this */ public function glow( string $name, string $messageLevel = MessageLevels::INFO, array $metaData = [] ): self { $this->flare->glow($name, $messageLevel, $metaData); return $this; } /** * @param array<int, HasSolutionsForThrowable|class-string<HasSolutionsForThrowable>> $solutionProviders * * @return $this */ public function addSolutionProviders(array $solutionProviders): self { $this->solutionProviderRepository->registerSolutionProviders($solutionProviders); return $this; } /** @deprecated Use `setTheme('dark')` instead */ public function useDarkMode(): self { return $this->setTheme('dark'); } /** @deprecated Use `setTheme($theme)` instead */ public function theme(string $theme): self { return $this->setTheme($theme); } public function setTheme(string $theme): self { $this->ignitionConfig->setOption('theme', $theme); return $this; } public function setEditor(string $editor): self { $this->ignitionConfig->setOption('editor', $editor); return $this; } public function sendToFlare(?string $apiKey): self { $this->flareApiKey = $apiKey ?? ''; return $this; } public function configureFlare(callable $callable): self { ($callable)($this->flare); return $this; } /** * @param FlareMiddleware|array<int, FlareMiddleware> $middleware * * @return $this */ public function registerMiddleware(array|FlareMiddleware $middleware): self { if (! is_array($middleware)) { $middleware = [$middleware]; } foreach ($middleware as $singleMiddleware) { $this->middleware = array_merge($this->middleware, $middleware); } return $this; } public function setContextProviderDetector(ContextProviderDetector $contextProviderDetector): self { $this->contextProviderDetector = $contextProviderDetector; return $this; } public function reset(): self { $this->flare->reset(); return $this; } public function register(?int $errorLevels = null): self { error_reporting($errorLevels ?? -1); $errorLevels ? set_error_handler([$this, 'renderError'], $errorLevels) : set_error_handler([$this, 'renderError']); set_exception_handler([$this, 'handleException']); return $this; } /** * @param int $level * @param string $message * @param string $file * @param int $line * @param array<int, mixed> $context * * @return void * @throws \ErrorException */ public function renderError( int $level, string $message, string $file = '', int $line = 0, array $context = [] ): void { if(error_reporting() === (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE)) { // This happens when PHP version is >=8 and we caught an error that was suppressed with the "@" operator // See the first warning box in https://www.php.net/manual/en/language.operators.errorcontrol.php return; } throw new ErrorException($message, 0, $level, $file, $line); } /** * This is the main entry point for the framework agnostic Ignition package. * Displays the Ignition page and optionally sends a report to Flare. */ public function handleException(Throwable $throwable): Report { $this->setUpFlare(); $report = $this->createReport($throwable); if ($this->shouldDisplayException && $this->inProductionEnvironment !== true) { $this->renderException($throwable, $report); } if ($this->flare->apiTokenSet() && $this->inProductionEnvironment !== false) { $this->flare->report($throwable, report: $report); } return $report; } /** * This is the main entrypoint for laravel-ignition. It only renders the exception. * Sending the report to Flare is handled in the laravel-ignition log handler. */ public function renderException(Throwable $throwable, ?Report $report = null): void { $this->setUpFlare(); $report ??= $this->createReport($throwable); $viewModel = new ErrorPageViewModel( $throwable, $this->ignitionConfig, $report, $this->solutionProviderRepository->getSolutionsForThrowable($throwable), $this->solutionTransformerClass, $this->customHtmlHead, $this->customHtmlBody, ); (new Renderer())->render(['viewModel' => $viewModel], self::viewPath('errorPage')); } public static function viewPath(string $viewName): string { return __DIR__ . "/../resources/views/{$viewName}.php"; } /** * Add custom HTML which will be added to the head tag of the error page. */ public function addCustomHtmlToHead(string $html): self { $this->customHtmlHead .= $html; return $this; } /** * Add custom HTML which will be added to the body tag of the error page. */ public function addCustomHtmlToBody(string $html): self { $this->customHtmlBody .= $html; return $this; } protected function setUpFlare(): self { if (! $this->flare->apiTokenSet()) { $this->flare->setApiToken($this->flareApiKey ?? ''); } $this->flare->setContextProviderDetector($this->contextProviderDetector); foreach ($this->middleware as $singleMiddleware) { $this->flare->registerMiddleware($singleMiddleware); } if ($this->applicationPath !== '') { $this->flare->applicationPath($this->applicationPath); } return $this; } /** @return array<class-string<HasSolutionsForThrowable>> */ protected function getDefaultSolutionProviders(): array { return [ BadMethodCallSolutionProvider::class, MergeConflictSolutionProvider::class, UndefinedPropertySolutionProvider::class, ]; } protected function createReport(Throwable $throwable): Report { return $this->flare->createReport($throwable); } } ErrorPage/ErrorPageViewModel.php 0000644 00000006614 15060250720 0012651 0 ustar 00 <?php namespace Spatie\Ignition\ErrorPage; use Spatie\ErrorSolutions\Contracts\Solution; use Spatie\ErrorSolutions\Solutions\SolutionTransformer; use Spatie\FlareClient\Report; use Spatie\FlareClient\Truncation\ReportTrimmer; use Spatie\Ignition\Config\IgnitionConfig; use Throwable; class ErrorPageViewModel { /** * @param \Throwable|null $throwable * @param \Spatie\Ignition\Config\IgnitionConfig $ignitionConfig * @param \Spatie\FlareClient\Report $report * @param array<int, Solution> $solutions * @param string|null $solutionTransformerClass */ public function __construct( protected ?Throwable $throwable, protected IgnitionConfig $ignitionConfig, protected Report $report, protected array $solutions, protected ?string $solutionTransformerClass = null, protected string $customHtmlHead = '', protected string $customHtmlBody = '' ) { $this->solutionTransformerClass ??= SolutionTransformer::class; } public function throwableString(): string { if (! $this->throwable) { return ''; } $throwableString = sprintf( "%s: %s in file %s on line %d\n\n%s\n", get_class($this->throwable), $this->throwable->getMessage(), $this->throwable->getFile(), $this->throwable->getLine(), $this->report->getThrowable()?->getTraceAsString() ); return htmlspecialchars($throwableString); } public function title(): string { return htmlspecialchars($this->report->getMessage()); } /** * @return array<string, mixed> */ public function config(): array { return $this->ignitionConfig->toArray(); } public function theme(): string { return $this->config()['theme'] ?? 'auto'; } /** * @return array<int, mixed> */ public function solutions(): array { return array_map(function (Solution $solution) { /** @var class-string $transformerClass */ $transformerClass = $this->solutionTransformerClass; /** @var SolutionTransformer $transformer */ $transformer = new $transformerClass($solution); return ($transformer)->toArray(); }, $this->solutions); } /** * @return array<string, mixed> */ public function report(): array { return $this->report->toArray(); } public function jsonEncode(mixed $data): string { $jsonOptions = JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; return (string) json_encode($data, $jsonOptions); } public function getAssetContents(string $asset): string { $assetPath = __DIR__."/../../resources/compiled/{$asset}"; return (string) file_get_contents($assetPath); } /** * @return array<int|string, mixed> */ public function shareableReport(): array { return (new ReportTrimmer())->trim($this->report()); } public function updateConfigEndpoint(): string { // TODO: Should be based on Ignition config return '/_ignition/update-config'; } public function customHtmlHead(): string { return $this->customHtmlHead; } public function customHtmlBody(): string { return $this->customHtmlBody; } } ErrorPage/Renderer.php 0000644 00000000771 15060250720 0010713 0 ustar 00 <?php namespace Spatie\Ignition\ErrorPage; class Renderer { /** * @param array<string, mixed> $data * * @return void */ public function render(array $data, string $viewPath): void { $viewFile = $viewPath; extract($data, EXTR_OVERWRITE); include $viewFile; } public function renderAsString(array $date, string $viewPath): string { ob_start(); $this->render($date, $viewPath); return ob_get_clean(); } } Contracts/ConfigManager.php 0000644 00000000516 15060250720 0011714 0 ustar 00 <?php namespace Spatie\Ignition\Contracts; interface ConfigManager { /** @return array<string, mixed> */ public function load(): array; /** @param array<string, mixed> $options */ public function save(array $options): bool; /** @return array<string, mixed> */ public function getPersistentInfo(): array; }
| ver. 1.4 |
Github
|
.
| PHP 8.2.29 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0.01 |
proxy
|
phpinfo
|
ÐаÑтройка