Skip to content

Commit

Permalink
Further work on solutions
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvanassche committed Jun 10, 2024
1 parent 04fcdb7 commit 451cc0d
Show file tree
Hide file tree
Showing 37 changed files with 854 additions and 37 deletions.
66 changes: 41 additions & 25 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
name: Tests
name: Run tests

on:
push:
paths:
- '**.php'
- '.github/workflows/run-tests.yml'
- 'phpunit.xml.dist'
- 'composer.json'
- 'composer.lock'
on: [push, pull_request]

jobs:
test:
php-tests:
runs-on: ${{ matrix.os }}
timeout-minutes: 5

strategy:
fail-fast: true
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.3, 8.2, 8.1]
php: [8.0, 8.1, 8.2, 8.3]
laravel: [9.*, 10.*, 11.*]
livewire: [2.11, 3.3.5]
stability: [prefer-lowest, prefer-stable]
os: [ubuntu-latest, windows-latest]
exclude:
- php: 8.0
laravel: 10.*
- php: 8.0
laravel: 11.*
- php: 8.1
laravel: 11.*
- laravel: 9.*
livewire: 3.3.5
- laravel: 11.*
livewire: 2.11

name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
name: P${{ matrix.php }} - L${{ matrix.laravel }} - LW${{ matrix.livewire }} -${{ matrix.stability }} - ${{ matrix.os }}

steps:
- name: Checkout code
Expand All @@ -30,19 +36,29 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
extensions: mbstring, fileinfo, pdo_sqlite
coverage: none
tools: composer:v2

- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"

- name: Cache composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-php-${{ matrix.php }}-laravel-${{ matrix.laravel }}-composer-${{ matrix.stability }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ matrix.os }}-php-${{ matrix.php }}-laravel-${{ matrix.laravel }}-composer-${{ matrix.stability }}-

- name: Install dependencies
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "nesbot/carbon:^2.72|^3.0" --no-interaction --no-update
composer update --with="livewire/livewire:^${{ matrix.livewire }}" --${{ matrix.stability }} --prefer-dist --no-interaction --no-suggest
- name: List Installed Dependencies
run: composer show -D
- name: Install extra package
if: matrix.php != '8.0'
run: composer require openai-php/client

- name: Execute tests
run: vendor/bin/pest --ci
run: vendor/bin/pest
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![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)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/error-solutions.svg?style=flat-square)](https://packagist.org/packages/spatie/error-solutions)

This is where your description should go. Try and limit it to a paragraph or two. Consider adding a small example.
At Spatie we develop multiple packages handling errors and providing solutions for these errors. This package is a collection of all these solutions.

## Support us

Expand All @@ -24,10 +24,10 @@ composer require spatie/error-solutions

## Usage

```php
$skeleton = new Spatie\ErrorSolutions();
echo $skeleton->echoPhrase('Hello, Spatie!');
```
We've got some excellent documentation on how to use solutions:

- [Flare](https://flareapp.io/docs/ignition/solutions/implementing-solutions)
- [Ignition](https://github.com/spatie/ignition/?tab=readme-ov-file#displaying-solutions)

## Testing

Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
},
"require-dev" : {
"livewire/livewire": "^2.11|^3.3.5",
"illuminate/support": "^10.0|^11.0",
"illuminate/broadcasting" : "^10.0|^11.0",
"illuminate/support": "^9.0|^10.0|^11.0",
"illuminate/broadcasting" : "^9.0|^10.0|^11.0",
"openai-php/client": "^0.10.1",
"illuminate/cache" : "^9.52|^10.0|^11.0",
"laravel/pint" : "^1.0",
Expand All @@ -30,7 +30,8 @@
"spatie/ray" : "^1.28",
"symfony/cache" : "^5.4|^6.0|^7.0",
"symfony/process" : "^5.4|^6.0|^7.0",
"vlucas/phpdotenv" : "^5.5"
"vlucas/phpdotenv" : "^5.5",
"orchestra/testbench": "^7.0|8.22.3|^9.0"
},
"autoload" : {
"psr-4" : {
Expand Down
25 changes: 25 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,28 @@ parameters:
message: "#^Method Spatie\\\\ErrorSolutions\\\\Support\\\\Renderer\\:\\:renderAsString\\(\\) should return string but returns string\\|false\\.$#"
count: 1
path: src/Support/Renderer.php

-
message: "#^Parameter \\#1 \\$missingView of method Spatie\\\\ErrorSolutions\\\\Laravel\\\\Solutions\\\\SolutionProviders\\\\ViewNotFoundSolutionProvider\\:\\:findRelatedView\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/Laravel/Solutions/SolutionProviders/ViewNotFoundSolutionProvider.php

-
message: "#^Method Spatie\\\\ErrorSolutions\\\\Laravel\\\\Solutions\\\\SolutionProviders\\\\UnknownValidationSolutionProvider\\:\\:getAvailableMethods\\(\\) return type with generic class Illuminate\\\\Support\\\\Collection does not specify its types\\: TKey, TValue$#"
count: 1
path: src/Laravel/Solutions/SolutionProviders/UnknownValidationSolutionProvider.php

-
message: "#^Parameter \\#1 \\$callback of method Illuminate\\\\Support\\\\Collection\\<int,ReflectionMethod\\>\\:\\:filter\\(\\) expects \\(callable\\(ReflectionMethod, int\\)\\: bool\\)\\|null, Closure\\(ReflectionMethod\\)\\: \\(0\\|1\\|false\\) given\\.$#"
count: 1
path: src/Laravel/Solutions/SolutionProviders/UnknownValidationSolutionProvider.php

-
message: "#^Unable to resolve the template type TMakeKey in call to method static method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),mixed\\>\\:\\:make\\(\\)$#"
count: 1
path: src/Laravel/Solutions/SolutionProviders/UnknownValidationSolutionProvider.php

-
message: "#^Unable to resolve the template type TMakeValue in call to method static method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),mixed\\>\\:\\:make\\(\\)$#"
count: 1
path: src/Laravel/Solutions/SolutionProviders/UnknownValidationSolutionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class SolutionProviderRepository implements SolutionProviderRepositoryContract
{
/**
* @param array<int, ProvidesSolution> $solutionProviders
* @var Collection<int, ProvidesSolution> $solutionProviders
*/
protected Collection $solutionProviders;

Expand Down Expand Up @@ -50,7 +50,6 @@ public function getSolutionsForThrowable(Throwable $throwable): array
$solutions[] = $throwable->getSolution();
}

/** @phpstan-ignore-next-line */
$providedSolutions = $this->solutionProviders
->filter(function (string $solutionClass) {

Check failure on line 54 in src/Laravel/Solutions/SolutionProviders/SolutionProviderRepository.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $callback of method Illuminate\Support\Collection<int,Spatie\ErrorSolutions\Contracts\ProvidesSolution>::filter() expects (callable(Spatie\ErrorSolutions\Contracts\ProvidesSolution, int): bool)|null, Closure(string): bool given.
if (! in_array(HasSolutionsForThrowable::class, class_implements($solutionClass) ?: [])) {
Expand Down
1 change: 0 additions & 1 deletion src/Laravel/Support/LivewireComponentParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use function Spatie\LaravelIgnition\Support\app;

class LivewireComponentParser
{
Expand Down
80 changes: 80 additions & 0 deletions tests/Laravel/ExceptionSolutionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

use Illuminate\Foundation\Auth\User;
use Spatie\ErrorSolutions\Tests\Laravel\Exceptions\AlwaysFalseSolutionProvider;
use Spatie\ErrorSolutions\Tests\Laravel\Exceptions\AlwaysTrueSolutionProvider;
use Spatie\ErrorSolutions\Contracts\BaseSolution;
use Spatie\ErrorSolutions\Solutions\SolutionProviders\BadMethodCallSolutionProvider;
use Spatie\ErrorSolutions\Solutions\SolutionProviders\SolutionProviderRepository;
use Spatie\ErrorSolutions\Laravel\Solutions\SolutionProviders\MissingAppKeySolutionProvider;

it('returns possible solutions', function () {
$repository = new SolutionProviderRepository();

$repository->registerSolutionProvider(AlwaysTrueSolutionProvider::class);
$repository->registerSolutionProvider(AlwaysFalseSolutionProvider::class);

$solutions = $repository->getSolutionsForThrowable(new Exception());

$this->assertNotNull($solutions);
expect($solutions)->toHaveCount(1);
expect($solutions[0] instanceof BaseSolution)->toBeTrue();
});

it('returns possible solutions when registered together', function () {
$repository = new SolutionProviderRepository();

$repository->registerSolutionProviders([
AlwaysTrueSolutionProvider::class,
AlwaysFalseSolutionProvider::class,
]);

$solutions = $repository->getSolutionsForThrowable(new Exception());

$this->assertNotNull($solutions);
expect($solutions)->toHaveCount(1);
expect($solutions[0] instanceof BaseSolution)->toBeTrue();
});

it('can suggest bad method call exceptions', function () {
if (version_compare(app()->version(), '5.6.3', '<')) {
$this->markTestSkipped('Laravel version < 5.6.3 do not support bad method call solutions');
}

try {
collect([])->faltten();
} catch (Exception $exception) {
$solution = new BadMethodCallSolutionProvider();

expect($solution->canSolve($exception))->toBeTrue();
}
});

it('can propose a solution for bad method call exceptions on collections', function () {
try {
collect([])->frist(fn ($item) => null);
} catch (Exception $exception) {
$solution = new BadMethodCallSolutionProvider();

expect($solution->getSolutions($exception)[0]->getSolutionDescription())->toBe('Did you mean Illuminate\Support\Collection::first() ?');
}
});

it('can propose a solution for bad method call exceptions on models', function () {
try {
$user = new User();
$user->sarve();
} catch (Exception $exception) {
$solution = new BadMethodCallSolutionProvider();

expect($solution->getSolutions($exception)[0]->getSolutionDescription())->toBe('Did you mean Illuminate\Foundation\Auth\User::save() ?');
}
});

it('can propose a solution for missing app key exceptions', function () {
$exception = new RuntimeException('No application encryption key has been specified.');

$solution = new MissingAppKeySolutionProvider();

expect($solution->getSolutions($exception)[0]->getSolutionActionDescription())->toBe('Generate your application encryption key using `php artisan key:generate`.');
});
20 changes: 20 additions & 0 deletions tests/Laravel/Exceptions/AlwaysFalseSolutionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Spatie\ErrorSolutions\Tests\Laravel\Exceptions;

use Spatie\ErrorSolutions\Contracts\BaseSolution;
use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable;
use Throwable;

class AlwaysFalseSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return false;
}

public function getSolutions(Throwable $throwable): array
{
return [new BaseSolution('Base Solution')];
}
}
20 changes: 20 additions & 0 deletions tests/Laravel/Exceptions/AlwaysTrueSolutionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Spatie\ErrorSolutions\Tests\Laravel\Exceptions;

use Spatie\ErrorSolutions\Contracts\BaseSolution;
use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable;
use Throwable;

class AlwaysTrueSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return true;
}

public function getSolutions(Throwable $throwable): array
{
return [new BaseSolution('Base Solution')];
}
}
48 changes: 48 additions & 0 deletions tests/Laravel/Solutions/InvalidRouteActionSolutionProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Spatie\ErrorSolutions\Tests\Laravel\stubs\Controllers\TestTypoController;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\InvalidRouteActionSolutionProvider;
use Spatie\ErrorSolutions\Laravel\Support\Composer\ComposerClassMap;

beforeEach(function () {
app()->bind(
ComposerClassMap::class,
function () {
return new ComposerClassMap(__DIR__.'/../../../vendor/autoload.php');
}
);
});

it('can solve the exception', function () {
$canSolve = app(InvalidRouteActionSolutionProvider::class)->canSolve(getInvalidRouteActionException());

expect($canSolve)->toBeTrue();
});

it('can recommend changing the routes method', function () {
Route::get('/test', TestTypoController::class);

/** @var \Spatie\Ignition\Contracts\Solution $solution */
$solution = app(InvalidRouteActionSolutionProvider::class)->getSolutions(getInvalidRouteActionException())[0];

expect(Str::contains($solution->getSolutionDescription(), 'Did you mean `TestTypoController`'))->toBeTrue();
});

it('wont recommend another controller class if the names are too different', function () {
Route::get('/test', TestTypoController::class);

$invalidController = 'UnrelatedTestTypoController';

/** @var \Spatie\Ignition\Contracts\Solution $solution */
$solution = app(InvalidRouteActionSolutionProvider::class)->getSolutions(getInvalidRouteActionException($invalidController))[0];

expect(Str::contains($solution->getSolutionDescription(), 'Did you mean `TestTypoController`'))->toBeFalse();
});

// Helpers
function getInvalidRouteActionException(string $controller = 'TestTypooController'): UnexpectedValueException
{
return new UnexpectedValueException("Invalid route action: [{$controller}]");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Foundation\Auth\User;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\LazyLoadingViolationSolutionProvider;

it('can solve lazy loading violations', function () {
$canSolve = app(LazyLoadingViolationSolutionProvider::class)
->canSolve(new LazyLoadingViolationException(new User(), 'posts'));

expect($canSolve)->toBeTrue();

$canSolve = app(LazyLoadingViolationSolutionProvider::class)
->canSolve(new Exception('generic exception'));

expect($canSolve)->toBeFalse();
});

// Helpers
function it_can_provide_the_solution_for_lazy_loading_exceptions()
{
$solutions = app(LazyLoadingViolationSolutionProvider::class)
->getSolutions(new LazyLoadingViolationException(new User(), 'posts'));

expect($solutions)->toHaveCount(1);
}
Loading

0 comments on commit 451cc0d

Please sign in to comment.