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

@@ -0,0 +1,65 @@
<?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;
}
}

View File

@@ -0,0 +1,16 @@
<?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;
}

View File

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

View File

@@ -0,0 +1,16 @@
<?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;
}

View File

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

View File

@@ -0,0 +1,36 @@
<?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;
}

View File

@@ -0,0 +1,91 @@
<?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;
}
}

View File

@@ -0,0 +1,111 @@
<?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;
});
}
}

View File

@@ -0,0 +1,98 @@
<?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());
}
}

View File

@@ -0,0 +1,35 @@
<?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()];
}
}

View File

@@ -0,0 +1,63 @@
<?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",
],
],
];
}
}

View File

@@ -0,0 +1,67 @@
<?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') === '';
}
}

View File

@@ -0,0 +1,83 @@
<?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;
}
}

View File

@@ -0,0 +1,41 @@
<?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',
]),];
}
}

View File

@@ -0,0 +1,25 @@
<?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()];
}
}

View File

@@ -0,0 +1,35 @@
<?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')];
}
}

View File

@@ -0,0 +1,55 @@
<?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);
}
}
}

View File

@@ -0,0 +1,42 @@
<?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;
}
}

View File

@@ -0,0 +1,25 @@
<?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`?'),
];
}
}

View File

@@ -0,0 +1,62 @@
<?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 Spatie\ErrorSolutions\Support\Laravel\LaravelVersion;
use Throwable;
class MissingViteManifestSolutionProvider implements HasSolutionsForThrowable
{
/** @var array<string, string> */
protected array $links = [];
public function __construct()
{
$this->links = [
'Asset bundling with Vite' => 'https://laravel.com/docs/'.LaravelVersion::major().'.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);
}
}

View File

@@ -0,0 +1,39 @@
<?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('error-solutions.open_ai_key') === null) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
$solutionProvider = new BaseOpenAiSolutionProvider(
openAiKey: config('error-solutions.open_ai_key'),
cache: cache()->store(config('cache.default')),
cacheTtlInSeconds: 60,
applicationType: 'Laravel ' . Str::before(app()->version(), '.'),
applicationPath: base_path(),
openAiModel: config('error-solutions.open_ai_model', 'gpt-3.5-turbo'),
);
return $solutionProvider->getSolutions($throwable);
}
}

View File

@@ -0,0 +1,52 @@
<?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);
}
}

View File

@@ -0,0 +1,33 @@
<?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.'),
];
}
}

View File

@@ -0,0 +1,30 @@
<?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',
]),
];
}
}

View File

@@ -0,0 +1,35 @@
<?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')];
}
}

View File

@@ -0,0 +1,49 @@
<?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,
];
}
}

View File

@@ -0,0 +1,53 @@
<?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,
];
}
}

View File

@@ -0,0 +1,111 @@
<?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;
}
}

View File

@@ -0,0 +1,34 @@
<?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()];
}
}

View File

@@ -0,0 +1,34 @@
<?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()];
}
}

View File

@@ -0,0 +1,78 @@
<?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);
}
}

View File

@@ -0,0 +1,117 @@
<?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;
}
}

View File

@@ -0,0 +1,74 @@
<?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;
}
}

View File

@@ -0,0 +1,121 @@
<?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());
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Concerns;
trait IsProvidedByFlare
{
public function solutionProvidedByName(): string
{
return 'Flare';
}
public function solutionProvidedByLink(): string
{
return 'https://flareapp.io';
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Facades\Artisan;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class GenerateAppKeySolution implements RunnableSolution
{
use IsProvidedByFlare;
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');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Livewire\LivewireComponentsFinder;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class LivewireDiscoverSolution implements RunnableSolution
{
use IsProvidedByFlare;
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();
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class MakeViewVariableOptionalSolution implements RunnableSolution
{
use IsProvidedByFlare;
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;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Facades\Artisan;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class RunMigrationsSolution implements RunnableSolution
{
use IsProvidedByFlare;
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');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestLivewireMethodNameSolution implements Solution
{
use IsProvidedByFlare;
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;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestLivewirePropertyNameSolution implements Solution
{
use IsProvidedByFlare;
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;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestUsingCorrectDbNameSolution implements Solution
{
use IsProvidedByFlare;
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',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestUsingMariadbDatabaseSolution implements Solution
{
use IsProvidedByFlare;
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',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestUsingMysql8DatabaseSolution implements Solution
{
use IsProvidedByFlare;
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',
];
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\Laravel;
use Illuminate\Support\Str;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class UseDefaultValetDbCredentialsSolution implements RunnableSolution
{
use IsProvidedByFlare;
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.';
}
}

View File

@@ -0,0 +1,48 @@
<?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;
}
}

View File

@@ -0,0 +1,46 @@
<?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;
}
}

View File

@@ -0,0 +1,118 @@
<?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\Solutions\Concerns\IsProvidedByFlare;
use Spatie\ErrorSolutions\Support\AiPromptRenderer;
use Throwable;
class OpenAiSolution implements Solution
{
use IsProvidedByFlare;
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,
protected string|null $openAiModel = 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 $this->openAiModel ?? '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

@@ -0,0 +1,64 @@
<?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,
protected string $openAiModel = 'gpt-3.5-turbo',
) {
$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,
$this->openAiModel
),
];
}
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

@@ -0,0 +1,64 @@
<?php
namespace Spatie\ErrorSolutions\Solutions\OpenAi;
use Illuminate\Support\Str;
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
{
$rawText = Str::finish($this->rawText, 'ENDLINKS');
$textLinks = $this->between('LINKS', 'ENDLINKS', $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) {
if (isset($textLink['title']) && isset($textLink['url'])) {
$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

@@ -0,0 +1,30 @@
<?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,
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Spatie\ErrorSolutions\Solutions;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestCorrectVariableNameSolution implements Solution
{
use IsProvidedByFlare;
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

@@ -0,0 +1,33 @@
<?php
namespace Spatie\ErrorSolutions\Solutions;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\Solutions\Concerns\IsProvidedByFlare;
class SuggestImportSolution implements Solution
{
use IsProvidedByFlare;
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 [];
}
}

View File

@@ -0,0 +1,32 @@
<?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();
}
}

View File

@@ -0,0 +1,15 @@
<?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;
}

View File

@@ -0,0 +1,127 @@
<?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;
}
}

View File

@@ -0,0 +1,24 @@
<?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 [];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Spatie\ErrorSolutions\Support\Laravel;
class LaravelVersion
{
public static function major(): string
{
return explode('.', app()->version())[0];
}
}

View File

@@ -0,0 +1,87 @@
<?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();
}
}

View File

@@ -0,0 +1,65 @@
<?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();
}
}