Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\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\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Broadcasting\BroadcastException;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\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\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\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,79 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\Composer\ComposerClassMap;
use Spatie\LaravelIgnition\Support\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;
$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\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\LazyLoadingViolationException;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\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\LaravelIgnition\Solutions\SolutionProviders;
use RuntimeException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\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\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\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\LaravelIgnition\Solutions\SolutionProviders;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestImportSolution;
use Spatie\LaravelIgnition\Support\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\LaravelIgnition\Solutions\SolutionProviders;
use Livewire\Exceptions\ComponentNotFoundException;
use Livewire\LivewireComponentsFinder;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\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\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\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,55 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Contracts\Solution;
use Throwable;
class MissingViteManifestSolutionProvider implements HasSolutionsForThrowable
{
protected array $links = [
'Asset bundling with Vite' => 'https://laravel.com/docs/9.x/vite#running-vite',
];
public function canSolve(Throwable $throwable): bool
{
return Str::startsWith($throwable->getMessage(), 'Vite manifest not found');
}
public function getSolutions(Throwable $throwable): array
{
return [
$this->getSolution(),
];
}
public function getSolution(): Solution
{
/** @var string */
$baseCommand = collect([
'pnpm-lock.yaml' => 'pnpm',
'yarn.lock' => 'yarn',
])->first(fn ($_, $lockfile) => file_exists(base_path($lockfile)), 'npm run');
return app()->environment('local')
? $this->getLocalSolution($baseCommand)
: $this->getProductionSolution($baseCommand);
}
protected function getLocalSolution(string $baseCommand): Solution
{
return BaseSolution::create('Start the development server')
->setSolutionDescription("Run `{$baseCommand} dev` in your terminal and refresh the page.")
->setDocumentationLinks($this->links);
}
protected function getProductionSolution(string $baseCommand): Solution
{
return BaseSolution::create('Build the production assets')
->setSolutionDescription("Run `{$baseCommand} build` in your deployment script.")
->setDocumentationLinks($this->links);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Facades\Route;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\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] ?? null;
$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\LaravelIgnition\Solutions\SolutionProviders;
use Exception;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\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,96 @@
<?php
namespace Spatie\LaravelIgnition\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
{
protected Collection $solutionProviders;
/**
* @param array<int, ProvidesSolution> $solutionProviders
*/
public function __construct(array $solutionProviders = [])
{
$this->solutionProviders = Collection::make($solutionProviders);
}
public function registerSolutionProvider(string $solutionProviderClass): SolutionProviderRepositoryContract
{
$this->solutionProviders->push($solutionProviderClass);
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();
}
/** @phpstan-ignore-next-line */
$providedSolutions = $this->solutionProviders
->filter(function (string $solutionClass) {
if (! in_array(HasSolutionsForThrowable::class, class_implements($solutionClass) ?: [])) {
return false;
}
if (in_array($solutionClass, config('ignition.ignored_solution_providers', []))) {
return false;
}
return true;
})
->map(fn (string $solutionClass) => app($solutionClass))
->filter(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) {
try {
return $solutionProvider->canSolve($throwable);
} catch (Throwable $e) {
return false;
}
})
->map(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) {
try {
return $solutionProvider->getSolutions($throwable);
} catch (Throwable $e) {
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;
}
return app($solutionClass);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\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\LaravelIgnition\Solutions\SolutionProviders;
use Livewire\Exceptions\MethodNotFoundException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestLivewireMethodNameSolution;
use Spatie\LaravelIgnition\Support\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\LaravelIgnition\Solutions\SolutionProviders;
use Livewire\Exceptions\PropertyNotFoundException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestLivewirePropertyNameSolution;
use Spatie\LaravelIgnition\Support\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,110 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Contracts\Solution;
use Spatie\LaravelIgnition\Exceptions\ViewException;
use Spatie\LaravelIgnition\Solutions\MakeViewVariableOptionalSolution;
use Spatie\LaravelIgnition\Solutions\SuggestCorrectVariableNameSolution;
use Throwable;
class UndefinedViewVariableSolutionProvider implements HasSolutionsForThrowable
{
protected string $variableName;
protected string $viewFile;
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof ViewException) {
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 \Spatie\LaravelIgnition\Exceptions\ViewException $throwable
* @param string $variableName
* @param string $viewFile
*
* @return array<int, \Spatie\Ignition\Contracts\Solution>
*/
protected function findCorrectVariableSolutions(
ViewException $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,78 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use BadMethodCallException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\Validator;
use ReflectionClass;
use ReflectionMethod;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\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,116 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\View;
use InvalidArgumentException;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Exceptions\ViewException;
use Spatie\LaravelIgnition\Support\StringComparator;
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 ViewException) {
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;
}
}