├── phpstan.neon.dist ├── src ├── Support │ ├── Laravel │ │ ├── LaravelVersion.php │ │ ├── Composer │ │ │ ├── Composer.php │ │ │ ├── FakeComposer.php │ │ │ └── ComposerClassMap.php │ │ ├── StringComparator.php │ │ └── LivewireComponentParser.php │ └── AiPromptRenderer.php ├── Contracts │ ├── ProvidesSolution.php │ ├── Solution.php │ ├── HasSolutionsForThrowable.php │ ├── RunnableSolution.php │ ├── SolutionProviderRepository.php │ └── BaseSolution.php ├── Solutions │ ├── Concerns │ │ └── IsProvidedByFlare.php │ ├── SolutionTransformer.php │ ├── SuggestImportSolution.php │ ├── Laravel │ │ ├── SuggestLivewirePropertyNameSolution.php │ │ ├── SuggestLivewireMethodNameSolution.php │ │ ├── SuggestUsingCorrectDbNameSolution.php │ │ ├── SuggestUsingMariadbDatabaseSolution.php │ │ ├── SuggestUsingMysql8DatabaseSolution.php │ │ ├── GenerateAppKeySolution.php │ │ ├── RunMigrationsSolution.php │ │ ├── LivewireDiscoverSolution.php │ │ ├── UseDefaultValetDbCredentialsSolution.php │ │ └── MakeViewVariableOptionalSolution.php │ ├── OpenAi │ │ ├── OpenAiPromptViewModel.php │ │ ├── DummyCache.php │ │ ├── OpenAiSolutionProvider.php │ │ ├── OpenAiSolutionResponse.php │ │ └── OpenAiSolution.php │ └── SuggestCorrectVariableNameSolution.php ├── SolutionProviders │ ├── Laravel │ │ ├── MissingAppKeySolutionProvider.php │ │ ├── MissingMixManifestSolutionProvider.php │ │ ├── DefaultDbNameSolutionProvider.php │ │ ├── UnknownMysql8CollationSolutionProvider.php │ │ ├── UnknownMariadbCollationSolutionProvider.php │ │ ├── TableNotFoundSolutionProvider.php │ │ ├── MissingColumnSolutionProvider.php │ │ ├── SailNetworkSolutionProvider.php │ │ ├── RunningLaravelDuskInProductionProvider.php │ │ ├── MissingLivewireComponentSolutionProvider.php │ │ ├── OpenAiSolutionProvider.php │ │ ├── LazyLoadingViolationSolutionProvider.php │ │ ├── MissingImportSolutionProvider.php │ │ ├── UndefinedLivewireMethodSolutionProvider.php │ │ ├── RouteNotDefinedSolutionProvider.php │ │ ├── UndefinedLivewirePropertySolutionProvider.php │ │ ├── IncorrectValetDbCredentialsSolutionProvider.php │ │ ├── GenericLaravelExceptionSolutionProvider.php │ │ ├── MissingViteManifestSolutionProvider.php │ │ ├── UnknownValidationSolutionProvider.php │ │ ├── InvalidRouteActionSolutionProvider.php │ │ ├── UndefinedViewVariableSolutionProvider.php │ │ └── ViewNotFoundSolutionProvider.php │ ├── MergeConflictSolutionProvider.php │ ├── BadMethodCallSolutionProvider.php │ └── UndefinedPropertySolutionProvider.php ├── DiscoverSolutionProviders.php └── SolutionProviderRepository.php ├── LICENSE.md ├── resources └── views │ └── aiPrompt.php ├── .php_cs.php ├── composer.json ├── README.md ├── CHANGELOG.md └── phpstan-baseline.neon /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | 5 | parameters: 6 | level: 8 7 | paths: 8 | - src 9 | tmpDir: build/phpstan 10 | 11 | -------------------------------------------------------------------------------- /src/Support/Laravel/LaravelVersion.php: -------------------------------------------------------------------------------- 1 | version())[0]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Contracts/ProvidesSolution.php: -------------------------------------------------------------------------------- 1 | */ 12 | public function getDocumentationLinks(): array; 13 | } 14 | -------------------------------------------------------------------------------- /src/Solutions/Concerns/IsProvidedByFlare.php: -------------------------------------------------------------------------------- 1 | */ 15 | public function getSolutions(Throwable $throwable): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Support/Laravel/Composer/Composer.php: -------------------------------------------------------------------------------- 1 | */ 8 | public function getClassMap(): array; 9 | 10 | /** @return array */ 11 | public function getPrefixes(): array; 12 | 13 | /** @return array */ 14 | public function getPrefixesPsr4(): array; 15 | } 16 | -------------------------------------------------------------------------------- /src/Contracts/RunnableSolution.php: -------------------------------------------------------------------------------- 1 | $parameters */ 12 | public function run(array $parameters = []): void; 13 | 14 | /** @return array */ 15 | public function getRunParameters(): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Support/Laravel/Composer/FakeComposer.php: -------------------------------------------------------------------------------- 1 | */ 8 | public function getClassMap(): array 9 | { 10 | return []; 11 | } 12 | 13 | /** @return array */ 14 | public function getPrefixes(): array 15 | { 16 | return []; 17 | } 18 | 19 | /** @return array */ 20 | public function getPrefixesPsr4(): array 21 | { 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Solutions/SolutionTransformer.php: -------------------------------------------------------------------------------- 1 | |string|false> */ 10 | public function toArray(Solution $solution): array 11 | { 12 | return [ 13 | 'class' => get_class($solution), 14 | 'title' => $solution->getSolutionTitle(), 15 | 'links' => $solution->getDocumentationLinks(), 16 | 'description' => $solution->getSolutionDescription(), 17 | 'is_runnable' => false, 18 | 'ai_generated' => $solution->aiGenerated ?? false, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/MissingAppKeySolutionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage() === 'No application encryption key has been specified.'; 19 | } 20 | 21 | public function getSolutions(Throwable $throwable): array 22 | { 23 | return [new GenerateAppKeySolution()]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/MissingMixManifestSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage(), 'Mix manifest not found'); 16 | } 17 | 18 | public function getSolutions(Throwable $throwable): array 19 | { 20 | return [ 21 | BaseSolution::create('Missing Mix Manifest File') 22 | ->setSolutionDescription('Did you forget to run `npm install && npm run dev`?'), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Solutions/SuggestImportSolution.php: -------------------------------------------------------------------------------- 1 | class = $class; 17 | } 18 | 19 | public function getSolutionTitle(): string 20 | { 21 | return 'A class import is missing'; 22 | } 23 | 24 | public function getSolutionDescription(): string 25 | { 26 | return 'You have a missing class import. Try importing this class: `'.$this->class.'`.'; 27 | } 28 | 29 | public function getDocumentationLinks(): array 30 | { 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Support/AiPromptRenderer.php: -------------------------------------------------------------------------------- 1 | $data 9 | * 10 | * @return void 11 | */ 12 | public function render(array $data, string $viewPath): void 13 | { 14 | $viewFile = $viewPath; 15 | 16 | extract($data, EXTR_OVERWRITE); 17 | 18 | include $viewFile; 19 | } 20 | 21 | /** 22 | * @param array $data 23 | */ 24 | public function renderAsString(array $data, string $viewPath): string 25 | { 26 | ob_start(); 27 | 28 | $this->render($data, $viewPath); 29 | 30 | $rendered = ob_get_clean(); 31 | 32 | if ($rendered === false) { 33 | throw new \RuntimeException('Failed to get the output buffer content.'); 34 | } 35 | 36 | return $rendered; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/SuggestLivewirePropertyNameSolution.php: -------------------------------------------------------------------------------- 1 | variableName}"; 22 | } 23 | 24 | public function getDocumentationLinks(): array 25 | { 26 | return []; 27 | } 28 | 29 | public function getSolutionDescription(): string 30 | { 31 | return "Did you mean `$this->suggested`?"; 32 | } 33 | 34 | public function isRunnable(): bool 35 | { 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/SuggestLivewireMethodNameSolution.php: -------------------------------------------------------------------------------- 1 | componentClass}::{$this->methodName}`"; 22 | } 23 | 24 | public function getDocumentationLinks(): array 25 | { 26 | return []; 27 | } 28 | 29 | public function getSolutionDescription(): string 30 | { 31 | return "Did you mean `{$this->componentClass}::{$this->suggested}`?"; 32 | } 33 | 34 | public function isRunnable(): bool 35 | { 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Contracts/SolutionProviderRepository.php: -------------------------------------------------------------------------------- 1 | |HasSolutionsForThrowable $solutionProvider 11 | * 12 | * @return $this 13 | */ 14 | public function registerSolutionProvider(string $solutionProvider): self; 15 | 16 | /** 17 | * @param array|HasSolutionsForThrowable> $solutionProviders 18 | * 19 | * @return $this 20 | */ 21 | public function registerSolutionProviders(array $solutionProviders): self; 22 | 23 | /** 24 | * @param Throwable $throwable 25 | * 26 | * @return array 27 | */ 28 | public function getSolutionsForThrowable(Throwable $throwable): array; 29 | 30 | /** 31 | * @param class-string $solutionClass 32 | * 33 | * @return null|Solution 34 | */ 35 | public function getSolutionForClass(string $solutionClass): ?Solution; 36 | } 37 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/SuggestUsingCorrectDbNameSolution.php: -------------------------------------------------------------------------------- 1 | */ 25 | public function getDocumentationLinks(): array 26 | { 27 | return [ 28 | 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/SuggestUsingMariadbDatabaseSolution.php: -------------------------------------------------------------------------------- 1 | */ 23 | public function getDocumentationLinks(): array 24 | { 25 | return [ 26 | 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/DefaultDbNameSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getCode() !== self::MYSQL_UNKNOWN_DATABASE_CODE) { 21 | return false; 22 | } 23 | 24 | if (! in_array(env('DB_DATABASE'), ['homestead', 'laravel'])) { 25 | return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | public function getSolutions(Throwable $throwable): array 32 | { 33 | return [new SuggestUsingCorrectDbNameSolution()]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/SuggestUsingMysql8DatabaseSolution.php: -------------------------------------------------------------------------------- 1 | */ 23 | public function getDocumentationLinks(): array 24 | { 25 | return [ 26 | 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/UnknownMysql8CollationSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getCode() !== self::MYSQL_UNKNOWN_COLLATION_CODE) { 21 | return false; 22 | } 23 | 24 | return str_contains( 25 | $throwable->getMessage(), 26 | 'Unknown collation: \'utf8mb4_0900_ai_ci\'' 27 | ); 28 | } 29 | 30 | public function getSolutions(Throwable $throwable): array 31 | { 32 | return [new SuggestUsingMysql8DatabaseSolution()]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Solutions/OpenAi/OpenAiPromptViewModel.php: -------------------------------------------------------------------------------- 1 | file; 20 | } 21 | 22 | public function line(): string 23 | { 24 | return $this->line; 25 | } 26 | 27 | public function snippet(): string 28 | { 29 | return $this->snippet; 30 | } 31 | 32 | public function exceptionMessage(): string 33 | { 34 | return $this->exceptionMessage; 35 | } 36 | 37 | public function exceptionClass(): string 38 | { 39 | return $this->exceptionClass; 40 | } 41 | 42 | public function applicationType(): string|null 43 | { 44 | return $this->applicationType; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/UnknownMariadbCollationSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getCode() !== self::MYSQL_UNKNOWN_COLLATION_CODE) { 21 | return false; 22 | } 23 | 24 | return str_contains( 25 | $throwable->getMessage(), 26 | 'Unknown collation: \'utf8mb4_uca1400_ai_ci\'' 27 | ); 28 | } 29 | 30 | public function getSolutions(Throwable $throwable): array 31 | { 32 | return [new SuggestUsingMariadbDatabaseSolution()]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Solutions/OpenAi/DummyCache.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | You are a very skilled PHP programmer. 4 | 5 | applicationType()) { ?> 6 | You are working on a applicationType() ?> application. 7 | 8 | 9 | Use the following context to find a possible fix for the exception message at the end. Limit your answer to 4 or 5 sentences. Also include a few links to documentation that might help. 10 | 11 | Use this format in your answer, make sure links are json: 12 | 13 | FIX 14 | insert the possible fix here 15 | ENDFIX 16 | LINKS 17 | {"title": "Title link 1", "url": "URL link 1"} 18 | {"title": "Title link 2", "url": "URL link 2"} 19 | ENDLINKS 20 | --- 21 | 22 | Here comes the context and the exception message: 23 | 24 | Line: line() ?> 25 | 26 | File: 27 | file() ?> 28 | 29 | Snippet including line numbers: 30 | snippet() ?> 31 | 32 | Exception class: 33 | exceptionClass() ?> 34 | 35 | Exception message: 36 | exceptionMessage() ?> 37 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/TableNotFoundSolutionProvider.php: -------------------------------------------------------------------------------- 1 | isBadTableErrorCode($throwable->getCode()); 24 | } 25 | 26 | protected function isBadTableErrorCode(string $code): bool 27 | { 28 | return $code === static::MYSQL_BAD_TABLE_CODE; 29 | } 30 | 31 | public function getSolutions(Throwable $throwable): array 32 | { 33 | return [new RunMigrationsSolution('A table was not found')]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/MissingColumnSolutionProvider.php: -------------------------------------------------------------------------------- 1 | isBadTableErrorCode($throwable->getCode()); 24 | } 25 | 26 | protected function isBadTableErrorCode(string $code): bool 27 | { 28 | return $code === static::MYSQL_BAD_FIELD_CODE; 29 | } 30 | 31 | public function getSolutions(Throwable $throwable): array 32 | { 33 | return [new RunMigrationsSolution('A column was not found')]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/SailNetworkSolutionProvider.php: -------------------------------------------------------------------------------- 1 | runningInConsole() 14 | && str_contains($throwable->getMessage(), 'php_network_getaddresses') 15 | && file_exists(base_path('vendor/bin/sail')) 16 | && file_exists(base_path('docker-compose.yml')) 17 | && env('LARAVEL_SAIL') === null; 18 | } 19 | 20 | public function getSolutions(Throwable $throwable): array 21 | { 22 | return [ 23 | BaseSolution::create('Network address not found') 24 | ->setSolutionDescription('Did you mean to use `sail artisan`?') 25 | ->setDocumentationLinks([ 26 | 'Sail: Executing Artisan Commands' => 'https://laravel.com/docs/sail#executing-artisan-commands', 27 | ]), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Solutions/SuggestCorrectVariableNameSolution.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 21 | 22 | $this->viewFile = $viewFile; 23 | 24 | $this->suggested = $suggested; 25 | } 26 | 27 | public function getSolutionTitle(): string 28 | { 29 | return 'Possible typo $'.$this->variableName; 30 | } 31 | 32 | public function getDocumentationLinks(): array 33 | { 34 | return []; 35 | } 36 | 37 | public function getSolutionDescription(): string 38 | { 39 | return "Did you mean `$$this->suggested`?"; 40 | } 41 | 42 | public function isRunnable(): bool 43 | { 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.php_cs.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | 'keep_multiple_spaces_after_comma' => true, 32 | ], 33 | 'single_trait_insert_per_statement' => true, 34 | ]) 35 | ->setFinder($finder); 36 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/RunningLaravelDuskInProductionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage() === 'It is unsafe to run Dusk in production.'; 19 | } 20 | 21 | public function getSolutions(Throwable $throwable): array 22 | { 23 | return [ 24 | BaseSolution::create() 25 | ->setSolutionTitle('Laravel Dusk should not be run in production.') 26 | ->setSolutionDescription('Install the dependencies with the `--no-dev` flag.'), 27 | 28 | BaseSolution::create() 29 | ->setSolutionTitle('Laravel Dusk can be run in other environments.') 30 | ->setSolutionDescription('Consider setting the `APP_ENV` to something other than `production` like `local` for example.'), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/MissingLivewireComponentSolutionProvider.php: -------------------------------------------------------------------------------- 1 | livewireIsInstalled()) { 16 | return false; 17 | } 18 | 19 | if (! $throwable instanceof ComponentNotFoundException) { 20 | return false; 21 | } 22 | 23 | return true; 24 | } 25 | 26 | public function getSolutions(Throwable $throwable): array 27 | { 28 | return [new LivewireDiscoverSolution('A Livewire component was not found')]; 29 | } 30 | 31 | public function livewireIsInstalled(): bool 32 | { 33 | if (! class_exists(ComponentNotFoundException::class)) { 34 | return false; 35 | } 36 | if (! class_exists(LivewireComponentsFinder::class)) { 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/OpenAiSolutionProvider.php: -------------------------------------------------------------------------------- 1 | store(config('cache.default')), 31 | cacheTtlInSeconds: 60, 32 | applicationType: 'Laravel ' . Str::before(app()->version(), '.'), 33 | applicationPath: base_path(), 34 | openAiModel: config('error-solutions.open_ai_model', 'gpt-3.5-turbo'), 35 | ); 36 | 37 | return $solutionProvider->getSolutions($throwable); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/GenerateAppKeySolution.php: -------------------------------------------------------------------------------- 1 | 'https://laravel.com/docs/master/installation#configuration', 22 | ]; 23 | } 24 | 25 | public function getSolutionActionDescription(): string 26 | { 27 | return 'Generate your application encryption key using `php artisan key:generate`.'; 28 | } 29 | 30 | public function getRunButtonText(): string 31 | { 32 | return 'Generate app key'; 33 | } 34 | 35 | public function getSolutionDescription(): string 36 | { 37 | return $this->getSolutionActionDescription(); 38 | } 39 | 40 | public function getRunParameters(): array 41 | { 42 | return []; 43 | } 44 | 45 | public function run(array $parameters = []): void 46 | { 47 | Artisan::call('key:generate'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/RunMigrationsSolution.php: -------------------------------------------------------------------------------- 1 | customTitle = $customTitle; 18 | } 19 | 20 | public function getSolutionTitle(): string 21 | { 22 | return $this->customTitle; 23 | } 24 | 25 | public function getSolutionDescription(): string 26 | { 27 | return 'You might have forgotten to run your database migrations.'; 28 | } 29 | 30 | public function getDocumentationLinks(): array 31 | { 32 | return [ 33 | 'Database: Running Migrations docs' => 'https://laravel.com/docs/master/migrations#running-migrations', 34 | ]; 35 | } 36 | 37 | public function getRunParameters(): array 38 | { 39 | return []; 40 | } 41 | 42 | public function getSolutionActionDescription(): string 43 | { 44 | return 'You can try to run your migrations using `php artisan migrate`.'; 45 | } 46 | 47 | public function getRunButtonText(): string 48 | { 49 | return 'Run migrations'; 50 | } 51 | 52 | public function run(array $parameters = []): void 53 | { 54 | Artisan::call('migrate'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/LazyLoadingViolationSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getPrevious()) { 20 | return false; 21 | } 22 | 23 | return $previous instanceof LazyLoadingViolationException; 24 | } 25 | 26 | public function getSolutions(Throwable $throwable): array 27 | { 28 | $majorVersion = LaravelVersion::major(); 29 | 30 | return [BaseSolution::create( 31 | 'Lazy loading was disabled to detect N+1 problems' 32 | ) 33 | ->setSolutionDescription( 34 | 'Either avoid lazy loading the relation or allow lazy loading.' 35 | ) 36 | ->setDocumentationLinks([ 37 | 'Read the docs on preventing lazy loading' => "https://laravel.com/docs/{$majorVersion}.x/eloquent-relationships#preventing-lazy-loading", 38 | 'Watch a video on how to deal with the N+1 problem' => 'https://www.youtube.com/watch?v=ZE7KBeraVpc', 39 | ]),]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Contracts/BaseSolution.php: -------------------------------------------------------------------------------- 1 | */ 12 | protected array $links = []; 13 | 14 | public static function create(string $title = ''): static 15 | { 16 | // It's important to keep the return type as static because 17 | // the old Facade Ignition contracts extend from this method. 18 | 19 | /** @phpstan-ignore-next-line */ 20 | return new static($title); 21 | } 22 | 23 | public function __construct(string $title = '') 24 | { 25 | $this->title = $title; 26 | } 27 | 28 | public function getSolutionTitle(): string 29 | { 30 | return $this->title; 31 | } 32 | 33 | public function setSolutionTitle(string $title): self 34 | { 35 | $this->title = $title; 36 | 37 | return $this; 38 | } 39 | 40 | public function getSolutionDescription(): string 41 | { 42 | return $this->description; 43 | } 44 | 45 | public function setSolutionDescription(string $description): self 46 | { 47 | $this->description = $description; 48 | 49 | return $this; 50 | } 51 | 52 | /** @return array */ 53 | public function getDocumentationLinks(): array 54 | { 55 | return $this->links; 56 | } 57 | 58 | /** @param array $links */ 59 | public function setDocumentationLinks(array $links): self 60 | { 61 | $this->links = $links; 62 | 63 | return $this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/LivewireDiscoverSolution.php: -------------------------------------------------------------------------------- 1 | customTitle = $customTitle; 18 | } 19 | 20 | public function getSolutionTitle(): string 21 | { 22 | return $this->customTitle; 23 | } 24 | 25 | public function getSolutionDescription(): string 26 | { 27 | return 'You might have forgotten to discover your Livewire components.'; 28 | } 29 | 30 | public function getDocumentationLinks(): array 31 | { 32 | return [ 33 | 'Livewire: Artisan Commands' => 'https://laravel-livewire.com/docs/2.x/artisan-commands', 34 | ]; 35 | } 36 | 37 | public function getRunParameters(): array 38 | { 39 | return []; 40 | } 41 | 42 | public function getSolutionActionDescription(): string 43 | { 44 | return 'You can discover your Livewire components using `php artisan livewire:discover`.'; 45 | } 46 | 47 | public function getRunButtonText(): string 48 | { 49 | return 'Run livewire:discover'; 50 | } 51 | 52 | public function run(array $parameters = []): void 53 | { 54 | if (app()->has(LivewireComponentsFinder::class)) { 55 | app(LivewireComponentsFinder::class)->build(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/MissingImportSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage(), $matches)) { 21 | return false; 22 | } 23 | 24 | $class = $matches[1]; 25 | 26 | $this->composerClassMap = new ComposerClassMap(); 27 | 28 | $this->search($class); 29 | 30 | return ! is_null($this->foundClass); 31 | } 32 | 33 | /** 34 | * @param \Throwable $throwable 35 | * 36 | * @return array 37 | */ 38 | public function getSolutions(Throwable $throwable): array 39 | { 40 | if (is_null($this->foundClass)) { 41 | return []; 42 | } 43 | 44 | return [new SuggestImportSolution($this->foundClass)]; 45 | } 46 | 47 | protected function search(string $missingClass): void 48 | { 49 | $this->foundClass = $this->composerClassMap->searchClassMap($missingClass); 50 | 51 | if (is_null($this->foundClass)) { 52 | $this->foundClass = $this->composerClassMap->searchPsrMaps($missingClass); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Support/Laravel/StringComparator.php: -------------------------------------------------------------------------------- 1 | $strings 11 | * @param string $input 12 | * @param int $sensitivity 13 | * 14 | * @return string|null 15 | */ 16 | public static function findClosestMatch(array $strings, string $input, int $sensitivity = 4): ?string 17 | { 18 | $closestDistance = -1; 19 | 20 | $closestMatch = null; 21 | 22 | foreach ($strings as $string) { 23 | $levenshteinDistance = levenshtein($input, $string); 24 | 25 | if ($levenshteinDistance === 0) { 26 | $closestMatch = $string; 27 | $closestDistance = 0; 28 | 29 | break; 30 | } 31 | 32 | if ($levenshteinDistance <= $closestDistance || $closestDistance < 0) { 33 | $closestMatch = $string; 34 | $closestDistance = $levenshteinDistance; 35 | } 36 | } 37 | 38 | if ($closestDistance <= $sensitivity) { 39 | return $closestMatch; 40 | } 41 | 42 | return null; 43 | } 44 | 45 | /** 46 | * @param array $strings 47 | * @param string $input 48 | * 49 | * @return string|null 50 | */ 51 | public static function findSimilarText(array $strings, string $input): ?string 52 | { 53 | if (empty($strings)) { 54 | return null; 55 | } 56 | 57 | return Collection::make($strings) 58 | ->sortByDesc(function (string $string) use ($input) { 59 | similar_text($input, $string, $percentage); 60 | 61 | return $percentage; 62 | }) 63 | ->first(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/UndefinedLivewireMethodSolutionProvider.php: -------------------------------------------------------------------------------- 1 | $methodName, 'component' => $component] = $this->getMethodAndComponent($throwable); 21 | 22 | if ($methodName === null || $component === null) { 23 | return []; 24 | } 25 | 26 | $parsed = LivewireComponentParser::create($component); 27 | 28 | return $parsed->getMethodNamesLike($methodName) 29 | ->map(function (string $suggested) use ($parsed, $methodName) { 30 | return new SuggestLivewireMethodNameSolution( 31 | $methodName, 32 | $parsed->getComponentClass(), 33 | $suggested 34 | ); 35 | }) 36 | ->toArray(); 37 | } 38 | 39 | /** @return array */ 40 | protected function getMethodAndComponent(Throwable $throwable): array 41 | { 42 | preg_match_all('/\[([\d\w\-_]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER); 43 | 44 | return [ 45 | 'methodName' => $matches[0][1] ?? null, 46 | 'component' => $matches[1][1] ?? null, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/RouteNotDefinedSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage(), $matches); 23 | } 24 | 25 | public function getSolutions(Throwable $throwable): array 26 | { 27 | preg_match(self::REGEX, $throwable->getMessage(), $matches); 28 | 29 | $missingRoute = $matches[1] ?? ''; 30 | 31 | $suggestedRoute = $this->findRelatedRoute($missingRoute); 32 | 33 | if ($suggestedRoute) { 34 | return [ 35 | BaseSolution::create("{$missingRoute} was not defined.") 36 | ->setSolutionDescription("Did you mean `{$suggestedRoute}`?"), 37 | ]; 38 | } 39 | 40 | return [ 41 | BaseSolution::create("{$missingRoute} was not defined.") 42 | ->setSolutionDescription('Are you sure that the route is defined'), 43 | ]; 44 | } 45 | 46 | protected function findRelatedRoute(string $missingRoute): ?string 47 | { 48 | Route::getRoutes()->refreshNameLookups(); 49 | 50 | return StringComparator::findClosestMatch(array_keys(Route::getRoutes()->getRoutesByName()), $missingRoute); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/UndefinedLivewirePropertySolutionProvider.php: -------------------------------------------------------------------------------- 1 | $variable, 'component' => $component] = $this->getMethodAndComponent($throwable); 21 | 22 | if ($variable === null || $component === null) { 23 | return []; 24 | } 25 | 26 | $parsed = LivewireComponentParser::create($component); 27 | 28 | return $parsed->getPropertyNamesLike($variable) 29 | ->map(function (string $suggested) use ($parsed, $variable) { 30 | return new SuggestLivewirePropertyNameSolution( 31 | $variable, 32 | $parsed->getComponentClass(), 33 | '$'.$suggested 34 | ); 35 | }) 36 | ->toArray(); 37 | } 38 | 39 | /** 40 | * @param \Throwable $throwable 41 | * 42 | * @return array 43 | */ 44 | protected function getMethodAndComponent(Throwable $throwable): array 45 | { 46 | preg_match_all('/\[([\d\w\-_\$]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER, 0); 47 | 48 | return [ 49 | 'variable' => $matches[0][1] ?? null, 50 | 'component' => $matches[1][1] ?? null, 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/IncorrectValetDbCredentialsSolutionProvider.php: -------------------------------------------------------------------------------- 1 | isAccessDeniedCode((int) $throwable->getCode())) { 25 | return false; 26 | } 27 | 28 | if (! $this->envFileExists()) { 29 | return false; 30 | } 31 | 32 | if (! $this->isValetInstalled()) { 33 | return false; 34 | } 35 | 36 | if ($this->usingCorrectDefaultCredentials()) { 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | public function getSolutions(Throwable $throwable): array 44 | { 45 | return [new UseDefaultValetDbCredentialsSolution()]; 46 | } 47 | 48 | protected function envFileExists(): bool 49 | { 50 | return file_exists(base_path('.env')); 51 | } 52 | 53 | protected function isAccessDeniedCode(int $code): bool 54 | { 55 | return $code === static::MYSQL_ACCESS_DENIED_CODE; 56 | } 57 | 58 | protected function isValetInstalled(): bool 59 | { 60 | return file_exists('/usr/local/bin/valet'); 61 | } 62 | 63 | protected function usingCorrectDefaultCredentials(): bool 64 | { 65 | return env('DB_USERNAME') === 'root' && env('DB_PASSWORD') === ''; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Solutions/OpenAi/OpenAiSolutionProvider.php: -------------------------------------------------------------------------------- 1 | cache ??= new DummyCache(); 20 | } 21 | 22 | public function canSolve(Throwable $throwable): bool 23 | { 24 | return true; 25 | } 26 | 27 | public function getSolutions(Throwable $throwable): array 28 | { 29 | return [ 30 | new OpenAiSolution( 31 | $throwable, 32 | $this->openAiKey, 33 | $this->cache, 34 | $this->cacheTtlInSeconds, 35 | $this->applicationType, 36 | $this->applicationPath, 37 | $this->openAiModel 38 | ), 39 | ]; 40 | } 41 | 42 | public function applicationType(string $applicationType): self 43 | { 44 | $this->applicationType = $applicationType; 45 | 46 | return $this; 47 | } 48 | 49 | public function applicationPath(string $applicationPath): self 50 | { 51 | $this->applicationPath = $applicationPath; 52 | 53 | return $this; 54 | } 55 | 56 | public function useCache(CacheInterface $cache, int $cacheTtlInSeconds = 60 * 60): self 57 | { 58 | $this->cache = $cache; 59 | 60 | $this->cacheTtlInSeconds = $cacheTtlInSeconds; 61 | 62 | return $this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/UseDefaultValetDbCredentialsSolution.php: -------------------------------------------------------------------------------- 1 | ensureLineExists('DB_USERNAME', 'root'); 35 | $this->ensureLineExists('DB_PASSWORD', ''); 36 | } 37 | 38 | protected function ensureLineExists(string $key, string $value): void 39 | { 40 | $envPath = base_path('.env'); 41 | 42 | $envLines = array_map(fn (string $envLine) => Str::startsWith($envLine, $key) 43 | ? "{$key}={$value}".PHP_EOL 44 | : $envLine, file($envPath) ?: []); 45 | 46 | file_put_contents($envPath, implode('', $envLines)); 47 | } 48 | 49 | public function getRunParameters(): array 50 | { 51 | return []; 52 | } 53 | 54 | public function getDocumentationLinks(): array 55 | { 56 | return [ 57 | 'Valet documentation' => 'https://laravel.com/docs/master/valet', 58 | ]; 59 | } 60 | 61 | public function getSolutionDescription(): string 62 | { 63 | return 'You seem to be using Valet, but the .env file does not contain the right default database credentials.'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/GenericLaravelExceptionSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getSolutionTexts($throwable)); 16 | } 17 | 18 | public function getSolutions(Throwable $throwable): array 19 | { 20 | if (! $texts = $this->getSolutionTexts($throwable)) { 21 | return []; 22 | } 23 | 24 | $solution = BaseSolution::create($texts['title']) 25 | ->setSolutionDescription($texts['description']) 26 | ->setDocumentationLinks($texts['links']); 27 | 28 | return ([$solution]); 29 | } 30 | 31 | /** 32 | * @param \Throwable $throwable 33 | * 34 | * @return array|null 35 | */ 36 | protected function getSolutionTexts(Throwable $throwable) : ?array 37 | { 38 | foreach ($this->getSupportedExceptions() as $supportedClass => $texts) { 39 | if ($throwable instanceof $supportedClass) { 40 | return $texts; 41 | } 42 | } 43 | 44 | return null; 45 | } 46 | 47 | /** @return array */ 48 | protected function getSupportedExceptions(): array 49 | { 50 | $majorVersion = LaravelVersion::major(); 51 | 52 | return 53 | [ 54 | BroadcastException::class => [ 55 | 'title' => 'Here are some links that might help solve this problem', 56 | 'description' => '', 57 | 'links' => [ 58 | 'Laravel docs on authentication' => "https://laravel.com/docs/{$majorVersion}.x/authentication", 59 | ], 60 | ], 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/SolutionProviders/MergeConflictSolutionProvider.php: -------------------------------------------------------------------------------- 1 | hasMergeConflictExceptionMessage($throwable)) { 19 | return false; 20 | } 21 | 22 | $file = (string)file_get_contents($throwable->getFile()); 23 | 24 | if (! str_contains($file, '=======')) { 25 | return false; 26 | } 27 | if (! str_contains($file, '>>>>>>>')) { 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | public function getSolutions(Throwable $throwable): array 35 | { 36 | $file = (string)file_get_contents($throwable->getFile()); 37 | preg_match('/\>\>\>\>\>\>\> (.*?)\n/', $file, $matches); 38 | $source = $matches[1]; 39 | 40 | $target = $this->getCurrentBranch(basename($throwable->getFile())); 41 | 42 | return [ 43 | BaseSolution::create("Merge conflict from branch '$source' into $target") 44 | ->setSolutionDescription('You have a Git merge conflict. To undo your merge do `git reset --hard HEAD`'), 45 | ]; 46 | } 47 | 48 | protected function getCurrentBranch(string $directory): string 49 | { 50 | $branch = "'".trim((string)shell_exec("cd {$directory}; git branch | grep \\* | cut -d ' ' -f2"))."'"; 51 | 52 | if ($branch === "''") { 53 | $branch = 'current branch'; 54 | } 55 | 56 | return $branch; 57 | } 58 | 59 | protected function hasMergeConflictExceptionMessage(Throwable $throwable): bool 60 | { 61 | if (str_starts_with($throwable->getMessage(), 'syntax error, unexpected token "<<"')) { 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Solutions/OpenAi/OpenAiSolutionResponse.php: -------------------------------------------------------------------------------- 1 | rawText = trim($rawText); 12 | } 13 | 14 | public function description(): string 15 | { 16 | return $this->between('FIX', 'ENDFIX', $this->rawText); 17 | } 18 | 19 | public function links(): array 20 | { 21 | $rawText = $this->finishString($this->rawText, 'ENDLINKS'); 22 | 23 | $textLinks = $this->between('LINKS', 'ENDLINKS', $rawText); 24 | 25 | $textLinks = explode(PHP_EOL, $textLinks); 26 | 27 | $textLinks = array_map(function ($textLink) { 28 | $textLink = str_replace('\\', '\\\\', $textLink); 29 | $textLink = str_replace('\\\\\\', '\\\\', $textLink); 30 | 31 | return json_decode($textLink, true); 32 | }, $textLinks); 33 | 34 | array_filter($textLinks); 35 | 36 | $links = []; 37 | 38 | foreach ($textLinks as $textLink) { 39 | if (isset($textLink['title']) && isset($textLink['url'])) { 40 | $links[$textLink['title']] = $textLink['url']; 41 | } 42 | } 43 | 44 | return $links; 45 | } 46 | 47 | protected function between(string $start, string $end, string $text): string 48 | { 49 | $startPosition = strpos($text, $start); 50 | if ($startPosition === false) { 51 | return ""; 52 | } 53 | 54 | $startPosition += strlen($start); 55 | $endPosition = strpos($text, $end, $startPosition); 56 | 57 | if ($endPosition === false) { 58 | return ""; 59 | } 60 | 61 | return trim(substr($text, $startPosition, $endPosition - $startPosition)); 62 | } 63 | 64 | /** 65 | * @note This function was copied from Laravel's Str::finish() 66 | */ 67 | protected function finishString(string $value, string $cap): string 68 | { 69 | $quoted = preg_quote($cap, '/'); 70 | 71 | return preg_replace('/(?:'.$quoted.')+$/u', '', $value).$cap; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "spatie/error-solutions", 3 | "description" : "This is my package error-solutions", 4 | "keywords" : [ 5 | "Spatie", 6 | "error-solutions" 7 | ], 8 | "homepage" : "https://github.com/spatie/error-solutions", 9 | "license" : "MIT", 10 | "authors" : [ 11 | { 12 | "name" : "Ruben Van Assche", 13 | "email" : "ruben@spatie.be", 14 | "role" : "Developer" 15 | } 16 | ], 17 | "require" : { 18 | "php": "^8.2" 19 | }, 20 | "require-dev" : { 21 | "livewire/livewire": "^2.11|^3.5.20", 22 | "illuminate/support": "^10.0|^11.0|^12.0", 23 | "illuminate/broadcasting" : "^10.0|^11.0|^12.0", 24 | "openai-php/client": "^0.13.0", 25 | "illuminate/cache" : "^10.0|^11.0|^12.0", 26 | "pestphp/pest" : "^2.20|^3.0", 27 | "phpstan/phpstan" : "^2.1", 28 | "psr/simple-cache-implementation" : "^3.0", 29 | "psr/simple-cache" : "^3.0", 30 | "spatie/ray" : "^1.28", 31 | "symfony/cache" : "^5.4|^6.0|^7.0|^8.0", 32 | "symfony/process" : "^5.4|^6.0|^7.0|^8.0", 33 | "vlucas/phpdotenv" : "^5.5", 34 | "orchestra/testbench": "8.22.3|^9.0|^10.0" 35 | }, 36 | "autoload" : { 37 | "psr-4" : { 38 | "Spatie\\ErrorSolutions\\" : "src" 39 | } 40 | }, 41 | "autoload-dev" : { 42 | "psr-4" : { 43 | "Spatie\\ErrorSolutions\\Tests\\" : "tests" 44 | } 45 | }, 46 | "suggest" : { 47 | "openai-php/client" : "Require get solutions from OpenAI", 48 | "simple-cache-implementation" : "To cache solutions from OpenAI" 49 | }, 50 | "scripts" : { 51 | "analyse" : "vendor/bin/phpstan analyse", 52 | "baseline" : "vendor/bin/phpstan analyse --generate-baseline", 53 | "test" : "vendor/bin/pest", 54 | "test-coverage" : "vendor/bin/pest --coverage", 55 | "format" : "vendor/bin/pint" 56 | }, 57 | "config" : { 58 | "sort-packages" : true, 59 | "allow-plugins" : { 60 | "pestphp/pest-plugin": true, 61 | "phpstan/extension-installer": true, 62 | "php-http/discovery": false 63 | } 64 | }, 65 | "minimum-stability" : "dev", 66 | "prefer-stable" : true 67 | } 68 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/MissingViteManifestSolutionProvider.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected array $links = []; 16 | 17 | public function __construct() 18 | { 19 | $this->links = [ 20 | 'Asset bundling with Vite' => 'https://laravel.com/docs/'.LaravelVersion::major().'.x/vite#running-vite', 21 | ]; 22 | } 23 | 24 | public function canSolve(Throwable $throwable): bool 25 | { 26 | return Str::startsWith($throwable->getMessage(), 'Vite manifest not found'); 27 | } 28 | 29 | public function getSolutions(Throwable $throwable): array 30 | { 31 | return [ 32 | $this->getSolution(), 33 | ]; 34 | } 35 | 36 | public function getSolution(): Solution 37 | { 38 | /** @var string */ 39 | $baseCommand = collect([ 40 | 'bun.lockb' => 'bun', 41 | 'bun.lock' => 'bun', 42 | 'pnpm-lock.yaml' => 'pnpm', 43 | 'yarn.lock' => 'yarn', 44 | ])->first(fn ($_, $lockfile) => file_exists(base_path($lockfile)), 'npm run'); 45 | 46 | return app()->environment('local') 47 | ? $this->getLocalSolution($baseCommand) 48 | : $this->getProductionSolution($baseCommand); 49 | } 50 | 51 | protected function getLocalSolution(string $baseCommand): Solution 52 | { 53 | return BaseSolution::create('Start the development server') 54 | ->setSolutionDescription("Run `{$baseCommand} dev` in your terminal and refresh the page.") 55 | ->setDocumentationLinks($this->links); 56 | } 57 | 58 | protected function getProductionSolution(string $baseCommand): Solution 59 | { 60 | return BaseSolution::create('Build the production assets') 61 | ->setSolutionDescription("Run `{$baseCommand} build` in your deployment script.") 62 | ->setDocumentationLinks($this->links); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Error solutions 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/error-solutions.svg?style=flat-square)](https://packagist.org/packages/spatie/error-solutions) 4 | [![Tests](https://img.shields.io/github/actions/workflow/status/spatie/error-solutions/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/error-solutions/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/error-solutions.svg?style=flat-square)](https://packagist.org/packages/spatie/error-solutions) 6 | 7 | At Spatie we develop multiple packages handling errors and providing solutions for these errors. This package is a collection of all these solutions. 8 | 9 | ## Support us 10 | 11 | [](https://spatie.be/github-ad-click/error-solutions) 12 | 13 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 14 | 15 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 16 | 17 | ## Installation 18 | 19 | You can install the package via composer: 20 | 21 | ```bash 22 | composer require spatie/error-solutions 23 | ``` 24 | 25 | ## Usage 26 | 27 | We've got some excellent documentation on how to use solutions: 28 | 29 | - [Flare](https://flareapp.io/docs/ignition/solutions/implementing-solutions) 30 | - [Ignition](https://github.com/spatie/ignition/?tab=readme-ov-file#displaying-solutions) 31 | 32 | ## Testing 33 | 34 | ```bash 35 | composer test 36 | ``` 37 | 38 | ## Changelog 39 | 40 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 41 | 42 | ## Contributing 43 | 44 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 45 | 46 | ## Security Vulnerabilities 47 | 48 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 49 | 50 | ## Credits 51 | 52 | - [Ruben Van Assche](https://github.com/rubenvanassche) 53 | - [All Contributors](../../contributors) 54 | 55 | ## License 56 | 57 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 58 | -------------------------------------------------------------------------------- /src/DiscoverSolutionProviders.php: -------------------------------------------------------------------------------- 1 | */ 10 | protected array $config = [ 11 | 'ai' => 'SolutionProviders/OpenAi', 12 | 'php' => 'SolutionProviders', 13 | 'laravel' => 'SolutionProviders/Laravel', 14 | ]; 15 | 16 | /** 17 | * @param array $types 18 | * 19 | * @return array> 20 | */ 21 | public static function for(array $types): array 22 | { 23 | if (in_array('php', $types)) { 24 | $types[] = 'ai'; 25 | } 26 | 27 | return (new self($types))->get(); 28 | } 29 | 30 | /** 31 | * @param array $types 32 | */ 33 | public function __construct(protected array $types) 34 | { 35 | 36 | } 37 | 38 | /** @return array> */ 39 | public function get(): array 40 | { 41 | $providers = []; 42 | 43 | foreach ($this->types as $type) { 44 | $providers = array_merge($providers, $this->getProviderClassesForType($type)); 45 | } 46 | 47 | return $providers; 48 | } 49 | 50 | /** @return array> */ 51 | protected function getProviderClassesForType(string $type): array 52 | { 53 | $relativePath = $this->config[$type] ?? null; 54 | 55 | if (! $relativePath) { 56 | return []; 57 | } 58 | 59 | $namespace = $this->getNamespaceForPath($relativePath); 60 | 61 | $globPattern = __DIR__ . '/' . $relativePath . '/*.php'; 62 | 63 | $files = glob($globPattern); 64 | 65 | if (! $files) { 66 | return []; 67 | } 68 | 69 | $solutionProviders = array_map(function (string $solutionProviderFilePath) use ($namespace) { 70 | $fileName = pathinfo($solutionProviderFilePath, PATHINFO_FILENAME); 71 | 72 | /** @var class-string $fqcn */ 73 | $fqcn = $namespace . '\\' . $fileName; 74 | 75 | $validClass = in_array(HasSolutionsForThrowable::class, class_implements($fqcn) ?: []); 76 | 77 | return $validClass ? $fqcn : null; 78 | }, $files); 79 | 80 | return array_values(array_filter($solutionProviders)); 81 | } 82 | 83 | protected function getNamespaceForPath(string $relativePath): string 84 | { 85 | $namespacePath = str_replace('/', '\\', $relativePath); 86 | 87 | $namespace = 'Spatie\\ErrorSolutions\\' . $namespacePath; 88 | 89 | return $namespace; 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/UnknownValidationSolutionProvider.php: -------------------------------------------------------------------------------- 1 | validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/m'; 19 | 20 | public function canSolve(Throwable $throwable): bool 21 | { 22 | if (! $throwable instanceof BadMethodCallException) { 23 | return false; 24 | } 25 | 26 | return ! is_null($this->getMethodFromExceptionMessage($throwable->getMessage())); 27 | } 28 | 29 | public function getSolutions(Throwable $throwable): array 30 | { 31 | return [ 32 | BaseSolution::create() 33 | ->setSolutionTitle('Unknown Validation Rule') 34 | ->setSolutionDescription($this->getSolutionDescription($throwable)), 35 | ]; 36 | } 37 | 38 | protected function getSolutionDescription(Throwable $throwable): string 39 | { 40 | $method = (string)$this->getMethodFromExceptionMessage($throwable->getMessage()); 41 | 42 | $possibleMethod = StringComparator::findSimilarText( 43 | $this->getAvailableMethods()->toArray(), 44 | $method 45 | ); 46 | 47 | if (empty($possibleMethod)) { 48 | return ''; 49 | } 50 | 51 | $rule = Str::snake(str_replace('validate', '', $possibleMethod)); 52 | 53 | return "Did you mean `{$rule}` ?"; 54 | } 55 | 56 | protected function getMethodFromExceptionMessage(string $message): ?string 57 | { 58 | if (! preg_match(self::REGEX, $message, $matches)) { 59 | return null; 60 | } 61 | 62 | return $matches['method']; 63 | } 64 | 65 | protected function getAvailableMethods(): Collection 66 | { 67 | $class = new ReflectionClass(Validator::class); 68 | 69 | $extensions = Collection::make((app('validator')->make([], []))->extensions) 70 | ->keys() 71 | ->map(fn (string $extension) => 'validate'.Str::studly($extension)); 72 | 73 | return Collection::make($class->getMethods()) 74 | ->filter(fn (ReflectionMethod $method) => preg_match('/(validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/', $method->name)) 75 | ->map(fn (ReflectionMethod $method) => $method->name) 76 | ->merge($extensions); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/SolutionProviders/BadMethodCallSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getClassAndMethodFromExceptionMessage($throwable->getMessage()))) { 23 | return false; 24 | } 25 | 26 | return true; 27 | } 28 | 29 | public function getSolutions(Throwable $throwable): array 30 | { 31 | return [ 32 | BaseSolution::create('Bad Method Call') 33 | ->setSolutionDescription($this->getSolutionDescription($throwable)), 34 | ]; 35 | } 36 | 37 | public function getSolutionDescription(Throwable $throwable): string 38 | { 39 | if (! $this->canSolve($throwable)) { 40 | return ''; 41 | } 42 | 43 | /** @phpstan-ignore-next-line */ 44 | extract($this->getClassAndMethodFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE); 45 | 46 | $possibleMethod = $this->findPossibleMethod($class ?? '', $method ?? ''); 47 | 48 | $class ??= 'UnknownClass'; 49 | 50 | return "Did you mean {$class}::{$possibleMethod?->name}() ?"; 51 | } 52 | 53 | /** 54 | * @param string $message 55 | * 56 | * @return null|array 57 | */ 58 | protected function getClassAndMethodFromExceptionMessage(string $message): ?array 59 | { 60 | if (! preg_match(self::REGEX, $message, $matches)) { 61 | return null; 62 | } 63 | 64 | return [ 65 | 'class' => $matches[1], 66 | 'method' => $matches[2], 67 | ]; 68 | } 69 | 70 | /** 71 | * @param class-string $class 72 | * @param string $invalidMethodName 73 | * 74 | * @return \ReflectionMethod|null 75 | */ 76 | protected function findPossibleMethod(string $class, string $invalidMethodName): ?ReflectionMethod 77 | { 78 | $methods = $this->getAvailableMethods($class); 79 | 80 | usort($methods, function (ReflectionMethod $a, ReflectionMethod $b) use ($invalidMethodName) { 81 | similar_text($invalidMethodName, $a->name, $percentA); 82 | similar_text($invalidMethodName, $b->name, $percentB); 83 | 84 | return $percentB <=> $percentA; 85 | }); 86 | 87 | return $methods[0] ?? null; 88 | } 89 | 90 | /** 91 | * @param class-string $class 92 | * 93 | * @return ReflectionMethod[] 94 | */ 95 | protected function getAvailableMethods(string $class): array 96 | { 97 | $refClass = new ReflectionClass($class); 98 | 99 | return $refClass->getMethods(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/InvalidRouteActionSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage(), $matches)) { 24 | return false; 25 | } 26 | 27 | return Str::startsWith($throwable->getMessage(), 'Invalid route action: '); 28 | } 29 | 30 | public function getSolutions(Throwable $throwable): array 31 | { 32 | preg_match(self::REGEX, $throwable->getMessage(), $matches); 33 | 34 | $invalidController = $matches[1] ?? null; 35 | 36 | if (! $invalidController) { 37 | return []; 38 | } 39 | 40 | $suggestedController = $this->findRelatedController($invalidController); 41 | 42 | if ($suggestedController === $invalidController) { 43 | return [ 44 | BaseSolution::create("`{$invalidController}` is not invokable.") 45 | ->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?"), 46 | ]; 47 | } 48 | 49 | if ($suggestedController) { 50 | return [ 51 | BaseSolution::create("`{$invalidController}` was not found.") 52 | ->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Did you mean `{$suggestedController}`?"), 53 | ]; 54 | } 55 | 56 | return [ 57 | BaseSolution::create("`{$invalidController}` was not found.") 58 | ->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Are you sure this controller exists and is imported correctly?"), 59 | ]; 60 | } 61 | 62 | protected function findRelatedController(string $invalidController): ?string 63 | { 64 | $composerClassMap = app(ComposerClassMap::class); 65 | 66 | $controllers = collect($composerClassMap->listClasses()) 67 | ->filter(function (string $file, string $fqcn) { 68 | return Str::endsWith($fqcn, 'Controller'); 69 | }) 70 | ->mapWithKeys(function (string $file, string $fqcn) { 71 | return [$fqcn => class_basename($fqcn)]; 72 | }) 73 | ->toArray(); 74 | 75 | $basenameMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4); 76 | 77 | $controllers = array_flip($controllers); 78 | 79 | $fqcnMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4); 80 | 81 | return $fqcnMatch ?? $basenameMatch; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/SolutionProviderRepository.php: -------------------------------------------------------------------------------- 1 | |HasSolutionsForThrowable> */ 14 | protected array $solutionProviders; 15 | 16 | /** @param array|HasSolutionsForThrowable> $solutionProviders */ 17 | public function __construct(array $solutionProviders = []) 18 | { 19 | $this->solutionProviders = $solutionProviders; 20 | } 21 | 22 | public function registerSolutionProvider(string|HasSolutionsForThrowable $solutionProvider): SolutionProviderRepositoryContract 23 | { 24 | $this->solutionProviders[] = $solutionProvider; 25 | 26 | return $this; 27 | } 28 | 29 | public function registerSolutionProviders(array $solutionProviders): SolutionProviderRepositoryContract 30 | { 31 | $this->solutionProviders = array_merge($this->solutionProviders, $solutionProviders); 32 | 33 | return $this; 34 | } 35 | 36 | public function getSolutionsForThrowable(Throwable $throwable): array 37 | { 38 | $solutions = []; 39 | 40 | if ($throwable instanceof Solution) { 41 | $solutions[] = $throwable; 42 | } 43 | 44 | if ($throwable instanceof ProvidesSolution) { 45 | $solutions[] = $throwable->getSolution(); 46 | } 47 | 48 | $providedSolutions = []; 49 | 50 | foreach ($this->initialiseSolutionProviderRepositories() as $solutionProvider) { 51 | try { 52 | $solvable = $solutionProvider->canSolve($throwable); 53 | 54 | if (! $solvable) { 55 | continue; 56 | } 57 | 58 | array_push($providedSolutions, ...$solutionProvider->getSolutions($throwable)); 59 | } catch (Throwable $exception) { 60 | continue; 61 | } 62 | } 63 | 64 | return [ 65 | ...$solutions, 66 | ...$providedSolutions, 67 | ]; 68 | } 69 | 70 | public function getSolutionForClass(string $solutionClass): ?Solution 71 | { 72 | if (! class_exists($solutionClass)) { 73 | return null; 74 | } 75 | 76 | if (! in_array(Solution::class, class_implements($solutionClass) ?: [])) { 77 | return null; 78 | } 79 | 80 | if (! function_exists('app')) { 81 | return null; 82 | } 83 | 84 | return app($solutionClass); 85 | } 86 | 87 | /** @return array */ 88 | protected function initialiseSolutionProviderRepositories(): array 89 | { 90 | $solutionProviders = array_filter( 91 | $this->solutionProviders, 92 | function (HasSolutionsForThrowable|string $provider) { 93 | if (! in_array(HasSolutionsForThrowable::class, class_implements($provider) ?: [])) { 94 | return false; 95 | } 96 | 97 | if (function_exists('config') && in_array($provider, config('error_solutions.ignored_solution_providers', []))) { 98 | return false; 99 | } 100 | 101 | return true; 102 | } 103 | ); 104 | 105 | return array_map( 106 | fn (string|HasSolutionsForThrowable $provider) => is_string($provider) ? new $provider : $provider, 107 | $solutionProviders 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Solutions/OpenAi/OpenAiSolution.php: -------------------------------------------------------------------------------- 1 | prompt = $this->generatePrompt(); 34 | 35 | $this->openAiSolutionResponse = $this->getAiSolution(); 36 | } 37 | 38 | public function getSolutionTitle(): string 39 | { 40 | return 'AI Generated Solution'; 41 | } 42 | 43 | public function getSolutionDescription(): string 44 | { 45 | return $this->openAiSolutionResponse->description(); 46 | } 47 | 48 | public function getDocumentationLinks(): array 49 | { 50 | return $this->openAiSolutionResponse->links(); 51 | } 52 | 53 | public function getAiSolution(): ?OpenAiSolutionResponse 54 | { 55 | $solution = $this->cache->get($this->getCacheKey()); 56 | 57 | if ($solution) { 58 | return new OpenAiSolutionResponse($solution); 59 | } 60 | 61 | $solutionText = OpenAI::client($this->openAiKey) 62 | ->chat() 63 | ->create([ 64 | 'model' => $this->getModel(), 65 | 'messages' => [['role' => 'user', 'content' => $this->prompt]], 66 | 'max_tokens' => 1000, 67 | 'temperature' => 0, 68 | ])->choices[0]->message->content; 69 | 70 | $this->cache->set($this->getCacheKey(), $solutionText, $this->cacheTtlInSeconds); 71 | 72 | return new OpenAiSolutionResponse($solutionText); 73 | } 74 | 75 | protected function getCacheKey(): string 76 | { 77 | $hash = sha1($this->prompt); 78 | 79 | return "error-solution-{$hash}"; 80 | } 81 | 82 | protected function generatePrompt(): string 83 | { 84 | $viewPath = __DIR__.'/../../../resources/views/aiPrompt.php'; 85 | 86 | $viewModel = new OpenAiPromptViewModel( 87 | file: $this->throwable->getFile(), 88 | exceptionMessage: $this->throwable->getMessage(), 89 | exceptionClass: get_class($this->throwable), 90 | snippet: $this->getApplicationFrame($this->throwable)->getSnippetAsString(15), 91 | line: $this->throwable->getLine(), 92 | applicationType: $this->applicationType, 93 | ); 94 | 95 | return (new AiPromptRenderer())->renderAsString( 96 | ['viewModel' => $viewModel], 97 | $viewPath, 98 | ); 99 | } 100 | 101 | protected function getModel(): string 102 | { 103 | return $this->openAiModel ?? 'gpt-3.5-turbo'; 104 | } 105 | 106 | protected function getApplicationFrame(Throwable $throwable): ?Frame 107 | { 108 | $backtrace = Backtrace::createForThrowable($throwable); 109 | 110 | if ($this->applicationPath) { 111 | $backtrace->applicationPath($this->applicationPath); 112 | } 113 | 114 | $frames = $backtrace->frames(); 115 | 116 | return $frames[$backtrace->firstApplicationFrameIndex()] ?? null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getNameAndView($throwable) !== null; 26 | } 27 | 28 | public function getSolutions(Throwable $throwable): array 29 | { 30 | $solutions = []; 31 | 32 | /** @phpstan-ignore-next-line */ 33 | extract($this->getNameAndView($throwable)); 34 | 35 | if (! isset($variableName)) { 36 | return []; 37 | } 38 | 39 | if (isset($viewFile)) { 40 | /** @phpstan-ignore-next-line */ 41 | $solutions = $this->findCorrectVariableSolutions($throwable, $variableName, $viewFile); 42 | $solutions[] = $this->findOptionalVariableSolution($variableName, $viewFile); 43 | } 44 | 45 | 46 | return $solutions; 47 | } 48 | 49 | /** 50 | * @param FlareViewException $throwable 51 | * @param string $variableName 52 | * @param string $viewFile 53 | * 54 | * @return array 55 | */ 56 | protected function findCorrectVariableSolutions( 57 | FlareViewException $throwable, 58 | string $variableName, 59 | string $viewFile 60 | ): array { 61 | return collect($throwable->getViewData()) 62 | ->map(function ($value, $key) use ($variableName) { 63 | similar_text($variableName, $key, $percentage); 64 | 65 | return ['match' => $percentage, 'value' => $value]; 66 | }) 67 | ->sortByDesc('match') 68 | ->filter(fn ($var) => $var['match'] > 40) 69 | ->keys() 70 | ->map(fn ($suggestion) => new SuggestCorrectVariableNameSolution($variableName, $viewFile, $suggestion)) 71 | ->map(function ($solution) { 72 | return $solution->isRunnable() 73 | ? $solution 74 | : BaseSolution::create($solution->getSolutionTitle()) 75 | ->setSolutionDescription($solution->getSolutionDescription()); 76 | }) 77 | ->toArray(); 78 | } 79 | 80 | protected function findOptionalVariableSolution(string $variableName, string $viewFile): Solution 81 | { 82 | $optionalSolution = new MakeViewVariableOptionalSolution($variableName, $viewFile); 83 | 84 | return $optionalSolution->isRunnable() 85 | ? $optionalSolution 86 | : BaseSolution::create($optionalSolution->getSolutionTitle()) 87 | ->setSolutionDescription($optionalSolution->getSolutionDescription()); 88 | } 89 | 90 | /** 91 | * @param \Throwable $throwable 92 | * 93 | * @return array|null 94 | */ 95 | protected function getNameAndView(Throwable $throwable): ?array 96 | { 97 | $pattern = '/Undefined variable:? (.*?) \(View: (.*?)\)/'; 98 | 99 | preg_match($pattern, $throwable->getMessage(), $matches); 100 | 101 | if (count($matches) === 3) { 102 | [, $variableName, $viewFile] = $matches; 103 | $variableName = ltrim($variableName, '$'); 104 | 105 | return compact('variableName', 'viewFile'); 106 | } 107 | 108 | return null; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/SolutionProviders/Laravel/ViewNotFoundSolutionProvider.php: -------------------------------------------------------------------------------- 1 | getMessage(), $matches); 26 | } 27 | 28 | public function getSolutions(Throwable $throwable): array 29 | { 30 | preg_match(self::REGEX, $throwable->getMessage(), $matches); 31 | 32 | $missingView = $matches[1] ?? null; 33 | 34 | $suggestedView = $this->findRelatedView($missingView); 35 | 36 | if ($suggestedView) { 37 | return [ 38 | BaseSolution::create() 39 | ->setSolutionTitle("{$missingView} was not found.") 40 | ->setSolutionDescription("Did you mean `{$suggestedView}`?"), 41 | ]; 42 | } 43 | 44 | return [ 45 | BaseSolution::create() 46 | ->setSolutionTitle("{$missingView} was not found.") 47 | ->setSolutionDescription('Are you sure the view exists and is a `.blade.php` file?'), 48 | ]; 49 | } 50 | 51 | protected function findRelatedView(string $missingView): ?string 52 | { 53 | $views = $this->getAllViews(); 54 | 55 | return StringComparator::findClosestMatch($views, $missingView); 56 | } 57 | 58 | /** @return array */ 59 | protected function getAllViews(): array 60 | { 61 | /** @var \Illuminate\View\FileViewFinder $fileViewFinder */ 62 | $fileViewFinder = View::getFinder(); 63 | 64 | $extensions = $fileViewFinder->getExtensions(); 65 | 66 | $viewsForHints = collect($fileViewFinder->getHints()) 67 | ->flatMap(function ($paths, string $namespace) use ($extensions) { 68 | $paths = Arr::wrap($paths); 69 | 70 | return collect($paths) 71 | ->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions)) 72 | ->map(fn (string $view) => "{$namespace}::{$view}") 73 | ->toArray(); 74 | }); 75 | 76 | $viewsForViewPaths = collect($fileViewFinder->getPaths()) 77 | ->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions)); 78 | 79 | return $viewsForHints->merge($viewsForViewPaths)->toArray(); 80 | } 81 | 82 | /** 83 | * @param string $path 84 | * @param array $extensions 85 | * 86 | * @return array 87 | */ 88 | protected function getViewsInPath(string $path, array $extensions): array 89 | { 90 | $filePatterns = array_map(fn (string $extension) => "*.{$extension}", $extensions); 91 | 92 | $extensionsWithDots = array_map(fn (string $extension) => ".{$extension}", $extensions); 93 | 94 | $files = (new Finder()) 95 | ->in($path) 96 | ->files(); 97 | 98 | foreach ($filePatterns as $filePattern) { 99 | $files->name($filePattern); 100 | } 101 | 102 | $views = []; 103 | 104 | foreach ($files as $file) { 105 | $view = $file->getRelativePathname(); 106 | $view = str_replace($extensionsWithDots, '', $view); 107 | $view = str_replace('/', '.', $view); 108 | $views[] = $view; 109 | } 110 | 111 | return $views; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Support/Laravel/Composer/ComposerClassMap.php: -------------------------------------------------------------------------------- 1 | composer = file_exists($autoloaderPath) 23 | ? require $autoloaderPath 24 | : new FakeComposer(); 25 | 26 | $this->basePath = app_path(); 27 | } 28 | 29 | /** @return array */ 30 | public function listClasses(): array 31 | { 32 | $classes = $this->composer->getClassMap(); 33 | 34 | return array_merge($classes, $this->listClassesInPsrMaps()); 35 | } 36 | 37 | public function searchClassMap(string $missingClass): ?string 38 | { 39 | foreach ($this->composer->getClassMap() as $fqcn => $file) { 40 | $basename = basename($file, '.php'); 41 | 42 | if ($basename === $missingClass) { 43 | return $fqcn; 44 | } 45 | } 46 | 47 | return null; 48 | } 49 | 50 | /** @return array */ 51 | public function listClassesInPsrMaps(): array 52 | { 53 | // TODO: This is incorrect. Doesnt list all fqcns. Need to parse namespace? e.g. App\LoginController is wrong 54 | 55 | $prefixes = array_merge( 56 | $this->composer->getPrefixes(), 57 | $this->composer->getPrefixesPsr4() 58 | ); 59 | 60 | $classes = []; 61 | 62 | foreach ($prefixes as $namespace => $directories) { 63 | foreach ($directories as $directory) { 64 | if (file_exists($directory)) { 65 | $files = (new Finder) 66 | ->in($directory) 67 | ->files() 68 | ->name('*.php'); 69 | 70 | foreach ($files as $file) { 71 | $fqcn = $this->getFullyQualifiedClassNameFromFile($namespace, $file); 72 | 73 | $classes[$fqcn] = $file->getRelativePathname(); 74 | } 75 | } 76 | } 77 | } 78 | 79 | return $classes; 80 | } 81 | 82 | public function searchPsrMaps(string $missingClass): ?string 83 | { 84 | $prefixes = array_merge( 85 | $this->composer->getPrefixes(), 86 | $this->composer->getPrefixesPsr4() 87 | ); 88 | 89 | foreach ($prefixes as $namespace => $directories) { 90 | foreach ($directories as $directory) { 91 | if (file_exists($directory)) { 92 | $files = (new Finder) 93 | ->in($directory) 94 | ->files() 95 | ->name('*.php'); 96 | 97 | foreach ($files as $file) { 98 | $basename = basename($file->getRelativePathname(), '.php'); 99 | 100 | if ($basename === $missingClass) { 101 | return $namespace.basename($file->getRelativePathname(), '.php'); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | return null; 109 | } 110 | 111 | protected function getFullyQualifiedClassNameFromFile(string $rootNamespace, SplFileInfo $file): string 112 | { 113 | $class = trim(str_replace($this->basePath, '', (string) $file->getRealPath()), DIRECTORY_SEPARATOR); 114 | 115 | $class = str_replace( 116 | [DIRECTORY_SEPARATOR, 'App\\'], 117 | ['\\', app()->getNamespace()], 118 | ucfirst(Str::replaceLast('.php', '', $class)) 119 | ); 120 | 121 | return $rootNamespace.$class; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Support/Laravel/LivewireComponentParser.php: -------------------------------------------------------------------------------- 1 | */ 17 | protected string $componentClass; 18 | 19 | /** @var ReflectionClass */ 20 | protected ReflectionClass $reflectionClass; 21 | 22 | public static function create(string $componentAlias): self 23 | { 24 | return new self($componentAlias); 25 | } 26 | 27 | public function __construct(protected string $componentAlias) 28 | { 29 | $componentClass = null; 30 | 31 | if (app()->has(ComponentRegistry::class)) { 32 | $componentClass = app(ComponentRegistry::class)->getClass($this->componentAlias); 33 | } 34 | 35 | if ($componentClass === null && app()->has(LivewireManager::class)) { 36 | $livewireManager = app(LivewireManager::class); 37 | 38 | $componentClass = method_exists($livewireManager, 'getClass') 39 | ? $livewireManager->getClass($this->componentAlias) 40 | : null; 41 | } 42 | 43 | if ($componentClass === null) { 44 | throw new \RuntimeException("Could not resolve component class for alias `{$this->componentAlias}`."); 45 | } 46 | 47 | $this->componentClass = $componentClass; 48 | $this->reflectionClass = new ReflectionClass($componentClass); 49 | } 50 | 51 | public function getComponentClass(): string 52 | { 53 | return $this->componentClass; 54 | } 55 | 56 | /** @return Collection */ 57 | public function getPropertyNamesLike(string $similar): Collection 58 | { 59 | $properties = collect($this->reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC)) 60 | ->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->class !== $this->reflectionClass->name) 61 | ->map(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->name); 62 | 63 | $computedProperties = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) 64 | ->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name) 65 | ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'get') && str_ends_with($reflectionMethod->name, 'Property')) 66 | ->map(fn (ReflectionMethod $reflectionMethod) => lcfirst(Str::of($reflectionMethod->name)->after('get')->before('Property'))); 67 | 68 | return $this->filterItemsBySimilarity( 69 | $properties->merge($computedProperties), 70 | $similar 71 | ); 72 | } 73 | 74 | /** @return Collection */ 75 | public function getMethodNamesLike(string $similar): Collection 76 | { 77 | $methods = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) 78 | ->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name) 79 | ->map(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->name); 80 | 81 | return $this->filterItemsBySimilarity($methods, $similar); 82 | } 83 | 84 | /** 85 | * @param Collection $items 86 | * 87 | * @return Collection 88 | */ 89 | protected function filterItemsBySimilarity(Collection $items, string $similar): Collection 90 | { 91 | return $items 92 | ->map(function (string $name) use ($similar) { 93 | similar_text($similar, $name, $percentage); 94 | 95 | return ['match' => $percentage, 'value' => $name]; 96 | }) 97 | ->sortByDesc('match') 98 | ->filter(function (array $item) { 99 | return $item['match'] > 40; 100 | }) 101 | ->map(function (array $item) { 102 | return $item['value']; 103 | }) 104 | ->values(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/SolutionProviders/UndefinedPropertySolutionProvider.php: -------------------------------------------------------------------------------- 1 | getClassAndPropertyFromExceptionMessage($throwable->getMessage()))) { 24 | return false; 25 | } 26 | 27 | if (! $this->similarPropertyExists($throwable)) { 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | public function getSolutions(Throwable $throwable): array 35 | { 36 | return [ 37 | BaseSolution::create('Unknown Property') 38 | ->setSolutionDescription($this->getSolutionDescription($throwable)), 39 | ]; 40 | } 41 | 42 | public function getSolutionDescription(Throwable $throwable): string 43 | { 44 | if (! $this->canSolve($throwable) || ! $this->similarPropertyExists($throwable)) { 45 | return ''; 46 | } 47 | 48 | extract( 49 | /** @phpstan-ignore-next-line */ 50 | $this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()), 51 | EXTR_OVERWRITE, 52 | ); 53 | 54 | $possibleProperty = $this->findPossibleProperty($class ?? '', $property ?? ''); 55 | 56 | $class = $class ?? ''; 57 | 58 | if (! $possibleProperty) { 59 | return "Did you mean another property in {$class}?"; 60 | } 61 | 62 | return "Did you mean {$class}::\${$possibleProperty->name} ?"; 63 | } 64 | 65 | protected function similarPropertyExists(Throwable $throwable): bool 66 | { 67 | /** @phpstan-ignore-next-line */ 68 | extract($this->getClassAndPropertyFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE); 69 | 70 | $possibleProperty = $this->findPossibleProperty($class ?? '', $property ?? ''); 71 | 72 | return $possibleProperty !== null; 73 | } 74 | 75 | /** 76 | * @param string $message 77 | * 78 | * @return null|array 79 | */ 80 | protected function getClassAndPropertyFromExceptionMessage(string $message): ?array 81 | { 82 | if (! preg_match(self::REGEX, $message, $matches)) { 83 | return null; 84 | } 85 | 86 | return [ 87 | 'class' => $matches[1], 88 | 'property' => $matches[2], 89 | ]; 90 | } 91 | 92 | /** 93 | * @param class-string $class 94 | * @param string $invalidPropertyName 95 | * 96 | * @return ReflectionProperty|null 97 | */ 98 | protected function findPossibleProperty(string $class, string $invalidPropertyName): ?ReflectionProperty 99 | { 100 | $properties = $this->getAvailableProperties($class); 101 | 102 | usort($properties, function (ReflectionProperty $a, ReflectionProperty $b) use ($invalidPropertyName) { 103 | similar_text($invalidPropertyName, $a->name, $percentageA); 104 | similar_text($invalidPropertyName, $b->name, $percentageB); 105 | 106 | return $percentageB <=> $percentageA; // Sort descending 107 | }); 108 | 109 | $filteredProperties = array_values(array_filter($properties, function (ReflectionProperty $property) use ($invalidPropertyName) { 110 | similar_text($invalidPropertyName, $property->name, $percentage); 111 | 112 | return $percentage >= self::MINIMUM_SIMILARITY; 113 | })); 114 | 115 | return $filteredProperties[0] ?? null; 116 | } 117 | 118 | /** 119 | * @param class-string $class 120 | * 121 | * @return ReflectionProperty[] 122 | */ 123 | protected function getAvailableProperties(string $class): array 124 | { 125 | $reflectionClass = new ReflectionClass($class); 126 | 127 | return $reflectionClass->getProperties(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Solutions/Laravel/MakeViewVariableOptionalSolution.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 21 | 22 | $this->viewFile = $viewFile; 23 | } 24 | 25 | public function getSolutionTitle(): string 26 | { 27 | return "$$this->variableName is undefined"; 28 | } 29 | 30 | public function getDocumentationLinks(): array 31 | { 32 | return []; 33 | } 34 | 35 | public function getSolutionActionDescription(): string 36 | { 37 | $output = [ 38 | 'Make the variable optional in the blade template.', 39 | "Replace `{{ $$this->variableName }}` with `{{ $$this->variableName ?? '' }}`", 40 | ]; 41 | 42 | return implode(PHP_EOL, $output); 43 | } 44 | 45 | public function getRunButtonText(): string 46 | { 47 | return 'Make variable optional'; 48 | } 49 | 50 | public function getSolutionDescription(): string 51 | { 52 | return $this->getSolutionActionDescription(); 53 | } 54 | 55 | public function getRunParameters(): array 56 | { 57 | return [ 58 | 'variableName' => $this->variableName, 59 | 'viewFile' => $this->viewFile, 60 | ]; 61 | } 62 | 63 | /** 64 | * @param array $parameters 65 | * 66 | * @return bool 67 | */ 68 | public function isRunnable(array $parameters = []): bool 69 | { 70 | return $this->makeOptional($this->getRunParameters()) !== false; 71 | } 72 | 73 | /** 74 | * @param array $parameters 75 | * 76 | * @return void 77 | */ 78 | public function run(array $parameters = []): void 79 | { 80 | $output = $this->makeOptional($parameters); 81 | if ($output !== false) { 82 | file_put_contents($parameters['viewFile'], $output); 83 | } 84 | } 85 | 86 | protected function isSafePath(string $path): bool 87 | { 88 | if (! Str::startsWith($path, ['/', './'])) { 89 | return false; 90 | } 91 | if (! Str::endsWith($path, '.blade.php')) { 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | /** 99 | * @param array $parameters 100 | * 101 | * @return bool|string 102 | */ 103 | public function makeOptional(array $parameters = []): bool|string 104 | { 105 | if (! $this->isSafePath($parameters['viewFile'])) { 106 | return false; 107 | } 108 | 109 | $originalContents = (string)file_get_contents($parameters['viewFile']); 110 | $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents); 111 | 112 | $originalTokens = token_get_all(Blade::compileString($originalContents)); 113 | $newTokens = token_get_all(Blade::compileString($newContents)); 114 | 115 | $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']); 116 | 117 | if ($expectedTokens !== $newTokens) { 118 | return false; 119 | } 120 | 121 | return $newContents; 122 | } 123 | 124 | /** 125 | * @param array $originalTokens 126 | * @param string $variableName 127 | * 128 | * @return array 129 | */ 130 | protected function generateExpectedTokens(array $originalTokens, string $variableName): array 131 | { 132 | $expectedTokens = []; 133 | foreach ($originalTokens as $token) { 134 | $expectedTokens[] = $token; 135 | if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) { 136 | $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]]; 137 | $expectedTokens[] = [T_COALESCE, '??', $token[2]]; 138 | $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]]; 139 | $expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]]; 140 | } 141 | } 142 | 143 | return $expectedTokens; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `error-solutions` will be documented in this file. 4 | 5 | ## 2.0.4 - 2025-11-28 6 | 7 | ### What's Changed 8 | 9 | * fix: casted the error code to int before comparison by @yuriy-sorokin in https://github.com/spatie/error-solutions/pull/42 10 | 11 | ### New Contributors 12 | 13 | * @yuriy-sorokin made their first contribution in https://github.com/spatie/error-solutions/pull/42 14 | 15 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/2.0.3...2.0.4 16 | 17 | ## 2.0.3 - 2025-11-26 18 | 19 | ### What's Changed 20 | 21 | * Added Symfony 8 support to all symfony/* packages. by @thecaliskan in https://github.com/spatie/error-solutions/pull/40 22 | * Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/spatie/error-solutions/pull/41 23 | 24 | ### New Contributors 25 | 26 | * @thecaliskan made their first contribution in https://github.com/spatie/error-solutions/pull/40 27 | 28 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/2.0.2...2.0.3 29 | 30 | ## 2.0.2 - 2025-11-06 31 | 32 | ### What's Changed 33 | 34 | * Update issue template by @AlexVanderbist in https://github.com/spatie/error-solutions/pull/36 35 | * Update MissingViteManifestSolutionProvider.php to include bun by @fylzero in https://github.com/spatie/error-solutions/pull/39 36 | * Bump stefanzweifel/git-auto-commit-action from 5 to 7 by @dependabot[bot] in https://github.com/spatie/error-solutions/pull/37 37 | * Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/error-solutions/pull/35 38 | 39 | ### New Contributors 40 | 41 | * @AlexVanderbist made their first contribution in https://github.com/spatie/error-solutions/pull/36 42 | * @fylzero made their first contribution in https://github.com/spatie/error-solutions/pull/39 43 | 44 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/2.0.1...2.0.2 45 | 46 | ## 2.0.1 - 2025-07-01 47 | 48 | - Rewrite transformer 49 | 50 | ## 2.0.0 - 2025-05-15 51 | 52 | - Removes legacy stuff for Ignition 53 | - Removes dependency on Laravel for PHP framework agnostic parts 54 | - Fixes some issues which were raised by PHPStan 55 | 56 | ## 1.1.3 - 2025-02-14 57 | 58 | ### What's Changed 59 | 60 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/error-solutions/pull/26 61 | * Update phpstan/phpstan requirement from ^1.11 to ^2.1 by @dependabot in https://github.com/spatie/error-solutions/pull/25 62 | * Remove unneccesary conditional in "Run Tests" workflow by @duncanmcclean in https://github.com/spatie/error-solutions/pull/27 63 | * Supports Laravel 12 by @duncanmcclean in https://github.com/spatie/error-solutions/pull/28 64 | 65 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.1.2...1.1.3 66 | 67 | ## 1.1.2 - 2024-12-11 68 | 69 | ### What's Changed 70 | 71 | * Documentation link should follow the latest major version by @mshukurlu in https://github.com/spatie/error-solutions/pull/17 72 | * Replace implicitly nullable parameters for PHP 8.4 by @txdFabio in https://github.com/spatie/error-solutions/pull/22 73 | 74 | ### New Contributors 75 | 76 | * @mshukurlu made their first contribution in https://github.com/spatie/error-solutions/pull/17 77 | * @txdFabio made their first contribution in https://github.com/spatie/error-solutions/pull/22 78 | 79 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.1.1...1.1.2 80 | 81 | ## 1.1.1 - 2024-07-25 82 | 83 | ### What's Changed 84 | 85 | * Fix OpenAI response text links by @Lukaaashek in https://github.com/spatie/error-solutions/pull/9 86 | 87 | ### New Contributors 88 | 89 | * @Lukaaashek made their first contribution in https://github.com/spatie/error-solutions/pull/9 90 | 91 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.1.0...1.1.1 92 | 93 | ## 1.1.0 - 2024-07-22 94 | 95 | ### What's Changed 96 | 97 | * Allow to customize OpenAI Model by @arnebr in https://github.com/spatie/error-solutions/pull/7 98 | 99 | ### New Contributors 100 | 101 | * @arnebr made their first contribution in https://github.com/spatie/error-solutions/pull/7 102 | 103 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.0.5...1.1.0 104 | 105 | ## 1.0.5 - 2024-07-09 106 | 107 | ### What's Changed 108 | 109 | * Legacy `RunnableSolution` should continue to extend legacy `Solution` by @duncanmcclean in https://github.com/spatie/error-solutions/pull/6 110 | 111 | ### New Contributors 112 | 113 | * @duncanmcclean made their first contribution in https://github.com/spatie/error-solutions/pull/6 114 | 115 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.0.4...1.0.5 116 | 117 | ## 1.0.4 - 2024-06-28 118 | 119 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.0.3...1.0.4 120 | 121 | ## 1.0.3 - 2024-06-27 122 | 123 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.0.2...1.0.3 124 | 125 | ## 1.0.2 - 2024-06-26 126 | 127 | ### What's Changed 128 | 129 | * Fix AI solutions 130 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.1.0 by @dependabot in https://github.com/spatie/error-solutions/pull/1 131 | 132 | ### New Contributors 133 | 134 | * @dependabot made their first contribution in https://github.com/spatie/error-solutions/pull/1 135 | 136 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/0.0.1...1.0.2 137 | 138 | ## 1.0.1 - 2024-06-21 139 | 140 | - Add the legacy string comperator 141 | 142 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/1.0.0...1.0.1 143 | 144 | ## 1.0.0 - 2024-06-12 145 | 146 | - Initial release 147 | 148 | **Full Changelog**: https://github.com/spatie/error-solutions/compare/0.0.1...1.0.0 149 | 150 | ## 0.0.1 - 2024-06-11 151 | 152 | **Full Changelog**: https://github.com/spatie/error-solutions/commits/0.0.1 153 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^PHPDoc tag @param for parameter \$solutionProvider with type class\-string\\|Spatie\\ErrorSolutions\\Contracts\\HasSolutionsForThrowable is not subtype of native type string\.$#' 5 | identifier: parameter.phpDocType 6 | count: 1 7 | path: src/Contracts/SolutionProviderRepository.php 8 | 9 | - 10 | message: '#^Call to method getSolutionDescription\(\) on an unknown class Spatie\\ErrorSolutions\\Solutions\\Laravel\\SuggestCorrectVariableNameSolution\.$#' 11 | identifier: class.notFound 12 | count: 1 13 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 14 | 15 | - 16 | message: '#^Call to method getSolutionTitle\(\) on an unknown class Spatie\\ErrorSolutions\\Solutions\\Laravel\\SuggestCorrectVariableNameSolution\.$#' 17 | identifier: class.notFound 18 | count: 1 19 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 20 | 21 | - 22 | message: '#^Call to method getViewData\(\) on an unknown class Spatie\\LaravelFlare\\Exceptions\\ViewException\.$#' 23 | identifier: class.notFound 24 | count: 1 25 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 26 | 27 | - 28 | message: '#^Call to method isRunnable\(\) on an unknown class Spatie\\ErrorSolutions\\Solutions\\Laravel\\SuggestCorrectVariableNameSolution\.$#' 29 | identifier: class.notFound 30 | count: 1 31 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 32 | 33 | - 34 | message: '#^Class Spatie\\LaravelFlare\\Exceptions\\ViewException not found\.$#' 35 | identifier: class.notFound 36 | count: 1 37 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 38 | 39 | - 40 | message: '#^Instantiated class Spatie\\ErrorSolutions\\Solutions\\Laravel\\SuggestCorrectVariableNameSolution not found\.$#' 41 | identifier: class.notFound 42 | count: 1 43 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 44 | 45 | - 46 | message: '#^Parameter \$throwable of method Spatie\\ErrorSolutions\\SolutionProviders\\Laravel\\UndefinedViewVariableSolutionProvider\:\:findCorrectVariableSolutions\(\) has invalid type Spatie\\LaravelFlare\\Exceptions\\ViewException\.$#' 47 | identifier: class.notFound 48 | count: 2 49 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 50 | 51 | - 52 | message: '#^Unable to resolve the template type TKey in call to function collect$#' 53 | identifier: argument.templateType 54 | count: 1 55 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 56 | 57 | - 58 | message: '#^Unable to resolve the template type TValue in call to function collect$#' 59 | identifier: argument.templateType 60 | count: 1 61 | path: src/SolutionProviders/Laravel/UndefinedViewVariableSolutionProvider.php 62 | 63 | - 64 | message: '#^Method Spatie\\ErrorSolutions\\SolutionProviders\\Laravel\\UnknownValidationSolutionProvider\:\:getAvailableMethods\(\) return type with generic class Illuminate\\Support\\Collection does not specify its types\: TKey, TValue$#' 65 | identifier: missingType.generics 66 | count: 1 67 | path: src/SolutionProviders/Laravel/UnknownValidationSolutionProvider.php 68 | 69 | - 70 | message: '#^Parameter \#1 \$callback of method Illuminate\\Support\\Collection\\:\:filter\(\) expects \(callable\(ReflectionMethod, int\)\: bool\)\|null, Closure\(ReflectionMethod\)\: \(0\|1\|false\) given\.$#' 71 | identifier: argument.type 72 | count: 1 73 | path: src/SolutionProviders/Laravel/UnknownValidationSolutionProvider.php 74 | 75 | - 76 | message: '#^Unable to resolve the template type TMakeKey in call to method static method Illuminate\\Support\\Collection\<\(int\|string\),mixed\>\:\:make\(\)$#' 77 | identifier: argument.templateType 78 | count: 1 79 | path: src/SolutionProviders/Laravel/UnknownValidationSolutionProvider.php 80 | 81 | - 82 | message: '#^Unable to resolve the template type TMakeValue in call to method static method Illuminate\\Support\\Collection\<\(int\|string\),mixed\>\:\:make\(\)$#' 83 | identifier: argument.templateType 84 | count: 1 85 | path: src/SolutionProviders/Laravel/UnknownValidationSolutionProvider.php 86 | 87 | - 88 | message: '#^Call to method getMessage\(\) on an unknown class Spatie\\LaravelFlare\\Exceptions\\ViewException\.$#' 89 | identifier: class.notFound 90 | count: 1 91 | path: src/SolutionProviders/Laravel/ViewNotFoundSolutionProvider.php 92 | 93 | - 94 | message: '#^Class Spatie\\LaravelFlare\\Exceptions\\ViewException not found\.$#' 95 | identifier: class.notFound 96 | count: 1 97 | path: src/SolutionProviders/Laravel/ViewNotFoundSolutionProvider.php 98 | 99 | - 100 | message: '#^Parameter \#1 \$missingView of method Spatie\\ErrorSolutions\\SolutionProviders\\Laravel\\ViewNotFoundSolutionProvider\:\:findRelatedView\(\) expects string, string\|null given\.$#' 101 | identifier: argument.type 102 | count: 1 103 | path: src/SolutionProviders/Laravel/ViewNotFoundSolutionProvider.php 104 | 105 | - 106 | message: '#^Call to method build\(\) on an unknown class Livewire\\LivewireComponentsFinder\.$#' 107 | identifier: class.notFound 108 | count: 1 109 | path: src/Solutions/Laravel/LivewireDiscoverSolution.php 110 | 111 | - 112 | message: '#^Class Livewire\\LivewireComponentsFinder not found\.$#' 113 | identifier: class.notFound 114 | count: 2 115 | path: src/Solutions/Laravel/LivewireDiscoverSolution.php 116 | 117 | - 118 | message: '#^Method Spatie\\ErrorSolutions\\Solutions\\OpenAi\\DummyCache\:\:setMultiple\(\) has parameter \$values with no value type specified in iterable type iterable\.$#' 119 | identifier: missingType.iterableValue 120 | count: 1 121 | path: src/Solutions/OpenAi/DummyCache.php 122 | 123 | - 124 | message: '#^Cannot call method get\(\) on Psr\\SimpleCache\\CacheInterface\|null\.$#' 125 | identifier: method.nonObject 126 | count: 1 127 | path: src/Solutions/OpenAi/OpenAiSolution.php 128 | 129 | - 130 | message: '#^Cannot call method getSnippetAsString\(\) on Spatie\\Backtrace\\Frame\|null\.$#' 131 | identifier: method.nonObject 132 | count: 1 133 | path: src/Solutions/OpenAi/OpenAiSolution.php 134 | 135 | - 136 | message: '#^Cannot call method set\(\) on Psr\\SimpleCache\\CacheInterface\|null\.$#' 137 | identifier: method.nonObject 138 | count: 1 139 | path: src/Solutions/OpenAi/OpenAiSolution.php 140 | 141 | - 142 | message: '#^Parameter \#1 \$rawText of class Spatie\\ErrorSolutions\\Solutions\\OpenAi\\OpenAiSolutionResponse constructor expects string, string\|null given\.$#' 143 | identifier: argument.type 144 | count: 1 145 | path: src/Solutions/OpenAi/OpenAiSolution.php 146 | 147 | - 148 | message: '#^Parameter \$line of class Spatie\\ErrorSolutions\\Solutions\\OpenAi\\OpenAiPromptViewModel constructor expects string, int given\.$#' 149 | identifier: argument.type 150 | count: 1 151 | path: src/Solutions/OpenAi/OpenAiSolution.php 152 | 153 | - 154 | message: '#^Property Spatie\\ErrorSolutions\\Solutions\\OpenAi\\OpenAiSolution\:\:\$openAiSolutionResponse \(Spatie\\ErrorSolutions\\Solutions\\OpenAi\\OpenAiSolutionResponse\) does not accept Spatie\\ErrorSolutions\\Solutions\\OpenAi\\OpenAiSolutionResponse\|null\.$#' 155 | identifier: assign.propertyType 156 | count: 1 157 | path: src/Solutions/OpenAi/OpenAiSolution.php 158 | 159 | - 160 | message: '#^Method Spatie\\ErrorSolutions\\Solutions\\OpenAi\\OpenAiSolutionResponse\:\:links\(\) return type has no value type specified in iterable type array\.$#' 161 | identifier: missingType.iterableValue 162 | count: 1 163 | path: src/Solutions/OpenAi/OpenAiSolutionResponse.php 164 | 165 | - 166 | message: '#^Property Spatie\\ErrorSolutions\\Support\\Laravel\\LivewireComponentParser\:\:\$reflectionClass \(ReflectionClass\\) does not accept ReflectionClass\\.$#' 167 | identifier: assign.propertyType 168 | count: 1 169 | path: src/Support/Laravel/LivewireComponentParser.php 170 | --------------------------------------------------------------------------------