This commit is contained in:
2025-05-12 14:25:25 +02:00
parent ab2db755ef
commit 9e378ca2b7
2719 changed files with 46505 additions and 60181 deletions

View File

@@ -56,6 +56,8 @@ For Symfony apps, go to [symfony-ignition-bundle](https://github.com/spatie/symf
For Drupal 10+ websites, use the [Ignition module](https://www.drupal.org/project/ignition).
For OpenMage websites, use the [Ignition module](https://github.com/empiricompany/openmage_ignition).
For all other PHP projects, install the package via composer:
```bash
@@ -87,7 +89,7 @@ By default, Ignition uses a nice white based theme. If this is too bright for yo
```php
\Spatie\Ignition\Ignition::make()
->useDarkMode()
->setTheme('dark')
->register();
```

View File

@@ -20,13 +20,13 @@
"php": "^8.0",
"ext-json": "*",
"ext-mbstring": "*",
"spatie/backtrace": "^1.5.3",
"spatie/flare-client-php": "^1.4.0",
"spatie/flare-client-php": "^1.7",
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0"
"symfony/var-dumper": "^5.4|^6.0|^7.0",
"spatie/error-solutions": "^1.0"
},
"require-dev" : {
"illuminate/cache" : "^9.52|^10.0|^11.0",
"illuminate/cache" : "^9.52|^10.0|^11.0|^12.0",
"mockery/mockery" : "^1.4",
"pestphp/pest" : "^1.20|^2.0",
"phpstan/extension-installer" : "^1.1",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -203,6 +203,10 @@ class IgnitionConfig implements Arrayable
'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',

View File

@@ -1,65 +0,0 @@
<?php
namespace Spatie\Ignition\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;
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Spatie\Ignition\Contracts;
use Throwable;
/**
* Interface used for SolutionProviders.
*/
interface HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool;
/** @return array<int, \Spatie\Ignition\Contracts\Solution> */
public function getSolutions(Throwable $throwable): array;
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Spatie\Ignition\Contracts;
/**
* Interface to be used on exceptions that provide their own solution.
*/
interface ProvidesSolution
{
public function getSolution(): Solution;
}

View File

@@ -1,16 +0,0 @@
<?php
namespace Spatie\Ignition\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;
}

View File

@@ -1,13 +0,0 @@
<?php
namespace Spatie\Ignition\Contracts;
interface Solution
{
public function getSolutionTitle(): string;
public function getSolutionDescription(): string;
/** @return array<string, string> */
public function getDocumentationLinks(): array;
}

View File

@@ -1,36 +0,0 @@
<?php
namespace Spatie\Ignition\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;
}

View File

@@ -2,11 +2,11 @@
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 Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Solutions\SolutionTransformer;
use Throwable;
class ErrorPageViewModel

View File

@@ -4,6 +4,12 @@ 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;
@@ -13,14 +19,8 @@ use Spatie\FlareClient\FlareMiddleware\AddSolutions;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
use Spatie\Ignition\Config\IgnitionConfig;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract;
use Spatie\Ignition\ErrorPage\ErrorPageViewModel;
use Spatie\Ignition\ErrorPage\Renderer;
use Spatie\Ignition\Solutions\SolutionProviders\BadMethodCallSolutionProvider;
use Spatie\Ignition\Solutions\SolutionProviders\MergeConflictSolutionProvider;
use Spatie\Ignition\Solutions\SolutionProviders\SolutionProviderRepository;
use Spatie\Ignition\Solutions\SolutionProviders\UndefinedPropertySolutionProvider;
use Throwable;
class Ignition
@@ -58,9 +58,10 @@ class Ignition
return new self();
}
public function __construct()
{
$this->flare = Flare::make();
public function __construct(
?Flare $flare = null,
) {
$this->flare = $flare ?? Flare::make();
$this->ignitionConfig = IgnitionConfig::loadFromConfigFile();
@@ -237,14 +238,14 @@ class Ignition
return $this;
}
public function register(): self
public function register(?int $errorLevels = null): self
{
error_reporting(-1);
error_reporting($errorLevels ?? -1);
/** @phpstan-ignore-next-line */
set_error_handler([$this, 'renderError']);
$errorLevels
? set_error_handler([$this, 'renderError'], $errorLevels)
: set_error_handler([$this, 'renderError']);
/** @phpstan-ignore-next-line */
set_exception_handler([$this, 'handleException']);
return $this;
@@ -267,6 +268,12 @@ class Ignition
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);
}

View File

@@ -1,48 +0,0 @@
<?php
namespace Spatie\Ignition\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;
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace Spatie\Ignition\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;
}
}

View File

@@ -1,115 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions\OpenAi;
use OpenAI;
use Psr\SimpleCache\CacheInterface;
use Spatie\Backtrace\Backtrace;
use Spatie\Backtrace\Frame;
use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\ErrorPage\Renderer;
use Spatie\Ignition\Ignition;
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 = Ignition::viewPath('aiPrompt');
$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 Renderer())->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;
}
}

View File

@@ -1,62 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions\OpenAi;
use Psr\SimpleCache\CacheInterface;
use Spatie\Ignition\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;
}
}

View File

@@ -1,58 +0,0 @@
<?php
namespace Spatie\Ignition\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));
}
}

View File

@@ -1,98 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions\SolutionProviders;
use BadMethodCallException;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionMethod;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\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());
}
}

View File

@@ -1,74 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use ParseError;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\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;
}
}

View File

@@ -1,101 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions\SolutionProviders;
use Illuminate\Support\Collection;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\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(fn (HasSolutionsForThrowable|string $provider) => in_array(HasSolutionsForThrowable::class, class_implements($provider) ?: []))
->map(function (string|HasSolutionsForThrowable $provider): HasSolutionsForThrowable {
if (is_string($provider)) {
return new $provider;
}
return $provider;
});
}
}

View File

@@ -1,121 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions\SolutionProviders;
use ErrorException;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionProperty;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\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());
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions;
use Illuminate\Contracts\Support\Arrayable;
use Spatie\Ignition\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,
];
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions;
use Spatie\Ignition\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;
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace Spatie\Ignition\Solutions;
use Spatie\Ignition\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 [];
}
}