first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,50 @@
# Ignition: a beautiful error page for Laravel apps
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-ignition.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-ignition)
![Tests](https://github.com/spatie/laravel-ignition/workflows/Run%20tests/badge.svg)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-ignition.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-ignition)
[Ignition](https://flareapp.io/docs/ignition-for-laravel/introduction) is a beautiful and customizable error page for Laravel applications. It is the default error page for new Laravel applications. It also allows to publicly share your errors on [Flare](https://flareapp.io). If configured with a valid Flare API key, your errors in production applications will be tracked, and you'll get notified when they happen.
`spatie/laravel-ignition` works for Laravel 8 and 9 applications running on PHP 8.0 and above. Looking for Ignition for Laravel 5.x, 6.x or 7.x or old PHP versions? `facade/ignition` is still compatible.
![Screenshot of ignition](https://spatie.github.io/laravel-ignition/images/screenshot.png)
## Are you a visual learner?
In [this video on YouTube](https://youtu.be/LEY0N0Bteew?t=739), you'll see a demo of all of the features.
Do know more about the design decisions we made, read [this blog post](https://freek.dev/2168-ignition-the-most-beautiful-error-page-for-laravel-and-php-got-a-major-redesign).
## Official Documentation
The official documentation for Ignition can be found on the [Flare website](https://flareapp.io/docs/ignition/introducing-ignition/overview).
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-ignition.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-ignition)
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).
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).
### Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Credits
- [Spatie](https://spatie.be)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@ -0,0 +1,92 @@
{
"name": "spatie/laravel-ignition",
"description": "A beautiful error page for Laravel applications.",
"keywords": [
"error",
"page",
"laravel",
"flare"
],
"authors": [
{
"name": "Spatie",
"email": "info@spatie.be",
"role": "Developer"
}
],
"homepage": "https://flareapp.io/ignition",
"license": "MIT",
"require": {
"php": "^8.1",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"illuminate/support": "^10.0|^11.0",
"spatie/flare-client-php": "^1.3.5",
"spatie/ignition": "^1.13.2",
"symfony/console": "^6.2.3|^7.0",
"symfony/var-dumper": "^6.2.3|^7.0"
},
"require-dev": {
"livewire/livewire": "^2.11|^3.3.5",
"mockery/mockery": "^1.5.1",
"openai-php/client": "^0.8.1",
"orchestra/testbench": "^8.0|^9.0",
"pestphp/pest": "^2.30",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan-deprecation-rules": "^1.1.1",
"phpstan/phpstan-phpunit": "^1.3.3",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"psr/simple-cache-implementation": "Needed to cache solutions from OpenAI"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"phpstan/extension-installer": true,
"pestphp/pest-plugin": true,
"php-http/discovery": false
}
},
"extra": {
"laravel": {
"providers": [
"Spatie\\LaravelIgnition\\IgnitionServiceProvider"
],
"aliases": {
"Flare": "Spatie\\LaravelIgnition\\Facades\\Flare"
}
}
},
"autoload": {
"psr-4": {
"Spatie\\LaravelIgnition\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Spatie\\LaravelIgnition\\Tests\\": "tests"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"analyse": "vendor/bin/phpstan analyse",
"baseline": "vendor/bin/phpstan --generate-baseline",
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"support": {
"issues": "https://github.com/spatie/laravel-ignition/issues",
"forum": "https://twitter.com/flareappio",
"source": "https://github.com/spatie/laravel-ignition",
"docs": "https://flareapp.io/docs/ignition-for-laravel/introduction"
}
}

View File

@ -0,0 +1,87 @@
<?php
use Spatie\FlareClient\FlareMiddleware\AddGitInformation;
use Spatie\FlareClient\FlareMiddleware\RemoveRequestIp;
use Spatie\FlareClient\FlareMiddleware\CensorRequestBodyFields;
use Spatie\FlareClient\FlareMiddleware\CensorRequestHeaders;
use Spatie\LaravelIgnition\FlareMiddleware\AddDumps;
use Spatie\LaravelIgnition\FlareMiddleware\AddEnvironmentInformation;
use Spatie\LaravelIgnition\FlareMiddleware\AddExceptionInformation;
use Spatie\LaravelIgnition\FlareMiddleware\AddJobs;
use Spatie\LaravelIgnition\FlareMiddleware\AddLogs;
use Spatie\LaravelIgnition\FlareMiddleware\AddQueries;
use Spatie\LaravelIgnition\FlareMiddleware\AddContext;
use Spatie\LaravelIgnition\FlareMiddleware\AddNotifierName;
return [
/*
|
|--------------------------------------------------------------------------
| Flare API key
|--------------------------------------------------------------------------
|
| Specify Flare's API key below to enable error reporting to the service.
|
| More info: https://flareapp.io/docs/general/projects
|
*/
'key' => env('FLARE_KEY'),
/*
|--------------------------------------------------------------------------
| Middleware
|--------------------------------------------------------------------------
|
| These middleware will modify the contents of the report sent to Flare.
|
*/
'flare_middleware' => [
RemoveRequestIp::class,
AddGitInformation::class,
AddNotifierName::class,
AddEnvironmentInformation::class,
AddExceptionInformation::class,
AddDumps::class,
AddLogs::class => [
'maximum_number_of_collected_logs' => 200,
],
AddQueries::class => [
'maximum_number_of_collected_queries' => 200,
'report_query_bindings' => true,
],
AddJobs::class => [
'max_chained_job_reporting_depth' => 5,
],
AddContext::class,
CensorRequestBodyFields::class => [
'censor_fields' => [
'password',
'password_confirmation',
],
],
CensorRequestHeaders::class => [
'headers' => [
'API-KEY',
'Authorization',
'Cookie',
'Set-Cookie',
'X-CSRF-TOKEN',
'X-XSRF-TOKEN',
]
]
],
/*
|--------------------------------------------------------------------------
| Reporting log statements
|--------------------------------------------------------------------------
|
| If this setting is `false` log statements won't be sent as events to Flare,
| no matter which error level you specified in the Flare log channel.
|
*/
'send_logs_as_events' => true,
];

View File

@ -0,0 +1,285 @@
<?php
use Spatie\Ignition\Solutions\SolutionProviders\BadMethodCallSolutionProvider;
use Spatie\Ignition\Solutions\SolutionProviders\MergeConflictSolutionProvider;
use Spatie\Ignition\Solutions\SolutionProviders\UndefinedPropertySolutionProvider;
use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder;
use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder;
use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder;
use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\DefaultDbNameSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\GenericLaravelExceptionSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\IncorrectValetDbCredentialsSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\InvalidRouteActionSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingAppKeySolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingColumnSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingImportSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingLivewireComponentSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingMixManifestSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingViteManifestSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\RunningLaravelDuskInProductionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\TableNotFoundSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UndefinedViewVariableSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UnknownValidationSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\ViewNotFoundSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\OpenAiSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\SailNetworkSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UnknownMariadbCollationSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UnknownMysql8CollationSolutionProvider;
return [
/*
|--------------------------------------------------------------------------
| Editor
|--------------------------------------------------------------------------
|
| Choose your preferred editor to use when clicking any edit button.
|
| Supported: "phpstorm", "vscode", "vscode-insiders", "textmate", "emacs",
| "sublime", "atom", "nova", "macvim", "idea", "netbeans",
| "xdebug", "phpstorm-remote"
|
*/
'editor' => env('IGNITION_EDITOR', 'phpstorm'),
/*
|--------------------------------------------------------------------------
| Theme
|--------------------------------------------------------------------------
|
| Here you may specify which theme Ignition should use.
|
| Supported: "light", "dark", "auto"
|
*/
'theme' => env('IGNITION_THEME', 'auto'),
/*
|--------------------------------------------------------------------------
| Sharing
|--------------------------------------------------------------------------
|
| You can share local errors with colleagues or others around the world.
| Sharing is completely free and doesn't require an account on Flare.
|
| If necessary, you can completely disable sharing below.
|
*/
'enable_share_button' => env('IGNITION_SHARING_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Register Ignition commands
|--------------------------------------------------------------------------
|
| Ignition comes with an additional make command that lets you create
| new solution classes more easily. To keep your default Laravel
| installation clean, this command is not registered by default.
|
| You can enable the command registration below.
|
*/
'register_commands' => env('REGISTER_IGNITION_COMMANDS', false),
/*
|--------------------------------------------------------------------------
| Solution Providers
|--------------------------------------------------------------------------
|
| List of solution providers that should be loaded. You may specify additional
| providers as fully qualified class names.
|
*/
'solution_providers' => [
// from spatie/ignition
BadMethodCallSolutionProvider::class,
MergeConflictSolutionProvider::class,
UndefinedPropertySolutionProvider::class,
// from spatie/laravel-ignition
IncorrectValetDbCredentialsSolutionProvider::class,
MissingAppKeySolutionProvider::class,
DefaultDbNameSolutionProvider::class,
TableNotFoundSolutionProvider::class,
MissingImportSolutionProvider::class,
InvalidRouteActionSolutionProvider::class,
ViewNotFoundSolutionProvider::class,
RunningLaravelDuskInProductionProvider::class,
MissingColumnSolutionProvider::class,
UnknownValidationSolutionProvider::class,
MissingMixManifestSolutionProvider::class,
MissingViteManifestSolutionProvider::class,
MissingLivewireComponentSolutionProvider::class,
UndefinedViewVariableSolutionProvider::class,
GenericLaravelExceptionSolutionProvider::class,
OpenAiSolutionProvider::class,
SailNetworkSolutionProvider::class,
UnknownMysql8CollationSolutionProvider::class,
UnknownMariadbCollationSolutionProvider::class,
],
/*
|--------------------------------------------------------------------------
| Ignored Solution Providers
|--------------------------------------------------------------------------
|
| You may specify a list of solution providers (as fully qualified class
| names) that shouldn't be loaded. Ignition will ignore these classes
| and possible solutions provided by them will never be displayed.
|
*/
'ignored_solution_providers' => [
],
/*
|--------------------------------------------------------------------------
| Runnable Solutions
|--------------------------------------------------------------------------
|
| Some solutions that Ignition displays are runnable and can perform
| various tasks. By default, runnable solutions are only enabled when your
| app has debug mode enabled and the environment is `local` or
| `development`.
|
| Using the `IGNITION_ENABLE_RUNNABLE_SOLUTIONS` environment variable, you
| can override this behaviour and enable or disable runnable solutions
| regardless of the application's environment.
|
| Default: env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS')
|
*/
'enable_runnable_solutions' => env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS'),
/*
|--------------------------------------------------------------------------
| Remote Path Mapping
|--------------------------------------------------------------------------
|
| If you are using a remote dev server, like Laravel Homestead, Docker, or
| even a remote VPS, it will be necessary to specify your path mapping.
|
| Leaving one, or both of these, empty or null will not trigger the remote
| URL changes and Ignition will treat your editor links as local files.
|
| "remote_sites_path" is an absolute base path for your sites or projects
| in Homestead, Vagrant, Docker, or another remote development server.
|
| Example value: "/home/vagrant/Code"
|
| "local_sites_path" is an absolute base path for your sites or projects
| on your local computer where your IDE or code editor is running on.
|
| Example values: "/Users/<name>/Code", "C:\Users\<name>\Documents\Code"
|
*/
'remote_sites_path' => env('IGNITION_REMOTE_SITES_PATH', base_path()),
'local_sites_path' => env('IGNITION_LOCAL_SITES_PATH', ''),
/*
|--------------------------------------------------------------------------
| Housekeeping Endpoint Prefix
|--------------------------------------------------------------------------
|
| Ignition registers a couple of routes when it is enabled. Below you may
| specify a route prefix that will be used to host all internal links.
|
*/
'housekeeping_endpoint_prefix' => '_ignition',
/*
|--------------------------------------------------------------------------
| Settings File
|--------------------------------------------------------------------------
|
| Ignition allows you to save your settings to a specific global file.
|
| If no path is specified, a file with settings will be saved to the user's
| home directory. The directory depends on the OS and its settings but it's
| typically `~/.ignition.json`. In this case, the settings will be applied
| to all of your projects where Ignition is used and the path is not
| specified.
|
| However, if you want to store your settings on a project basis, or you
| want to keep them in another directory, you can specify a path where
| the settings file will be saved. The path should be an existing directory
| with correct write access.
| For example, create a new `ignition` folder in the storage directory and
| use `storage_path('ignition')` as the `settings_file_path`.
|
| Default value: '' (empty string)
*/
'settings_file_path' => '',
/*
|--------------------------------------------------------------------------
| Recorders
|--------------------------------------------------------------------------
|
| Ignition registers a couple of recorders when it is enabled. Below you may
| specify a recorders will be used to record specific events.
|
*/
'recorders' => [
DumpRecorder::class,
JobRecorder::class,
LogRecorder::class,
QueryRecorder::class,
],
/*
* When a key is set, we'll send your exceptions to Open AI to generate a solution
*/
'open_ai_key' => env('IGNITION_OPEN_AI_KEY'),
/*
|--------------------------------------------------------------------------
| Include arguments
|--------------------------------------------------------------------------
|
| Ignition show you stack traces of exceptions with the arguments that were
| passed to each method. This feature can be disabled here.
|
*/
'with_stack_frame_arguments' => true,
/*
|--------------------------------------------------------------------------
| Argument reducers
|--------------------------------------------------------------------------
|
| Ignition show you stack traces of exceptions with the arguments that were
| passed to each method. To make these variables more readable, you can
| specify a list of classes here which summarize the variables.
|
*/
'argument_reducers' => [
\Spatie\Backtrace\Arguments\Reducers\BaseTypeArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\StdClassArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\EnumArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\ClosureArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\DateTimeArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\DateTimeZoneArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\SymphonyRequestArgumentReducer::class,
\Spatie\LaravelIgnition\ArgumentReducers\ModelArgumentReducer::class,
\Spatie\LaravelIgnition\ArgumentReducers\CollectionArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\StringableArgumentReducer::class,
],
];

View File

@ -0,0 +1,20 @@
<?php
namespace Spatie\LaravelIgnition\ArgumentReducers;
use Illuminate\Support\Collection;
use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract;
use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument;
use Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer;
class CollectionArgumentReducer extends ArrayArgumentReducer
{
public function execute(mixed $argument): ReducedArgumentContract
{
if (! $argument instanceof Collection) {
return UnReducedArgument::create();
}
return $this->reduceArgument($argument->toArray(), get_class($argument));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Spatie\LaravelIgnition\ArgumentReducers;
use Illuminate\Database\Eloquent\Model;
use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument;
use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract;
use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument;
use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer;
class ModelArgumentReducer implements ArgumentReducer
{
public function execute(mixed $argument): ReducedArgumentContract
{
if (! $argument instanceof Model) {
return UnReducedArgument::create();
}
return new ReducedArgument(
"{$argument->getKeyName()}:{$argument->getKey()}",
get_class($argument)
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Commands;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class SolutionMakeCommand extends GeneratorCommand
{
protected $name = 'ignition:make-solution';
protected $description = 'Create a new custom Ignition solution class';
protected $type = 'Solution';
protected function getStub(): string
{
return $this->option('runnable')
? __DIR__.'/stubs/runnable-solution.stub'
: __DIR__.'/stubs/solution.stub';
}
protected function getDefaultNamespace($rootNamespace)
{
return "{$rootNamespace}\\Solutions";
}
/** @return array<int, mixed> */
protected function getOptions(): array
{
return [
['runnable', null, InputOption::VALUE_NONE, 'Create runnable solution'],
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Spatie\LaravelIgnition\Commands;
use Illuminate\Console\GeneratorCommand;
class SolutionProviderMakeCommand extends GeneratorCommand
{
protected $name = 'ignition:make-solution-provider';
protected $description = 'Create a new custom Ignition solution provider class';
protected $type = 'Solution Provider';
protected function getStub(): string
{
return __DIR__.'/stubs/solution-provider.stub';
}
protected function getDefaultNamespace($rootNamespace)
{
return "{$rootNamespace}\\SolutionProviders";
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Spatie\LaravelIgnition\Commands;
use Composer\InstalledVersions;
use Exception;
use Illuminate\Config\Repository;
use Illuminate\Console\Command;
use Illuminate\Log\LogManager;
use Spatie\FlareClient\Flare;
use Spatie\FlareClient\Http\Exceptions\BadResponseCode;
class TestCommand extends Command
{
protected $signature = 'flare:test';
protected $description = 'Send a test notification to Flare';
protected Repository $config;
public function handle(Repository $config): void
{
$this->config = $config;
$this->checkFlareKey();
if (app()->make('log') instanceof LogManager) {
$this->checkFlareLogger();
}
$this->sendTestException();
}
protected function checkFlareKey(): self
{
$message = empty($this->config->get('flare.key'))
? '❌ Flare key not specified. Make sure you specify a value in the `key` key of the `flare` config file.'
: '✅ Flare key specified';
$this->info($message);
return $this;
}
public function checkFlareLogger(): self
{
$defaultLogChannel = $this->config->get('logging.default');
$activeStack = $this->config->get("logging.channels.{$defaultLogChannel}");
if (is_null($activeStack)) {
$this->info("❌ The default logging channel `{$defaultLogChannel}` is not configured in the `logging` config file");
}
if (! isset($activeStack['channels']) || ! in_array('flare', $activeStack['channels'])) {
$this->info("❌ The logging channel `{$defaultLogChannel}` does not contain the 'flare' channel");
}
if (is_null($this->config->get('logging.channels.flare'))) {
$this->info('❌ There is no logging channel named `flare` in the `logging` config file');
}
if ($this->config->get('logging.channels.flare.driver') !== 'flare') {
$this->info('❌ The `flare` logging channel defined in the `logging` config file is not set to `flare`.');
}
if ($this->config->get('ignition.with_stack_frame_arguments') && ini_get('zend.exception_ignore_args')) {
$this->info('⚠️ The `zend.exception_ignore_args` php ini setting is enabled. This will prevent Flare from showing stack trace arguments.');
}
$this->info('✅ The Flare logging driver was configured correctly.');
return $this;
}
protected function sendTestException(): void
{
$testException = new Exception('This is an exception to test if the integration with Flare works.');
try {
app(Flare::class)->sendTestReport($testException);
$this->info('');
} catch (Exception $exception) {
$this->warn('❌ We were unable to send an exception to Flare. ');
if ($exception instanceof BadResponseCode) {
$this->info('');
$message = 'Unknown error';
$body = $exception->response->getBody();
if (is_array($body) && isset($body['message'])) {
$message = $body['message'];
}
$this->warn("{$exception->response->getHttpResponseCode()} - {$message}");
} else {
$this->warn($exception->getMessage());
}
$this->warn('Make sure that your key is correct and that you have a valid subscription.');
$this->info('');
$this->info('For more info visit the docs on https://flareapp.io/docs/ignition-for-laravel/introduction');
$this->info('You can see the status page of Flare at https://status.flareapp.io');
$this->info('Flare support can be reached at support@flareapp.io');
$this->line('');
$this->line('Extra info');
$this->table([], [
['Platform', PHP_OS],
['PHP', phpversion()],
['Laravel', app()->version()],
['spatie/ignition', InstalledVersions::getVersion('spatie/ignition')],
['spatie/laravel-ignition', InstalledVersions::getVersion('spatie/laravel-ignition')],
['spatie/flare-client-php', InstalledVersions::getVersion('spatie/flare-client-php')],
/** @phpstan-ignore-next-line */
['Curl', curl_version()['version'] ?? 'Unknown'],
/** @phpstan-ignore-next-line */
['SSL', curl_version()['ssl_version'] ?? 'Unknown'],
]);
if ($this->output->isVerbose()) {
throw $exception;
}
return;
}
$this->info('We tried to send an exception to Flare. Please check if it arrived!');
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace DummyNamespace;
use Spatie\Ignition\Contracts\RunnableSolution;
class DummyClass implements RunnableSolution
{
public function getSolutionTitle(): string
{
return '';
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
return '';
}
public function getRunButtonText(): string
{
return '';
}
public function getSolutionDescription(): string
{
return '';
}
public function getRunParameters(): array
{
return [];
}
public function run(array $parameters = [])
{
//
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace DummyNamespace;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Throwable;
class DummyClass implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return false;
}
public function getSolutions(Throwable $throwable): array
{
return [];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace DummyNamespace;
use Spatie\Ignition\Contracts\Solution;
class DummyClass implements Solution
{
public function getSolutionTitle(): string
{
return '';
}
public function getSolutionDescription(): string
{
return '';
}
public function getDocumentationLinks(): array
{
return [];
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Spatie\LaravelIgnition\ContextProviders;
use Spatie\FlareClient\Context\ConsoleContextProvider;
class LaravelConsoleContextProvider extends ConsoleContextProvider
{
}

View File

@ -0,0 +1,31 @@
<?php
namespace Spatie\LaravelIgnition\ContextProviders;
use Illuminate\Http\Request;
use Livewire\LivewireManager;
use Spatie\FlareClient\Context\ContextProvider;
use Spatie\FlareClient\Context\ContextProviderDetector;
class LaravelContextProviderDetector implements ContextProviderDetector
{
public function detectCurrentContext(): ContextProvider
{
if (app()->runningInConsole()) {
return new LaravelConsoleContextProvider($_SERVER['argv'] ?? []);
}
$request = app(Request::class);
if ($this->isRunningLiveWire($request)) {
return new LaravelLivewireRequestContextProvider($request, app(LivewireManager::class));
}
return new LaravelRequestContextProvider($request);
}
protected function isRunningLiveWire(Request $request): bool
{
return $request->hasHeader('x-livewire') && $request->hasHeader('referer');
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace Spatie\LaravelIgnition\ContextProviders;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Livewire\LivewireManager;
class LaravelLivewireRequestContextProvider extends LaravelRequestContextProvider
{
public function __construct(
Request $request,
protected LivewireManager $livewireManager
) {
parent::__construct($request);
}
/** @return array<string, string> */
public function getRequest(): array
{
$properties = parent::getRequest();
$properties['method'] = $this->livewireManager->originalMethod();
$properties['url'] = $this->livewireManager->originalUrl();
return $properties;
}
/** @return array<int|string, mixed> */
public function toArray(): array
{
$properties = parent::toArray();
$properties['livewire'] = $this->getLivewireInformation();
return $properties;
}
/** @return array<string, mixed> */
protected function getLivewireInformation(): array
{
/** @phpstan-ignore-next-line */
$componentId = $this->request->input('fingerprint.id');
/** @phpstan-ignore-next-line */
$componentAlias = $this->request->input('fingerprint.name');
if ($componentAlias === null) {
return [];
}
try {
$componentClass = $this->livewireManager->getClass($componentAlias);
} catch (Exception $e) {
$componentClass = null;
}
return [
'component_class' => $componentClass,
'component_alias' => $componentAlias,
'component_id' => $componentId,
'data' => $this->resolveData(),
'updates' => $this->resolveUpdates(),
];
}
/** @return array<string, mixed> */
protected function resolveData(): array
{
/** @phpstan-ignore-next-line */
$data = $this->request->input('serverMemo.data') ?? [];
/** @phpstan-ignore-next-line */
$dataMeta = $this->request->input('serverMemo.dataMeta') ?? [];
foreach ($dataMeta['modelCollections'] ?? [] as $key => $value) {
$data[$key] = array_merge($data[$key] ?? [], $value);
}
foreach ($dataMeta['models'] ?? [] as $key => $value) {
$data[$key] = array_merge($data[$key] ?? [], $value);
}
return $data;
}
/** @return array<string, mixed> */
protected function resolveUpdates(): array
{
/** @phpstan-ignore-next-line */
$updates = $this->request->input('updates') ?? [];
return array_map(function (array $update) {
$update['payload'] = Arr::except($update['payload'] ?? [], ['id']);
return $update;
}, $updates);
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace Spatie\LaravelIgnition\ContextProviders;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request as LaravelRequest;
use Spatie\FlareClient\Context\RequestContextProvider;
use Symfony\Component\HttpFoundation\Request as SymphonyRequest;
use Throwable;
class LaravelRequestContextProvider extends RequestContextProvider
{
protected LaravelRequest|SymphonyRequest|null $request;
public function __construct(LaravelRequest $request)
{
$this->request = $request;
}
/** @return null|array<string, mixed> */
public function getUser(): array|null
{
try {
/** @var object|null $user */
/** @phpstan-ignore-next-line */
$user = $this->request?->user();
if (! $user) {
return null;
}
} catch (Throwable) {
return null;
}
try {
if (method_exists($user, 'toFlare')) {
return $user->toFlare();
}
if (method_exists($user, 'toArray')) {
return $user->toArray();
}
} catch (Throwable $e) {
return null;
}
return null;
}
/** @return null|array<string, mixed> */
public function getRoute(): array|null
{
/**
* @phpstan-ignore-next-line
* @var \Illuminate\Routing\Route|null $route
*/
$route = $this->request->route();
if (! $route) {
return null;
}
return [
'route' => $route->getName(),
'routeParameters' => $this->getRouteParameters(),
'controllerAction' => $route->getActionName(),
'middleware' => array_values($route->gatherMiddleware() ?? []),
];
}
/** @return array<int, mixed> */
protected function getRouteParameters(): array
{
try {
/** @phpstan-ignore-next-line */
return collect(optional($this->request->route())->parameters ?? [])
->map(fn ($parameter) => $parameter instanceof Model ? $parameter->withoutRelations() : $parameter)
->map(function ($parameter) {
return method_exists($parameter, 'toFlare') ? $parameter->toFlare() : $parameter;
})
->toArray();
} catch (Throwable) {
return [];
}
}
/** @return array<int, mixed> */
public function toArray(): array
{
$properties = parent::toArray();
if ($route = $this->getRoute()) {
$properties['route'] = $route;
}
if ($user = $this->getUser()) {
$properties['user'] = $user;
}
return $properties;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
use Symfony\Component\HttpKernel\Exception\HttpException;
class CannotExecuteSolutionForNonLocalIp extends HttpException implements ProvidesSolution
{
public static function make(): self
{
return new self(403, 'Solutions cannot be run from your current IP address.');
}
public function getSolution(): Solution
{
return BaseSolution::create()
->setSolutionTitle('Checking your environment settings')
->setSolutionDescription("Solutions can only be executed by requests from a local IP address. Keep in mind that `APP_DEBUG` should set to false on any production environment.");
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use Exception;
use Monolog\Level;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
class InvalidConfig extends Exception implements ProvidesSolution
{
public static function invalidLogLevel(string $logLevel): self
{
return new self("Invalid log level `{$logLevel}` specified.");
}
public function getSolution(): Solution
{
$validLogLevels = array_map(
fn (string $level) => strtolower($level),
array_keys(Level::VALUES)
);
$validLogLevelsString = implode(',', $validLogLevels);
return BaseSolution::create()
->setSolutionTitle('You provided an invalid log level')
->setSolutionDescription("Please change the log level in your `config/logging.php` file. Valid log levels are {$validLogLevelsString}.");
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use ErrorException;
use Spatie\FlareClient\Contracts\ProvidesFlareContext;
use Spatie\LaravelIgnition\Recorders\DumpRecorder\HtmlDumper;
class ViewException extends ErrorException implements ProvidesFlareContext
{
/** @var array<string, mixed> */
protected array $viewData = [];
protected string $view = '';
/**
* @param array<string, mixed> $data
*
* @return void
*/
public function setViewData(array $data): void
{
$this->viewData = $data;
}
/** @return array<string, mixed> */
public function getViewData(): array
{
return $this->viewData;
}
public function setView(string $path): void
{
$this->view = $path;
}
protected function dumpViewData(mixed $variable): string
{
return (new HtmlDumper())->dumpVariable($variable);
}
/** @return array<string, mixed> */
public function context(): array
{
$context = [
'view' => [
'view' => $this->view,
],
];
$context['view']['data'] = array_map([$this, 'dumpViewData'], $this->viewData);
return $context;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
class ViewExceptionWithSolution extends ViewException implements ProvidesSolution
{
protected Solution $solution;
public function setSolution(Solution $solution): void
{
$this->solution = $solution;
}
public function getSolution(): Solution
{
return $this->solution;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Spatie\LaravelIgnition\Facades;
use Illuminate\Support\Facades\Facade;
use Spatie\LaravelIgnition\Support\SentReports;
/**
* @method static void glow(string $name, string $messageLevel = \Spatie\FlareClient\Enums\MessageLevels::INFO, array $metaData = [])
* @method static void context($key, $value)
* @method static void group(string $groupName, array $properties)
*
* @see \Spatie\FlareClient\Flare
*/
class Flare extends Facade
{
protected static function getFacadeAccessor()
{
return \Spatie\FlareClient\Flare::class;
}
public static function sentReports(): SentReports
{
return app(SentReports::class);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Closure;
use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Context;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
class AddContext implements FlareMiddleware
{
public function handle(Report $report, Closure $next)
{
if (! class_exists(Repository::class)) {
return $report;
}
$allContext = Context::all();
if (count($allContext)) {
$report->group('laravel_context', $allContext);
}
return $report;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Closure;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder;
class AddDumps implements FlareMiddleware
{
protected DumpRecorder $dumpRecorder;
public function __construct()
{
$this->dumpRecorder = app(DumpRecorder::class);
}
public function handle(Report $report, Closure $next)
{
$report->group('dumps', $this->dumpRecorder->getDumps());
return $next($report);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Closure;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
class AddEnvironmentInformation implements FlareMiddleware
{
public function handle(Report $report, Closure $next)
{
$report->frameworkVersion(app()->version());
$report->group('env', [
'laravel_version' => app()->version(),
'laravel_locale' => app()->getLocale(),
'laravel_config_cached' => app()->configurationIsCached(),
'app_debug' => config('app.debug'),
'app_env' => config('app.env'),
'php_version' => phpversion(),
]);
return $next($report);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Illuminate\Database\QueryException;
use Spatie\FlareClient\Contracts\ProvidesFlareContext;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
class AddExceptionInformation implements FlareMiddleware
{
public function handle(Report $report, $next)
{
$throwable = $report->getThrowable();
$this->addUserDefinedContext($report);
if (! $throwable instanceof QueryException) {
return $next($report);
}
$report->group('exception', [
'raw_sql' => $throwable->getSql(),
]);
return $next($report);
}
private function addUserDefinedContext(Report $report): void
{
$throwable = $report->getThrowable();
if ($throwable === null) {
return;
}
if ($throwable instanceof ProvidesFlareContext) {
// ProvidesFlareContext writes directly to context groups and is handled in the flare-client-php package.
return;
}
if (! method_exists($throwable, 'context')) {
return;
}
$context = $throwable->context();
if (! is_array($context)) {
return;
}
$exceptionContextGroup = [];
foreach ($context as $key => $value) {
$exceptionContextGroup[$key] = $value;
}
$report->group('exception', $exceptionContextGroup);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder;
class AddJobs implements FlareMiddleware
{
protected JobRecorder $jobRecorder;
public function __construct()
{
$this->jobRecorder = app(JobRecorder::class);
}
public function handle(Report $report, $next)
{
if ($job = $this->jobRecorder->getJob()) {
$report->group('job', $job);
}
return $next($report);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder;
class AddLogs implements FlareMiddleware
{
protected LogRecorder $logRecorder;
public function __construct()
{
$this->logRecorder = app(LogRecorder::class);
}
public function handle(Report $report, $next)
{
$report->group('logs', $this->logRecorder->getLogMessages());
return $next($report);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
class AddNotifierName implements FlareMiddleware
{
public const NOTIFIER_NAME = 'Laravel Client';
public function handle(Report $report, $next)
{
$report->notifierName(static::NOTIFIER_NAME);
return $next($report);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Spatie\FlareClient\Report;
use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder;
class AddQueries
{
protected QueryRecorder $queryRecorder;
public function __construct()
{
$this->queryRecorder = app(QueryRecorder::class);
}
public function handle(Report $report, $next)
{
$report->group('queries', $this->queryRecorder->getQueries());
return $next($report);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Spatie\LaravelIgnition\Http\Controllers;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Spatie\Ignition\Contracts\SolutionProviderRepository;
use Spatie\LaravelIgnition\Exceptions\CannotExecuteSolutionForNonLocalIp;
use Spatie\LaravelIgnition\Http\Requests\ExecuteSolutionRequest;
use Spatie\LaravelIgnition\Support\RunnableSolutionsGuard;
class ExecuteSolutionController
{
use ValidatesRequests;
public function __invoke(
ExecuteSolutionRequest $request,
SolutionProviderRepository $solutionProviderRepository
) {
$this
->ensureRunnableSolutionsEnabled()
->ensureLocalRequest();
$solution = $request->getRunnableSolution();
$solution->run($request->get('parameters', []));
return response()->noContent();
}
public function ensureRunnableSolutionsEnabled(): self
{
// Should already be checked in middleware but we want to be 100% certain.
abort_unless(RunnableSolutionsGuard::check(), 400);
return $this;
}
public function ensureLocalRequest(): self
{
$ipIsPublic = filter_var(
request()->ip(),
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
if ($ipIsPublic) {
throw CannotExecuteSolutionForNonLocalIp::make();
}
return $this;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelIgnition\Http\Controllers;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Str;
class HealthCheckController
{
public function __invoke()
{
return [
'can_execute_commands' => $this->canExecuteCommands(),
];
}
protected function canExecuteCommands(): bool
{
Artisan::call('help', ['--version']);
$output = Artisan::output();
return Str::contains($output, app()->version());
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Spatie\LaravelIgnition\Http\Controllers;
use Spatie\Ignition\Config\IgnitionConfig;
use Spatie\LaravelIgnition\Http\Requests\UpdateConfigRequest;
class UpdateConfigController
{
public function __invoke(UpdateConfigRequest $request)
{
$result = (new IgnitionConfig())->saveValues($request->validated());
return response()->json($result);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelIgnition\Http\Middleware;
use Closure;
use Spatie\LaravelIgnition\Support\RunnableSolutionsGuard;
class RunnableSolutionsEnabled
{
public function handle($request, Closure $next)
{
if (! RunnableSolutionsGuard::check()) {
abort(404);
}
return $next($request);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Spatie\LaravelIgnition\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Spatie\Ignition\Contracts\RunnableSolution;
use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Contracts\SolutionProviderRepository;
class ExecuteSolutionRequest extends FormRequest
{
public function rules(): array
{
return [
'solution' => 'required',
'parameters' => 'array',
];
}
public function getSolution(): Solution
{
$solution = app(SolutionProviderRepository::class)
->getSolutionForClass($this->get('solution'));
abort_if(is_null($solution), 404, 'Solution could not be found');
return $solution;
}
public function getRunnableSolution(): RunnableSolution
{
$solution = $this->getSolution();
if (! $solution instanceof RunnableSolution) {
abort(404, 'Runnable solution could not be found');
}
return $solution;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Spatie\LaravelIgnition\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateConfigRequest extends FormRequest
{
public function rules(): array
{
return [
'theme' => ['required', Rule::in(['light', 'dark', 'auto'])],
'editor' => ['required'],
'hide_solutions' => ['required', 'boolean'],
];
}
}

View File

@ -0,0 +1,349 @@
<?php
namespace Spatie\LaravelIgnition;
use Exception;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\ViewException;
use Laravel\Octane\Events\RequestReceived;
use Laravel\Octane\Events\RequestTerminated;
use Laravel\Octane\Events\TaskReceived;
use Laravel\Octane\Events\TickReceived;
use Monolog\Level;
use Monolog\Logger;
use Spatie\FlareClient\Flare;
use Spatie\FlareClient\FlareMiddleware\AddSolutions;
use Spatie\Ignition\Config\FileConfigManager;
use Spatie\Ignition\Config\IgnitionConfig;
use Spatie\Ignition\Contracts\ConfigManager;
use Spatie\Ignition\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract;
use Spatie\Ignition\Ignition;
use Spatie\LaravelIgnition\Commands\SolutionMakeCommand;
use Spatie\LaravelIgnition\Commands\SolutionProviderMakeCommand;
use Spatie\LaravelIgnition\Commands\TestCommand;
use Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector;
use Spatie\LaravelIgnition\Exceptions\InvalidConfig;
use Spatie\LaravelIgnition\FlareMiddleware\AddJobs;
use Spatie\LaravelIgnition\FlareMiddleware\AddLogs;
use Spatie\LaravelIgnition\FlareMiddleware\AddQueries;
use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder;
use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder;
use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder;
use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder;
use Spatie\LaravelIgnition\Renderers\IgnitionExceptionRenderer;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\SolutionProviderRepository;
use Spatie\LaravelIgnition\Support\FlareLogHandler;
use Spatie\LaravelIgnition\Support\SentReports;
use Spatie\LaravelIgnition\Views\ViewExceptionMapper;
class IgnitionServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->registerConfig();
$this->registerFlare();
$this->registerIgnition();
$this->registerRenderer();
$this->registerRecorders();
$this->registerLogHandler();
}
public function boot()
{
if ($this->app->runningInConsole()) {
$this->registerCommands();
$this->publishConfigs();
}
$this->registerRoutes();
$this->configureTinker();
$this->configureOctane();
$this->registerViewExceptionMapper();
$this->startRecorders();
$this->configureQueue();
}
protected function registerConfig(): void
{
$this->mergeConfigFrom(__DIR__ . '/../config/flare.php', 'flare');
$this->mergeConfigFrom(__DIR__ . '/../config/ignition.php', 'ignition');
}
protected function registerCommands(): void
{
if ($this->app['config']->get('flare.key')) {
$this->commands([
TestCommand::class,
]);
}
if ($this->app['config']->get('ignition.register_commands')) {
$this->commands([
SolutionMakeCommand::class,
SolutionProviderMakeCommand::class,
]);
}
}
protected function publishConfigs(): void
{
$this->publishes([
__DIR__ . '/../config/ignition.php' => config_path('ignition.php'),
], 'ignition-config');
$this->publishes([
__DIR__ . '/../config/flare.php' => config_path('flare.php'),
], 'flare-config');
}
protected function registerRenderer(): void
{
$this->app->bind(
'Illuminate\Contracts\Foundation\ExceptionRenderer',
fn (Application $app) => $app->make(IgnitionExceptionRenderer::class)
);
}
protected function registerFlare(): void
{
$this->app->singleton(Flare::class, function () {
return Flare::make()
->setApiToken(config('flare.key') ?? '')
->setBaseUrl(config('flare.base_url', 'https://flareapp.io/api'))
->applicationPath(base_path())
->setStage(app()->environment())
->setContextProviderDetector(new LaravelContextProviderDetector())
->registerMiddleware($this->getFlareMiddleware())
->registerMiddleware(new AddSolutions(new SolutionProviderRepository($this->getSolutionProviders())))
->argumentReducers(config('ignition.argument_reducers', []))
->withStackFrameArguments(config('ignition.with_stack_frame_arguments', true));
});
$this->app->singleton(SentReports::class);
}
protected function registerIgnition(): void
{
$this->app->singleton(
ConfigManager::class,
fn () => new FileConfigManager(config('ignition.settings_file_path', ''))
);
$ignitionConfig = (new IgnitionConfig())
->merge(config('ignition', []))
->loadConfigFile();
$solutionProviders = $this->getSolutionProviders();
$solutionProviderRepository = new SolutionProviderRepository($solutionProviders);
$this->app->singleton(IgnitionConfig::class, fn () => $ignitionConfig);
$this->app->singleton(SolutionProviderRepositoryContract::class, fn () => $solutionProviderRepository);
$this->app->singleton(
Ignition::class,
fn () => (new Ignition($this->app->make(Flare::class)))->applicationPath(base_path())
);
}
protected function registerRecorders(): void
{
$this->app->singleton(DumpRecorder::class);
$this->app->singleton(LogRecorder::class, function (Application $app): LogRecorder {
return new LogRecorder(
$app,
config()->get('flare.flare_middleware.' . AddLogs::class . '.maximum_number_of_collected_logs')
);
});
$this->app->singleton(
QueryRecorder::class,
function (Application $app): QueryRecorder {
return new QueryRecorder(
$app,
config('flare.flare_middleware.' . AddQueries::class . '.report_query_bindings', true),
config('flare.flare_middleware.' . AddQueries::class . '.maximum_number_of_collected_queries', 200)
);
}
);
$this->app->singleton(JobRecorder::class, function (Application $app): JobRecorder {
return new JobRecorder(
$app,
config('flare.flare_middleware.' . AddJobs::class . '.max_chained_job_reporting_depth', 5)
);
});
}
public function configureTinker(): void
{
if ($this->app->runningInConsole()) {
if (isset($_SERVER['argv']) && ['artisan', 'tinker'] === $_SERVER['argv']) {
app(Flare::class)->sendReportsImmediately();
}
}
}
protected function configureOctane(): void
{
if (isset($_SERVER['LARAVEL_OCTANE'])) {
$this->setupOctane();
}
}
protected function registerViewExceptionMapper(): void
{
$handler = $this->app->make(ExceptionHandler::class);
if (! method_exists($handler, 'map')) {
return;
}
$handler->map(function (ViewException $viewException) {
return $this->app->make(ViewExceptionMapper::class)->map($viewException);
});
}
protected function registerRoutes(): void
{
$this->loadRoutesFrom(realpath(__DIR__ . '/ignition-routes.php'));
}
protected function registerLogHandler(): void
{
$this->app->singleton('flare.logger', function ($app) {
$handler = new FlareLogHandler(
$app->make(Flare::class),
$app->make(SentReports::class),
);
$logLevelString = config('logging.channels.flare.level', 'error');
$logLevel = $this->getLogLevel($logLevelString);
$handler->setMinimumReportLogLevel($logLevel);
return tap(
new Logger('Flare'),
fn (Logger $logger) => $logger->pushHandler($handler)
);
});
Log::extend('flare', fn ($app) => $app['flare.logger']);
}
protected function startRecorders(): void
{
foreach ($this->app->config['ignition.recorders'] ?? [] as $recorder) {
$this->app->make($recorder)->start();
}
}
protected function configureQueue(): void
{
if (! $this->app->bound('queue')) {
return;
}
$queue = $this->app->get('queue');
// Reset before executing a queue job to make sure the job's log/query/dump recorders are empty.
// When using a sync queue this also reports the queued reports from previous exceptions.
$queue->before(function () {
$this->resetFlareAndLaravelIgnition();
app(Flare::class)->sendReportsImmediately();
});
// Send queued reports (and reset) after executing a queue job.
$queue->after(function () {
$this->resetFlareAndLaravelIgnition();
});
// Note: the $queue->looping() event can't be used because it's not triggered on Vapor
}
protected function getLogLevel(string $logLevelString): int
{
try {
$logLevel = Level::fromName($logLevelString);
} catch (Exception $exception) {
$logLevel = null;
}
if (! $logLevel) {
throw InvalidConfig::invalidLogLevel($logLevelString);
}
return $logLevel->value;
}
protected function getFlareMiddleware(): array
{
return collect(config('flare.flare_middleware'))
->map(function ($value, $key) {
if (is_string($key)) {
$middlewareClass = $key;
$parameters = $value ?? [];
} else {
$middlewareClass = $value;
$parameters = [];
}
return new $middlewareClass(...array_values($parameters));
})
->values()
->toArray();
}
protected function getSolutionProviders(): array
{
return collect(config('ignition.solution_providers'))
->reject(
fn (string $class) => in_array($class, config('ignition.ignored_solution_providers'))
)
->toArray();
}
protected function setupOctane(): void
{
$this->app['events']->listen(RequestReceived::class, function () {
$this->resetFlareAndLaravelIgnition();
});
$this->app['events']->listen(TaskReceived::class, function () {
$this->resetFlareAndLaravelIgnition();
});
$this->app['events']->listen(TickReceived::class, function () {
$this->resetFlareAndLaravelIgnition();
});
$this->app['events']->listen(RequestTerminated::class, function () {
$this->resetFlareAndLaravelIgnition();
});
}
protected function resetFlareAndLaravelIgnition(): void
{
$this->app->get(SentReports::class)->clear();
$this->app->get(Ignition::class)->reset();
if (config('flare.flare_middleware.' . AddLogs::class)) {
$this->app->make(LogRecorder::class)->reset();
}
if (config('flare.flare_middleware.' . AddQueries::class)) {
$this->app->make(QueryRecorder::class)->reset();
}
if (config('flare.flare_middleware.' . AddJobs::class)) {
$this->app->make(JobRecorder::class)->reset();
}
$this->app->make(DumpRecorder::class)->reset();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
class Dump
{
protected string $htmlDump;
protected ?string $file;
protected ?int $lineNumber;
protected float $microtime;
public function __construct(string $htmlDump, ?string $file, ?int $lineNumber, ?float $microtime = null)
{
$this->htmlDump = $htmlDump;
$this->file = $file;
$this->lineNumber = $lineNumber;
$this->microtime = $microtime ?? microtime(true);
}
/** @return array<string, mixed> */
public function toArray(): array
{
return [
'html_dump' => $this->htmlDump,
'file' => $this->file,
'line_number' => $this->lineNumber,
'microtime' => $this->microtime,
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
use Symfony\Component\VarDumper\Cloner\VarCloner;
class DumpHandler
{
protected DumpRecorder $dumpRecorder;
public function __construct(DumpRecorder $dumpRecorder)
{
$this->dumpRecorder = $dumpRecorder;
}
public function dump(mixed $value): void
{
$data = (new VarCloner)->cloneVar($value);
$this->dumpRecorder->record($data);
}
}

View File

@ -0,0 +1,135 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Arr;
use ReflectionMethod;
use ReflectionProperty;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\VarDumper;
class DumpRecorder
{
/** @var array<array<int,mixed>> */
protected array $dumps = [];
protected Application $app;
protected static bool $registeredHandler = false;
public function __construct(Application $app)
{
$this->app = $app;
}
public function start(): self
{
$multiDumpHandler = new MultiDumpHandler();
$this->app->singleton(MultiDumpHandler::class, fn () => $multiDumpHandler);
if (! self::$registeredHandler) {
static::$registeredHandler = true;
$this->ensureOriginalHandlerExists();
$originalHandler = VarDumper::setHandler(fn ($dumpedVariable) => $multiDumpHandler->dump($dumpedVariable));
$multiDumpHandler?->addHandler($originalHandler);
$multiDumpHandler->addHandler(fn ($var) => (new DumpHandler($this))->dump($var));
}
return $this;
}
public function record(Data $data): void
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 11);
$sourceFrame = $this->findSourceFrame($backtrace);
$file = (string) Arr::get($sourceFrame, 'file');
$lineNumber = (int) Arr::get($sourceFrame, 'line');
$htmlDump = (new HtmlDumper())->dump($data);
$this->dumps[] = new Dump($htmlDump, $file, $lineNumber);
}
public function getDumps(): array
{
return $this->toArray();
}
public function reset()
{
$this->dumps = [];
}
public function toArray(): array
{
$dumps = [];
foreach ($this->dumps as $dump) {
$dumps[] = $dump->toArray();
}
return $dumps;
}
/*
* Only the `VarDumper` knows how to create the orignal HTML or CLI VarDumper.
* Using reflection and the private VarDumper::register() method we can force it
* to create and register a new VarDumper::$handler before we'll overwrite it.
* Of course, we only need to do this if there isn't a registered VarDumper::$handler.
*
* @throws \ReflectionException
*/
protected function ensureOriginalHandlerExists(): void
{
$reflectionProperty = new ReflectionProperty(VarDumper::class, 'handler');
$reflectionProperty->setAccessible(true);
$handler = $reflectionProperty->getValue();
if (! $handler) {
// No handler registered yet, so we'll force VarDumper to create one.
$reflectionMethod = new ReflectionMethod(VarDumper::class, 'register');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invoke(null);
}
}
/**
* Find the first meaningful stack frame that is not the `DumpRecorder` itself.
*
* @template T of array{class?: class-string, function?: string, line?: int, file?: string}
*
* @param array<T> $stacktrace
*
* @return null|T
*/
protected function findSourceFrame(array $stacktrace): ?array
{
$seenVarDumper = false;
foreach ($stacktrace as $frame) {
// Keep looping until we're past the VarDumper::dump() call in Symfony's helper functions file.
if (Arr::get($frame, 'class') === VarDumper::class && Arr::get($frame, 'function') === 'dump') {
$seenVarDumper = true;
continue;
}
if (! $seenVarDumper) {
continue;
}
// Return the next frame in the stack after the VarDumper::dump() call:
return $frame;
}
return null;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper as BaseHtmlDumper;
class HtmlDumper extends BaseHtmlDumper
{
public function __construct($output = null, string $charset = null, int $flags = 0)
{
parent::__construct($output, $charset, $flags);
$this->setDumpHeader('');
}
public function dumpVariable($variable): string
{
$cloner = new VarCloner();
$clonedData = $cloner->cloneVar($variable)->withMaxDepth(3);
return $this->dump($clonedData);
}
public function dump(Data $data, $output = null, array $extraDisplayOptions = []): string
{
return (string)parent::dump($data, true, [
'maxDepth' => 3,
'maxStringLength' => 160,
]);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
class MultiDumpHandler
{
/** @var array<int, callable|null> */
protected array $handlers = [];
public function dump(mixed $value): void
{
foreach ($this->handlers as $handler) {
if ($handler) {
$handler($value);
}
}
}
public function addHandler(callable $callable = null): self
{
$this->handlers[] = $callable;
return $this;
}
}

View File

@ -0,0 +1,177 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\JobRecorder;
use DateTime;
use Error;
use Exception;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Jobs\RedisJob;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionProperty;
use RuntimeException;
class JobRecorder
{
protected ?Job $job = null;
public function __construct(
protected Application $app,
protected int $maxChainedJobReportingDepth = 5,
) {
}
public function start(): self
{
/** @phpstan-ignore-next-line */
$this->app['events']->listen(JobExceptionOccurred::class, [$this, 'record']);
return $this;
}
public function record(JobExceptionOccurred $event): void
{
$this->job = $event->job;
}
/**
* @return array<string, mixed>|null
*/
public function getJob(): ?array
{
if ($this->job === null) {
return null;
}
return array_merge(
$this->getJobProperties(),
[
'name' => $this->job->resolveName(),
'connection' => $this->job->getConnectionName(),
'queue' => $this->job->getQueue(),
]
);
}
public function reset(): void
{
$this->job = null;
}
protected function getJobProperties(): array
{
$payload = collect($this->resolveJobPayload());
$properties = [];
foreach ($payload as $key => $value) {
if (! in_array($key, ['job', 'data', 'displayName'])) {
$properties[$key] = $value;
}
}
try {
if (is_string($payload['data'])) {
$properties['data'] = json_decode($payload['data'], true, 512, JSON_THROW_ON_ERROR);
}
} catch (Exception $exception) {
}
if ($pushedAt = DateTime::createFromFormat('U.u', $payload->get('pushedAt', ''))) {
$properties['pushedAt'] = $pushedAt->format(DATE_ATOM);
}
try {
$properties['data'] = $this->resolveCommandProperties(
$this->resolveObjectFromCommand($payload['data']['command']),
$this->maxChainedJobReportingDepth
);
} catch (Exception $exception) {
}
return $properties;
}
protected function resolveJobPayload(): array
{
if (! $this->job instanceof RedisJob) {
return $this->job->payload();
}
try {
return json_decode($this->job->getReservedJob(), true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $e) {
return $this->job->payload();
}
}
protected function resolveCommandProperties(object $command, int $maxChainDepth): array
{
$propertiesToIgnore = ['job', 'closure'];
$properties = collect((new ReflectionClass($command))->getProperties())
->reject(function (ReflectionProperty $property) use ($propertiesToIgnore) {
return in_array($property->name, $propertiesToIgnore);
})
->mapWithKeys(function (ReflectionProperty $property) use ($command) {
try {
$property->setAccessible(true);
return [$property->name => $property->getValue($command)];
} catch (Error $error) {
return [$property->name => 'uninitialized'];
}
});
if ($properties->has('chained')) {
$properties['chained'] = $this->resolveJobChain($properties->get('chained'), $maxChainDepth);
}
return $properties->all();
}
/**
* @param array<string, mixed> $chainedCommands
* @param int $maxDepth
*
* @return array
*/
protected function resolveJobChain(array $chainedCommands, int $maxDepth): array
{
if ($maxDepth === 0) {
return ['Ignition stopped recording jobs after this point since the max chain depth was reached'];
}
return array_map(
function (string $command) use ($maxDepth) {
$commandObject = $this->resolveObjectFromCommand($command);
return [
'name' => $commandObject instanceof CallQueuedClosure ? $commandObject->displayName() : get_class($commandObject),
'data' => $this->resolveCommandProperties($commandObject, $maxDepth - 1),
];
},
$chainedCommands
);
}
// Taken from Illuminate\Queue\CallQueuedHandler
protected function resolveObjectFromCommand(string $command): object
{
if (Str::startsWith($command, 'O:')) {
return unserialize($command);
}
if ($this->app->bound(Encrypter::class)) {
/** @phpstan-ignore-next-line */
return unserialize($this->app[Encrypter::class]->decrypt($command));
}
throw new RuntimeException('Unable to extract job payload.');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\LogRecorder;
use Illuminate\Log\Events\MessageLogged;
class LogMessage
{
protected ?string $message;
protected string $level;
/** @var array<string, string> */
protected array $context = [];
protected ?float $microtime;
/**
* @param string|null $message
* @param string $level
* @param array<string, string> $context
* @param float|null $microtime
*/
public function __construct(
?string $message,
string $level,
array $context = [],
?float $microtime = null
) {
$this->message = $message;
$this->level = $level;
$this->context = $context;
$this->microtime = $microtime ?? microtime(true);
}
public static function fromMessageLoggedEvent(MessageLogged $event): self
{
return new self(
$event->message,
$event->level,
$event->context
);
}
/** @return array<string, mixed> */
public function toArray(): array
{
return [
'message' => $this->message,
'level' => $this->level,
'context' => $this->context,
'microtime' => $this->microtime,
];
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\LogRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\Events\MessageLogged;
use Throwable;
class LogRecorder
{
/** @var \Spatie\LaravelIgnition\Recorders\LogRecorder\LogMessage[] */
protected array $logMessages = [];
protected Application $app;
protected ?int $maxLogs;
public function __construct(Application $app, ?int $maxLogs = null)
{
$this->app = $app;
$this->maxLogs = $maxLogs;
}
public function start(): self
{
/** @phpstan-ignore-next-line */
$this->app['events']->listen(MessageLogged::class, [$this, 'record']);
return $this;
}
public function record(MessageLogged $event): void
{
if ($this->shouldIgnore($event)) {
return;
}
$this->logMessages[] = LogMessage::fromMessageLoggedEvent($event);
if (is_int($this->maxLogs)) {
$this->logMessages = array_slice($this->logMessages, -$this->maxLogs);
}
}
/** @return array<array<int,string>> */
public function getLogMessages(): array
{
return $this->toArray();
}
/** @return array<int, mixed> */
public function toArray(): array
{
$logMessages = [];
foreach ($this->logMessages as $log) {
$logMessages[] = $log->toArray();
}
return $logMessages;
}
protected function shouldIgnore(mixed $event): bool
{
if (! isset($event->context['exception'])) {
return false;
}
if (! $event->context['exception'] instanceof Throwable) {
return false;
}
return true;
}
public function reset(): void
{
$this->logMessages = [];
}
public function getMaxLogs(): ?int
{
return $this->maxLogs;
}
public function setMaxLogs(?int $maxLogs): self
{
$this->maxLogs = $maxLogs;
return $this;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\QueryRecorder;
use Illuminate\Database\Events\QueryExecuted;
class Query
{
protected string $sql;
protected float $time;
protected string $connectionName;
/** @var array<string, string>|null */
protected ?array $bindings;
protected float $microtime;
public static function fromQueryExecutedEvent(QueryExecuted $queryExecuted, bool $reportBindings = false): self
{
return new self(
$queryExecuted->sql,
$queryExecuted->time,
/** @phpstan-ignore-next-line */
$queryExecuted->connectionName ?? '',
$reportBindings ? $queryExecuted->bindings : null
);
}
/**
* @param string $sql
* @param float $time
* @param string $connectionName
* @param array<string, string>|null $bindings
* @param float|null $microtime
*/
protected function __construct(
string $sql,
float $time,
string $connectionName,
?array $bindings = null,
?float $microtime = null
) {
$this->sql = $sql;
$this->time = $time;
$this->connectionName = $connectionName;
$this->bindings = $bindings;
$this->microtime = $microtime ?? microtime(true);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'sql' => $this->sql,
'time' => $this->time,
'connection_name' => $this->connectionName,
'bindings' => $this->bindings,
'microtime' => $this->microtime,
];
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\QueryRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Events\QueryExecuted;
class QueryRecorder
{
/** @var \Spatie\LaravelIgnition\Recorders\QueryRecorder\Query[] */
protected array $queries = [];
protected Application $app;
protected bool $reportBindings = true;
protected ?int $maxQueries;
public function __construct(
Application $app,
bool $reportBindings = true,
?int $maxQueries = 200
) {
$this->app = $app;
$this->reportBindings = $reportBindings;
$this->maxQueries = $maxQueries;
}
public function start(): self
{
/** @phpstan-ignore-next-line */
$this->app['events']->listen(QueryExecuted::class, [$this, 'record']);
return $this;
}
public function record(QueryExecuted $queryExecuted): void
{
$this->queries[] = Query::fromQueryExecutedEvent($queryExecuted, $this->reportBindings);
if (is_int($this->maxQueries)) {
$this->queries = array_slice($this->queries, -$this->maxQueries);
}
}
/**
* @return array<int, array<string, mixed>>
*/
public function getQueries(): array
{
$queries = [];
foreach ($this->queries as $query) {
$queries[] = $query->toArray();
}
return $queries;
}
public function reset(): void
{
$this->queries = [];
}
public function getReportBindings(): bool
{
return $this->reportBindings;
}
public function setReportBindings(bool $reportBindings): self
{
$this->reportBindings = $reportBindings;
return $this;
}
public function getMaxQueries(): ?int
{
return $this->maxQueries;
}
public function setMaxQueries(?int $maxQueries): self
{
$this->maxQueries = $maxQueries;
return $this;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Spatie\LaravelIgnition\Renderers;
use Spatie\FlareClient\Flare;
use Spatie\Ignition\Config\IgnitionConfig;
use Spatie\Ignition\Contracts\SolutionProviderRepository;
use Spatie\Ignition\Ignition;
use Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector;
use Spatie\LaravelIgnition\Solutions\SolutionTransformers\LaravelSolutionTransformer;
use Spatie\LaravelIgnition\Support\LaravelDocumentationLinkFinder;
use Throwable;
class ErrorPageRenderer
{
public function render(Throwable $throwable): void
{
$viteJsAutoRefresh = '';
if (class_exists('Illuminate\Foundation\Vite')) {
$vite = app(\Illuminate\Foundation\Vite::class);
if (is_file($vite->hotFile())) {
$viteJsAutoRefresh = $vite->__invoke([]);
}
}
app(Ignition::class)
->resolveDocumentationLink(
fn (Throwable $throwable) => (new LaravelDocumentationLinkFinder())->findLinkForThrowable($throwable)
)
->setFlare(app(Flare::class))
->setConfig(app(IgnitionConfig::class))
->setSolutionProviderRepository(app(SolutionProviderRepository::class))
->setContextProviderDetector(new LaravelContextProviderDetector())
->setSolutionTransformerClass(LaravelSolutionTransformer::class)
->applicationPath(base_path())
->addCustomHtmlToHead($viteJsAutoRefresh)
->renderException($throwable);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Spatie\LaravelIgnition\Renderers;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
class IgnitionExceptionRenderer implements ExceptionRenderer
{
protected ErrorPageRenderer $errorPageHandler;
public function __construct(ErrorPageRenderer $errorPageHandler)
{
$this->errorPageHandler = $errorPageHandler;
}
public function render($throwable)
{
ob_start();
$this->errorPageHandler->render($throwable);
return ob_get_clean();
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Illuminate\Support\Facades\Artisan;
use Spatie\Ignition\Contracts\RunnableSolution;
class GenerateAppKeySolution implements RunnableSolution
{
public function getSolutionTitle(): string
{
return 'Your app key is missing';
}
public function getDocumentationLinks(): array
{
return [
'Laravel installation' => 'https://laravel.com/docs/master/installation#configuration',
];
}
public function getSolutionActionDescription(): string
{
return 'Generate your application encryption key using `php artisan key:generate`.';
}
public function getRunButtonText(): string
{
return 'Generate app key';
}
public function getSolutionDescription(): string
{
return '';
}
public function getRunParameters(): array
{
return [];
}
public function run(array $parameters = []): void
{
Artisan::call('key:generate');
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Livewire\LivewireComponentsFinder;
use Spatie\Ignition\Contracts\RunnableSolution;
class LivewireDiscoverSolution implements RunnableSolution
{
protected string $customTitle;
public function __construct(string $customTitle = '')
{
$this->customTitle = $customTitle;
}
public function getSolutionTitle(): string
{
return $this->customTitle;
}
public function getSolutionDescription(): string
{
return 'You might have forgotten to discover your Livewire components.';
}
public function getDocumentationLinks(): array
{
return [
'Livewire: Artisan Commands' => 'https://laravel-livewire.com/docs/2.x/artisan-commands',
];
}
public function getRunParameters(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
return 'You can discover your Livewire components using `php artisan livewire:discover`.';
}
public function getRunButtonText(): string
{
return 'Run livewire:discover';
}
public function run(array $parameters = []): void
{
app(LivewireComponentsFinder::class)->build();
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\RunnableSolution;
class MakeViewVariableOptionalSolution implements RunnableSolution
{
protected ?string $variableName;
protected ?string $viewFile;
public function __construct(string $variableName = null, string $viewFile = null)
{
$this->variableName = $variableName;
$this->viewFile = $viewFile;
}
public function getSolutionTitle(): string
{
return "$$this->variableName is undefined";
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
$output = [
'Make the variable optional in the blade template.',
"Replace `{{ $$this->variableName }}` with `{{ $$this->variableName ?? '' }}`",
];
return implode(PHP_EOL, $output);
}
public function getRunButtonText(): string
{
return 'Make variable optional';
}
public function getSolutionDescription(): string
{
return '';
}
public function getRunParameters(): array
{
return [
'variableName' => $this->variableName,
'viewFile' => $this->viewFile,
];
}
/**
* @param array<string, mixed> $parameters
*
* @return bool
*/
public function isRunnable(array $parameters = []): bool
{
return $this->makeOptional($this->getRunParameters()) !== false;
}
/**
* @param array<string, string> $parameters
*
* @return void
*/
public function run(array $parameters = []): void
{
$output = $this->makeOptional($parameters);
if ($output !== false) {
file_put_contents($parameters['viewFile'], $output);
}
}
protected function isSafePath(string $path): bool
{
if (! Str::startsWith($path, ['/', './'])) {
return false;
}
if (! Str::endsWith($path, '.blade.php')) {
return false;
}
return true;
}
/**
* @param array<string, string> $parameters
*
* @return bool|string
*/
public function makeOptional(array $parameters = []): bool|string
{
if (! $this->isSafePath($parameters['viewFile'])) {
return false;
}
$originalContents = (string)file_get_contents($parameters['viewFile']);
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
$originalTokens = token_get_all(Blade::compileString($originalContents));
$newTokens = token_get_all(Blade::compileString($newContents));
$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
if ($expectedTokens !== $newTokens) {
return false;
}
return $newContents;
}
/**
* @param array<int, mixed> $originalTokens
* @param string $variableName
*
* @return array<int, mixed>
*/
protected function generateExpectedTokens(array $originalTokens, string $variableName): array
{
$expectedTokens = [];
foreach ($originalTokens as $token) {
$expectedTokens[] = $token;
if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_COALESCE, '??', $token[2]];
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];
}
}
return $expectedTokens;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Illuminate\Support\Facades\Artisan;
use Spatie\Ignition\Contracts\RunnableSolution;
class RunMigrationsSolution implements RunnableSolution
{
protected string $customTitle;
public function __construct(string $customTitle = '')
{
$this->customTitle = $customTitle;
}
public function getSolutionTitle(): string
{
return $this->customTitle;
}
public function getSolutionDescription(): string
{
return 'You might have forgotten to run your database migrations.';
}
public function getDocumentationLinks(): array
{
return [
'Database: Running Migrations docs' => 'https://laravel.com/docs/master/migrations#running-migrations',
];
}
public function getRunParameters(): array
{
return [];
}
public function getSolutionActionDescription(): string
{
return 'You can try to run your migrations using `php artisan migrate`.';
}
public function getRunButtonText(): string
{
return 'Run migrations';
}
public function run(array $parameters = []): void
{
Artisan::call('migrate');
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestUsingCorrectDbNameSolution;
use Throwable;
class DefaultDbNameSolutionProvider implements HasSolutionsForThrowable
{
const MYSQL_UNKNOWN_DATABASE_CODE = 1049;
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof QueryException) {
return false;
}
if ($throwable->getCode() !== self::MYSQL_UNKNOWN_DATABASE_CODE) {
return false;
}
if (! in_array(env('DB_DATABASE'), ['homestead', 'laravel'])) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
return [new SuggestUsingCorrectDbNameSolution()];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Broadcasting\BroadcastException;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\LaravelVersion;
use Throwable;
class GenericLaravelExceptionSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return ! is_null($this->getSolutionTexts($throwable));
}
public function getSolutions(Throwable $throwable): array
{
if (! $texts = $this->getSolutionTexts($throwable)) {
return [];
}
$solution = BaseSolution::create($texts['title'])
->setSolutionDescription($texts['description'])
->setDocumentationLinks($texts['links']);
return ([$solution]);
}
/**
* @param \Throwable $throwable
*
* @return array<string, mixed>|null
*/
protected function getSolutionTexts(Throwable $throwable) : ?array
{
foreach ($this->getSupportedExceptions() as $supportedClass => $texts) {
if ($throwable instanceof $supportedClass) {
return $texts;
}
}
return null;
}
/** @return array<string, mixed> */
protected function getSupportedExceptions(): array
{
$majorVersion = LaravelVersion::major();
return
[
BroadcastException::class => [
'title' => 'Here are some links that might help solve this problem',
'description' => '',
'links' => [
'Laravel docs on authentication' => "https://laravel.com/docs/{$majorVersion}.x/authentication",
],
],
];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\UseDefaultValetDbCredentialsSolution;
use Throwable;
class IncorrectValetDbCredentialsSolutionProvider implements HasSolutionsForThrowable
{
const MYSQL_ACCESS_DENIED_CODE = 1045;
public function canSolve(Throwable $throwable): bool
{
if (PHP_OS !== 'Darwin') {
return false;
}
if (! $throwable instanceof QueryException) {
return false;
}
if (! $this->isAccessDeniedCode($throwable->getCode())) {
return false;
}
if (! $this->envFileExists()) {
return false;
}
if (! $this->isValetInstalled()) {
return false;
}
if ($this->usingCorrectDefaultCredentials()) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
return [new UseDefaultValetDbCredentialsSolution()];
}
protected function envFileExists(): bool
{
return file_exists(base_path('.env'));
}
protected function isAccessDeniedCode(string $code): bool
{
return $code === static::MYSQL_ACCESS_DENIED_CODE;
}
protected function isValetInstalled(): bool
{
return file_exists('/usr/local/bin/valet');
}
protected function usingCorrectDefaultCredentials(): bool
{
return env('DB_USERNAME') === 'root' && env('DB_PASSWORD') === '';
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\Composer\ComposerClassMap;
use Spatie\LaravelIgnition\Support\StringComparator;
use Throwable;
use UnexpectedValueException;
class InvalidRouteActionSolutionProvider implements HasSolutionsForThrowable
{
protected const REGEX = '/\[([a-zA-Z\\\\]+)\]/m';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof UnexpectedValueException) {
return false;
}
if (! preg_match(self::REGEX, $throwable->getMessage(), $matches)) {
return false;
}
return Str::startsWith($throwable->getMessage(), 'Invalid route action: ');
}
public function getSolutions(Throwable $throwable): array
{
preg_match(self::REGEX, $throwable->getMessage(), $matches);
$invalidController = $matches[1] ?? null;
$suggestedController = $this->findRelatedController($invalidController);
if ($suggestedController === $invalidController) {
return [
BaseSolution::create("`{$invalidController}` is not invokable.")
->setSolutionDescription("The controller class `{$invalidController}` is not invokable. Did you forget to add the `__invoke` method or is the controller's method missing in your routes file?"),
];
}
if ($suggestedController) {
return [
BaseSolution::create("`{$invalidController}` was not found.")
->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Did you mean `{$suggestedController}`?"),
];
}
return [
BaseSolution::create("`{$invalidController}` was not found.")
->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Are you sure this controller exists and is imported correctly?"),
];
}
protected function findRelatedController(string $invalidController): ?string
{
$composerClassMap = app(ComposerClassMap::class);
$controllers = collect($composerClassMap->listClasses())
->filter(function (string $file, string $fqcn) {
return Str::endsWith($fqcn, 'Controller');
})
->mapWithKeys(function (string $file, string $fqcn) {
return [$fqcn => class_basename($fqcn)];
})
->toArray();
$basenameMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4);
$controllers = array_flip($controllers);
$fqcnMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4);
return $fqcnMatch ?? $basenameMatch;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\LazyLoadingViolationException;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\LaravelVersion;
use Throwable;
class LazyLoadingViolationSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if ($throwable instanceof LazyLoadingViolationException) {
return true;
}
if (! $previous = $throwable->getPrevious()) {
return false;
}
return $previous instanceof LazyLoadingViolationException;
}
public function getSolutions(Throwable $throwable): array
{
$majorVersion = LaravelVersion::major();
return [BaseSolution::create(
'Lazy loading was disabled to detect N+1 problems'
)
->setSolutionDescription(
'Either avoid lazy loading the relation or allow lazy loading.'
)
->setDocumentationLinks([
'Read the docs on preventing lazy loading' => "https://laravel.com/docs/{$majorVersion}.x/eloquent-relationships#preventing-lazy-loading",
'Watch a video on how to deal with the N+1 problem' => 'https://www.youtube.com/watch?v=ZE7KBeraVpc',
]),];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use RuntimeException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\GenerateAppKeySolution;
use Throwable;
class MissingAppKeySolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof RuntimeException) {
return false;
}
return $throwable->getMessage() === 'No application encryption key has been specified.';
}
public function getSolutions(Throwable $throwable): array
{
return [new GenerateAppKeySolution()];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\RunMigrationsSolution;
use Throwable;
class MissingColumnSolutionProvider implements HasSolutionsForThrowable
{
/**
* See https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_bad_field_error.
*/
const MYSQL_BAD_FIELD_CODE = '42S22';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof QueryException) {
return false;
}
return $this->isBadTableErrorCode($throwable->getCode());
}
protected function isBadTableErrorCode(string $code): bool
{
return $code === static::MYSQL_BAD_FIELD_CODE;
}
public function getSolutions(Throwable $throwable): array
{
return [new RunMigrationsSolution('A column was not found')];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestImportSolution;
use Spatie\LaravelIgnition\Support\Composer\ComposerClassMap;
use Throwable;
class MissingImportSolutionProvider implements HasSolutionsForThrowable
{
protected ?string $foundClass;
protected ComposerClassMap $composerClassMap;
public function canSolve(Throwable $throwable): bool
{
$pattern = '/Class \"([^\s]+)\" not found/m';
if (! preg_match($pattern, $throwable->getMessage(), $matches)) {
return false;
}
$class = $matches[1];
$this->composerClassMap = new ComposerClassMap();
$this->search($class);
return ! is_null($this->foundClass);
}
/**
* @param \Throwable $throwable
*
* @return array<int, SuggestImportSolution>
*/
public function getSolutions(Throwable $throwable): array
{
if (is_null($this->foundClass)) {
return [];
}
return [new SuggestImportSolution($this->foundClass)];
}
protected function search(string $missingClass): void
{
$this->foundClass = $this->composerClassMap->searchClassMap($missingClass);
if (is_null($this->foundClass)) {
$this->foundClass = $this->composerClassMap->searchPsrMaps($missingClass);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Livewire\Exceptions\ComponentNotFoundException;
use Livewire\LivewireComponentsFinder;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\LivewireDiscoverSolution;
use Throwable;
class MissingLivewireComponentSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if (! $this->livewireIsInstalled()) {
return false;
}
if (! $throwable instanceof ComponentNotFoundException) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
return [new LivewireDiscoverSolution('A Livewire component was not found')];
}
public function livewireIsInstalled(): bool
{
if (! class_exists(ComponentNotFoundException::class)) {
return false;
}
if (! class_exists(LivewireComponentsFinder::class)) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Throwable;
class MissingMixManifestSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return Str::startsWith($throwable->getMessage(), 'Mix manifest not found');
}
public function getSolutions(Throwable $throwable): array
{
return [
BaseSolution::create('Missing Mix Manifest File')
->setSolutionDescription('Did you forget to run `npm install && npm run dev`?'),
];
}
}

View File

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

View File

@ -0,0 +1,38 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Str;
use OpenAI\Client;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Solutions\OpenAi\OpenAiSolutionProvider as BaseOpenAiSolutionProvider;
use Throwable;
class OpenAiSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if (! class_exists(Client::class)) {
return false;
}
if (config('ignition.open_ai_key') === null) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
$solutionProvider = new BaseOpenAiSolutionProvider(
openAiKey: config('ignition.open_ai_key'),
cache: cache()->store(config('cache.default')),
cacheTtlInSeconds: 60,
applicationType: 'Laravel ' . Str::before(app()->version(), '.'),
applicationPath: base_path(),
);
return $solutionProvider->getSolutions($throwable);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Facades\Route;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\StringComparator;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Throwable;
class RouteNotDefinedSolutionProvider implements HasSolutionsForThrowable
{
protected const REGEX = '/Route \[(.*)\] not defined/m';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof RouteNotFoundException) {
return false;
}
return (bool)preg_match(self::REGEX, $throwable->getMessage(), $matches);
}
public function getSolutions(Throwable $throwable): array
{
preg_match(self::REGEX, $throwable->getMessage(), $matches);
$missingRoute = $matches[1] ?? '';
$suggestedRoute = $this->findRelatedRoute($missingRoute);
if ($suggestedRoute) {
return [
BaseSolution::create("{$missingRoute} was not defined.")
->setSolutionDescription("Did you mean `{$suggestedRoute}`?"),
];
}
return [
BaseSolution::create("{$missingRoute} was not defined.")
->setSolutionDescription('Are you sure that the route is defined'),
];
}
protected function findRelatedRoute(string $missingRoute): ?string
{
Route::getRoutes()->refreshNameLookups();
return StringComparator::findClosestMatch(array_keys(Route::getRoutes()->getRoutesByName()), $missingRoute);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Exception;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Throwable;
class RunningLaravelDuskInProductionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof Exception) {
return false;
}
return $throwable->getMessage() === 'It is unsafe to run Dusk in production.';
}
public function getSolutions(Throwable $throwable): array
{
return [
BaseSolution::create()
->setSolutionTitle('Laravel Dusk should not be run in production.')
->setSolutionDescription('Install the dependencies with the `--no-dev` flag.'),
BaseSolution::create()
->setSolutionTitle('Laravel Dusk can be run in other environments.')
->setSolutionDescription('Consider setting the `APP_ENV` to something other than `production` like `local` for example.'),
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Throwable;
class SailNetworkSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return app()->runningInConsole()
&& str_contains($throwable->getMessage(), 'php_network_getaddresses')
&& file_exists(base_path('vendor/bin/sail'))
&& file_exists(base_path('docker-compose.yml'))
&& env('LARAVEL_SAIL') === null;
}
public function getSolutions(Throwable $throwable): array
{
return [
BaseSolution::create('Network address not found')
->setSolutionDescription('Did you mean to use `sail artisan`?')
->setDocumentationLinks([
'Sail: Executing Artisan Commands' => 'https://laravel.com/docs/sail#executing-artisan-commands',
]),
];
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Collection;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract;
use Throwable;
class SolutionProviderRepository implements SolutionProviderRepositoryContract
{
/**
* @param array<int, ProvidesSolution> $solutionProviders
*/
protected Collection $solutionProviders;
/**
* @param array<int, ProvidesSolution> $solutionProviders
*/
public function __construct(array $solutionProviders = [])
{
$this->solutionProviders = Collection::make($solutionProviders);
}
public function registerSolutionProvider(string $solutionProviderClass): SolutionProviderRepositoryContract
{
$this->solutionProviders->push($solutionProviderClass);
return $this;
}
public function registerSolutionProviders(array $solutionProviderClasses): SolutionProviderRepositoryContract
{
$this->solutionProviders = $this->solutionProviders->merge($solutionProviderClasses);
return $this;
}
public function getSolutionsForThrowable(Throwable $throwable): array
{
$solutions = [];
if ($throwable instanceof Solution) {
$solutions[] = $throwable;
}
if ($throwable instanceof ProvidesSolution) {
$solutions[] = $throwable->getSolution();
}
/** @phpstan-ignore-next-line */
$providedSolutions = $this->solutionProviders
->filter(function (string $solutionClass) {
if (! in_array(HasSolutionsForThrowable::class, class_implements($solutionClass) ?: [])) {
return false;
}
if (in_array($solutionClass, config('ignition.ignored_solution_providers', []))) {
return false;
}
return true;
})
->map(fn (string $solutionClass) => app($solutionClass))
->filter(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) {
try {
return $solutionProvider->canSolve($throwable);
} catch (Throwable $e) {
return false;
}
})
->map(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) {
try {
return $solutionProvider->getSolutions($throwable);
} catch (Throwable $e) {
return [];
}
})
->flatten()
->toArray();
return array_merge($solutions, $providedSolutions);
}
public function getSolutionForClass(string $solutionClass): ?Solution
{
if (! class_exists($solutionClass)) {
return null;
}
if (! in_array(Solution::class, class_implements($solutionClass) ?: [])) {
return null;
}
return app($solutionClass);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\RunMigrationsSolution;
use Throwable;
class TableNotFoundSolutionProvider implements HasSolutionsForThrowable
{
/**
* See https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_bad_table_error.
*/
const MYSQL_BAD_TABLE_CODE = '42S02';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof QueryException) {
return false;
}
return $this->isBadTableErrorCode($throwable->getCode());
}
protected function isBadTableErrorCode(string $code): bool
{
return $code === static::MYSQL_BAD_TABLE_CODE;
}
public function getSolutions(Throwable $throwable): array
{
return [new RunMigrationsSolution('A table was not found')];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Livewire\Exceptions\MethodNotFoundException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestLivewireMethodNameSolution;
use Spatie\LaravelIgnition\Support\LivewireComponentParser;
use Throwable;
class UndefinedLivewireMethodSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return $throwable instanceof MethodNotFoundException;
}
public function getSolutions(Throwable $throwable): array
{
['methodName' => $methodName, 'component' => $component] = $this->getMethodAndComponent($throwable);
if ($methodName === null || $component === null) {
return [];
}
$parsed = LivewireComponentParser::create($component);
return $parsed->getMethodNamesLike($methodName)
->map(function (string $suggested) use ($parsed, $methodName) {
return new SuggestLivewireMethodNameSolution(
$methodName,
$parsed->getComponentClass(),
$suggested
);
})
->toArray();
}
/** @return array<string, string|null> */
protected function getMethodAndComponent(Throwable $throwable): array
{
preg_match_all('/\[([\d\w\-_]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER);
return [
'methodName' => $matches[0][1] ?? null,
'component' => $matches[1][1] ?? null,
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Livewire\Exceptions\PropertyNotFoundException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestLivewirePropertyNameSolution;
use Spatie\LaravelIgnition\Support\LivewireComponentParser;
use Throwable;
class UndefinedLivewirePropertySolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return $throwable instanceof PropertyNotFoundException;
}
public function getSolutions(Throwable $throwable): array
{
['variable' => $variable, 'component' => $component] = $this->getMethodAndComponent($throwable);
if ($variable === null || $component === null) {
return [];
}
$parsed = LivewireComponentParser::create($component);
return $parsed->getPropertyNamesLike($variable)
->map(function (string $suggested) use ($parsed, $variable) {
return new SuggestLivewirePropertyNameSolution(
$variable,
$parsed->getComponentClass(),
'$'.$suggested
);
})
->toArray();
}
/**
* @param \Throwable $throwable
*
* @return array<string, string|null>
*/
protected function getMethodAndComponent(Throwable $throwable): array
{
preg_match_all('/\[([\d\w\-_\$]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER, 0);
return [
'variable' => $matches[0][1] ?? null,
'component' => $matches[1][1] ?? null,
];
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\Ignition\Contracts\Solution;
use Spatie\LaravelIgnition\Exceptions\ViewException;
use Spatie\LaravelIgnition\Solutions\MakeViewVariableOptionalSolution;
use Spatie\LaravelIgnition\Solutions\SuggestCorrectVariableNameSolution;
use Throwable;
class UndefinedViewVariableSolutionProvider implements HasSolutionsForThrowable
{
protected string $variableName;
protected string $viewFile;
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof ViewException) {
return false;
}
return $this->getNameAndView($throwable) !== null;
}
public function getSolutions(Throwable $throwable): array
{
$solutions = [];
/** @phpstan-ignore-next-line */
extract($this->getNameAndView($throwable));
if (! isset($variableName)) {
return [];
}
if (isset($viewFile)) {
/** @phpstan-ignore-next-line */
$solutions = $this->findCorrectVariableSolutions($throwable, $variableName, $viewFile);
$solutions[] = $this->findOptionalVariableSolution($variableName, $viewFile);
}
return $solutions;
}
/**
* @param \Spatie\LaravelIgnition\Exceptions\ViewException $throwable
* @param string $variableName
* @param string $viewFile
*
* @return array<int, \Spatie\Ignition\Contracts\Solution>
*/
protected function findCorrectVariableSolutions(
ViewException $throwable,
string $variableName,
string $viewFile
): array {
return collect($throwable->getViewData())
->map(function ($value, $key) use ($variableName) {
similar_text($variableName, $key, $percentage);
return ['match' => $percentage, 'value' => $value];
})
->sortByDesc('match')
->filter(fn ($var) => $var['match'] > 40)
->keys()
->map(fn ($suggestion) => new SuggestCorrectVariableNameSolution($variableName, $viewFile, $suggestion))
->map(function ($solution) {
return $solution->isRunnable()
? $solution
: BaseSolution::create($solution->getSolutionTitle())
->setSolutionDescription($solution->getSolutionDescription());
})
->toArray();
}
protected function findOptionalVariableSolution(string $variableName, string $viewFile): Solution
{
$optionalSolution = new MakeViewVariableOptionalSolution($variableName, $viewFile);
return $optionalSolution->isRunnable()
? $optionalSolution
: BaseSolution::create($optionalSolution->getSolutionTitle())
->setSolutionDescription($optionalSolution->getSolutionDescription());
}
/**
* @param \Throwable $throwable
*
* @return array<string, string>|null
*/
protected function getNameAndView(Throwable $throwable): ?array
{
$pattern = '/Undefined variable:? (.*?) \(View: (.*?)\)/';
preg_match($pattern, $throwable->getMessage(), $matches);
if (count($matches) === 3) {
[, $variableName, $viewFile] = $matches;
$variableName = ltrim($variableName, '$');
return compact('variableName', 'viewFile');
}
return null;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestUsingMariadbDatabaseSolution;
use Throwable;
class UnknownMariadbCollationSolutionProvider implements HasSolutionsForThrowable
{
const MYSQL_UNKNOWN_COLLATION_CODE = 1273;
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof QueryException) {
return false;
}
if ($throwable->getCode() !== self::MYSQL_UNKNOWN_COLLATION_CODE) {
return false;
}
return str_contains(
$throwable->getMessage(),
'Unknown collation: \'utf8mb4_uca1400_ai_ci\''
);
}
public function getSolutions(Throwable $throwable): array
{
return [new SuggestUsingMariadbDatabaseSolution()];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Database\QueryException;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Solutions\SuggestUsingMysql8DatabaseSolution;
use Throwable;
class UnknownMysql8CollationSolutionProvider implements HasSolutionsForThrowable
{
const MYSQL_UNKNOWN_COLLATION_CODE = 1273;
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof QueryException) {
return false;
}
if ($throwable->getCode() !== self::MYSQL_UNKNOWN_COLLATION_CODE) {
return false;
}
return str_contains(
$throwable->getMessage(),
'Unknown collation: \'utf8mb4_0900_ai_ci\''
);
}
public function getSolutions(Throwable $throwable): array
{
return [new SuggestUsingMysql8DatabaseSolution()];
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use BadMethodCallException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\Validator;
use ReflectionClass;
use ReflectionMethod;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Support\StringComparator;
use Throwable;
class UnknownValidationSolutionProvider implements HasSolutionsForThrowable
{
protected const REGEX = '/Illuminate\\\\Validation\\\\Validator::(?P<method>validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/m';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof BadMethodCallException) {
return false;
}
return ! is_null($this->getMethodFromExceptionMessage($throwable->getMessage()));
}
public function getSolutions(Throwable $throwable): array
{
return [
BaseSolution::create()
->setSolutionTitle('Unknown Validation Rule')
->setSolutionDescription($this->getSolutionDescription($throwable)),
];
}
protected function getSolutionDescription(Throwable $throwable): string
{
$method = (string)$this->getMethodFromExceptionMessage($throwable->getMessage());
$possibleMethod = StringComparator::findSimilarText(
$this->getAvailableMethods()->toArray(),
$method
);
if (empty($possibleMethod)) {
return '';
}
$rule = Str::snake(str_replace('validate', '', $possibleMethod));
return "Did you mean `{$rule}` ?";
}
protected function getMethodFromExceptionMessage(string $message): ?string
{
if (! preg_match(self::REGEX, $message, $matches)) {
return null;
}
return $matches['method'];
}
protected function getAvailableMethods(): Collection
{
$class = new ReflectionClass(Validator::class);
$extensions = Collection::make((app('validator')->make([], []))->extensions)
->keys()
->map(fn (string $extension) => 'validate'.Str::studly($extension));
return Collection::make($class->getMethods())
->filter(fn (ReflectionMethod $method) => preg_match('/(validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/', $method->name))
->map(fn (ReflectionMethod $method) => $method->name)
->merge($extensions);
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionProviders;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\View;
use InvalidArgumentException;
use Spatie\Ignition\Contracts\BaseSolution;
use Spatie\Ignition\Contracts\HasSolutionsForThrowable;
use Spatie\LaravelIgnition\Exceptions\ViewException;
use Spatie\LaravelIgnition\Support\StringComparator;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Throwable;
class ViewNotFoundSolutionProvider implements HasSolutionsForThrowable
{
protected const REGEX = '/View \[(.*)\] not found/m';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof InvalidArgumentException && ! $throwable instanceof ViewException) {
return false;
}
return (bool)preg_match(self::REGEX, $throwable->getMessage(), $matches);
}
public function getSolutions(Throwable $throwable): array
{
preg_match(self::REGEX, $throwable->getMessage(), $matches);
$missingView = $matches[1] ?? null;
$suggestedView = $this->findRelatedView($missingView);
if ($suggestedView) {
return [
BaseSolution::create()
->setSolutionTitle("{$missingView} was not found.")
->setSolutionDescription("Did you mean `{$suggestedView}`?"),
];
}
return [
BaseSolution::create()
->setSolutionTitle("{$missingView} was not found.")
->setSolutionDescription('Are you sure the view exists and is a `.blade.php` file?'),
];
}
protected function findRelatedView(string $missingView): ?string
{
$views = $this->getAllViews();
return StringComparator::findClosestMatch($views, $missingView);
}
/** @return array<int, string> */
protected function getAllViews(): array
{
/** @var \Illuminate\View\FileViewFinder $fileViewFinder */
$fileViewFinder = View::getFinder();
$extensions = $fileViewFinder->getExtensions();
$viewsForHints = collect($fileViewFinder->getHints())
->flatMap(function ($paths, string $namespace) use ($extensions) {
$paths = Arr::wrap($paths);
return collect($paths)
->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions))
->map(fn (string $view) => "{$namespace}::{$view}")
->toArray();
});
$viewsForViewPaths = collect($fileViewFinder->getPaths())
->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions));
return $viewsForHints->merge($viewsForViewPaths)->toArray();
}
/**
* @param string $path
* @param array<int, string> $extensions
*
* @return array<int, string>
*/
protected function getViewsInPath(string $path, array $extensions): array
{
$filePatterns = array_map(fn (string $extension) => "*.{$extension}", $extensions);
$extensionsWithDots = array_map(fn (string $extension) => ".{$extension}", $extensions);
$files = (new Finder())
->in($path)
->files();
foreach ($filePatterns as $filePattern) {
$files->name($filePattern);
}
$views = [];
foreach ($files as $file) {
if ($file instanceof SplFileInfo) {
$view = $file->getRelativePathname();
$view = str_replace($extensionsWithDots, '', $view);
$view = str_replace('/', '.', $view);
$views[] = $view;
}
}
return $views;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionTransformers;
use Spatie\Ignition\Contracts\RunnableSolution;
use Spatie\Ignition\Solutions\SolutionTransformer;
use Spatie\LaravelIgnition\Http\Controllers\ExecuteSolutionController;
use Throwable;
class LaravelSolutionTransformer extends SolutionTransformer
{
/** @return array<string|mixed> */
public function toArray(): array
{
$baseProperties = parent::toArray();
if (! $this->isRunnable()) {
return $baseProperties;
}
/** @var RunnableSolution $solution Type shenanigans */
$solution = $this->solution;
$runnableProperties = [
'is_runnable' => true,
'action_description' => $solution->getSolutionActionDescription(),
'run_button_text' => $solution->getRunButtonText(),
'execute_endpoint' => $this->executeEndpoint(),
'run_parameters' => $solution->getRunParameters(),
];
return array_merge($baseProperties, $runnableProperties);
}
protected function isRunnable(): bool
{
if (! $this->solution instanceof RunnableSolution) {
return false;
}
if (! $this->executeEndpoint()) {
return false;
}
return true;
}
protected function executeEndpoint(): ?string
{
try {
// The action class needs to be prefixed with a `\` to Laravel from trying
// to add its own global namespace from RouteServiceProvider::$namespace.
return action('\\'.ExecuteSolutionController::class);
} catch (Throwable $exception) {
report($exception);
return null;
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestCorrectVariableNameSolution implements Solution
{
protected ?string $variableName;
protected ?string $viewFile;
protected ?string $suggested;
public function __construct(string $variableName = null, string $viewFile = null, string $suggested = null)
{
$this->variableName = $variableName;
$this->viewFile = $viewFile;
$this->suggested = $suggested;
}
public function getSolutionTitle(): string
{
return 'Possible typo $'.$this->variableName;
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionDescription(): string
{
return "Did you mean `$$this->suggested`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestImportSolution implements Solution
{
protected string $class;
public function __construct(string $class = '')
{
$this->class = $class;
}
public function getSolutionTitle(): string
{
return 'A class import is missing';
}
public function getSolutionDescription(): string
{
return 'You have a missing class import. Try importing this class: `'.$this->class.'`.';
}
public function getDocumentationLinks(): array
{
return [];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestLivewireMethodNameSolution implements Solution
{
public function __construct(
protected string $methodName,
protected string $componentClass,
protected string $suggested
) {
}
public function getSolutionTitle(): string
{
return "Possible typo `{$this->componentClass}::{$this->methodName}`";
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionDescription(): string
{
return "Did you mean `{$this->componentClass}::{$this->suggested}`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestLivewirePropertyNameSolution implements Solution
{
public function __construct(
protected string $variableName,
protected string $componentClass,
protected string $suggested,
) {
}
public function getSolutionTitle(): string
{
return "Possible typo {$this->variableName}";
}
public function getDocumentationLinks(): array
{
return [];
}
public function getSolutionDescription(): string
{
return "Did you mean `$this->suggested`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestUsingCorrectDbNameSolution implements Solution
{
public function getSolutionTitle(): string
{
return 'Database name seems incorrect';
}
public function getSolutionDescription(): string
{
$defaultDatabaseName = env('DB_DATABASE');
return "You're using the default database name `$defaultDatabaseName`. This database does not exist.\n\nEdit the `.env` file and use the correct database name in the `DB_DATABASE` key.";
}
/** @return array<string, string> */
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestUsingMariadbDatabaseSolution implements Solution
{
public function getSolutionTitle(): string
{
return 'Database is not a MariaDB database';
}
public function getSolutionDescription(): string
{
return "Laravel 11 changed the default collation for MySQL and MariaDB. It seems you are trying to use the MariaDB collation `utf8mb4_uca1400_ai_ci` with a MySQL database.\n\nEdit the `.env` file and use the correct database in the `DB_CONNECTION` key.";
}
/** @return array<string, string> */
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Spatie\Ignition\Contracts\Solution;
class SuggestUsingMysql8DatabaseSolution implements Solution
{
public function getSolutionTitle(): string
{
return 'Database is not a MySQL 8 database';
}
public function getSolutionDescription(): string
{
return "Laravel 11 changed the default collation for MySQL and MariaDB. It seems you are trying to use the MySQL 8 collation `utf8mb4_0900_ai_ci` with a MariaDB or MySQL 5.7 database.\n\nEdit the `.env` file and use the correct database in the `DB_CONNECTION` key.";
}
/** @return array<string, string> */
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Spatie\LaravelIgnition\Solutions;
use Illuminate\Support\Str;
use Spatie\Ignition\Contracts\RunnableSolution;
class UseDefaultValetDbCredentialsSolution implements RunnableSolution
{
public function getSolutionActionDescription(): string
{
return 'Pressing the button will change `DB_USER` and `DB_PASSWORD` in your `.env` file.';
}
public function getRunButtonText(): string
{
return 'Use default Valet credentials';
}
public function getSolutionTitle(): string
{
return 'Could not connect to database';
}
public function run(array $parameters = []): void
{
if (! file_exists(base_path('.env'))) {
return;
}
$this->ensureLineExists('DB_USERNAME', 'root');
$this->ensureLineExists('DB_PASSWORD', '');
}
protected function ensureLineExists(string $key, string $value): void
{
$envPath = base_path('.env');
$envLines = array_map(fn (string $envLine) => Str::startsWith($envLine, $key)
? "{$key}={$value}".PHP_EOL
: $envLine, file($envPath) ?: []);
file_put_contents($envPath, implode('', $envLines));
}
public function getRunParameters(): array
{
return [];
}
public function getDocumentationLinks(): array
{
return [
'Valet documentation' => 'https://laravel.com/docs/master/valet',
];
}
public function getSolutionDescription(): string
{
return 'You seem to be using Valet, but the .env file does not contain the right default database credentials.';
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Spatie\LaravelIgnition\Support\Composer;
interface Composer
{
/** @return array<string, mixed> */
public function getClassMap(): array;
/** @return array<string, mixed> */
public function getPrefixes(): array;
/** @return array<string, mixed> */
public function getPrefixesPsr4(): array;
}

View File

@ -0,0 +1,127 @@
<?php
namespace Spatie\LaravelIgnition\Support\Composer;
use function app_path;
use function base_path;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
class ComposerClassMap
{
/** @var \Spatie\LaravelIgnition\Support\Composer\Composer */
protected object $composer;
protected string $basePath;
public function __construct(?string $autoloaderPath = null)
{
$autoloaderPath = $autoloaderPath ?? base_path('/vendor/autoload.php');
$this->composer = file_exists($autoloaderPath)
? require $autoloaderPath
: new FakeComposer();
$this->basePath = app_path();
}
/** @return array<string, string> */
public function listClasses(): array
{
$classes = $this->composer->getClassMap();
return array_merge($classes, $this->listClassesInPsrMaps());
}
public function searchClassMap(string $missingClass): ?string
{
foreach ($this->composer->getClassMap() as $fqcn => $file) {
$basename = basename($file, '.php');
if ($basename === $missingClass) {
return $fqcn;
}
}
return null;
}
/** @return array<string, mixed> */
public function listClassesInPsrMaps(): array
{
// TODO: This is incorrect. Doesnt list all fqcns. Need to parse namespace? e.g. App\LoginController is wrong
$prefixes = array_merge(
$this->composer->getPrefixes(),
$this->composer->getPrefixesPsr4()
);
$classes = [];
foreach ($prefixes as $namespace => $directories) {
foreach ($directories as $directory) {
if (file_exists($directory)) {
$files = (new Finder)
->in($directory)
->files()
->name('*.php');
foreach ($files as $file) {
if ($file instanceof SplFileInfo) {
$fqcn = $this->getFullyQualifiedClassNameFromFile($namespace, $file);
$classes[$fqcn] = $file->getRelativePathname();
}
}
}
}
}
return $classes;
}
public function searchPsrMaps(string $missingClass): ?string
{
$prefixes = array_merge(
$this->composer->getPrefixes(),
$this->composer->getPrefixesPsr4()
);
foreach ($prefixes as $namespace => $directories) {
foreach ($directories as $directory) {
if (file_exists($directory)) {
$files = (new Finder)
->in($directory)
->files()
->name('*.php');
foreach ($files as $file) {
if ($file instanceof SplFileInfo) {
$basename = basename($file->getRelativePathname(), '.php');
if ($basename === $missingClass) {
return $namespace . basename($file->getRelativePathname(), '.php');
}
}
}
}
}
}
return null;
}
protected function getFullyQualifiedClassNameFromFile(string $rootNamespace, SplFileInfo $file): string
{
$class = trim(str_replace($this->basePath, '', (string)$file->getRealPath()), DIRECTORY_SEPARATOR);
$class = str_replace(
[DIRECTORY_SEPARATOR, 'App\\'],
['\\', app()->getNamespace()],
ucfirst(Str::replaceLast('.php', '', $class))
);
return $rootNamespace . $class;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Spatie\LaravelIgnition\Support\Composer;
class FakeComposer implements Composer
{
/** @return array<string, mixed> */
public function getClassMap(): array
{
return [];
}
/** @return array<string, mixed> */
public function getPrefixes(): array
{
return [];
}
/** @return array<string, mixed> */
public function getPrefixesPsr4(): array
{
return [];
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace Spatie\LaravelIgnition\Support;
use InvalidArgumentException;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;
use Spatie\FlareClient\Flare;
use Spatie\FlareClient\Report;
use Throwable;
class FlareLogHandler extends AbstractProcessingHandler
{
protected Flare $flare;
protected SentReports $sentReports;
protected int $minimumReportLogLevel;
public function __construct(Flare $flare, SentReports $sentReports, $level = Level::Debug, $bubble = true)
{
$this->flare = $flare;
$this->minimumReportLogLevel = Level::Error->value;
$this->sentReports = $sentReports;
parent::__construct($level, $bubble);
}
public function setMinimumReportLogLevel(int $level): void
{
if (! in_array($level, Level::VALUES)) {
throw new InvalidArgumentException('The given minimum log level is not supported.');
}
$this->minimumReportLogLevel = $level;
}
protected function write(LogRecord $record): void
{
if (! $this->shouldReport($record->toArray())) {
return;
}
if ($this->hasException($record->toArray())) {
$report = $this->flare->report($record['context']['exception']);
if ($report) {
$this->sentReports->add($report);
}
return;
}
if (config('flare.send_logs_as_events')) {
if ($this->hasValidLogLevel($record->toArray())) {
$this->flare->reportMessage(
$record['message'],
'Log ' . Logger::toMonologLevel($record['level'])->getName(),
function (Report $flareReport) use ($record) {
foreach ($record['context'] as $key => $value) {
$flareReport->context($key, $value);
}
}
);
}
}
}
/**
* @param array<string, mixed> $report
*
* @return bool
*/
protected function shouldReport(array $report): bool
{
if (! config('flare.key')) {
return false;
}
return $this->hasException($report) || $this->hasValidLogLevel($report);
}
/**
* @param array<string, mixed> $report
*
* @return bool
*/
protected function hasException(array $report): bool
{
$context = $report['context'];
return isset($context['exception']) && $context['exception'] instanceof Throwable;
}
/**
* @param array<string, mixed> $report
*
* @return bool
*/
protected function hasValidLogLevel(array $report): bool
{
return $report['level'] >= $this->minimumReportLogLevel;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Spatie\LaravelIgnition\Support;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\LaravelIgnition\Exceptions\ViewException;
use Throwable;
class LaravelDocumentationLinkFinder
{
public function findLinkForThrowable(Throwable $throwable): ?string
{
if ($throwable instanceof ViewException) {
$throwable = $throwable->getPrevious();
}
$majorVersion = LaravelVersion::major();
if (str_contains($throwable->getMessage(), Collection::class)) {
return "https://laravel.com/docs/{$majorVersion}.x/collections#available-methods";
}
$type = $this->getType($throwable);
if (! $type) {
return null;
}
return match ($type) {
'Auth' => "https://laravel.com/docs/{$majorVersion}.x/authentication",
'Broadcasting' => "https://laravel.com/docs/{$majorVersion}.x/broadcasting",
'Container' => "https://laravel.com/docs/{$majorVersion}.x/container",
'Database' => "https://laravel.com/docs/{$majorVersion}.x/eloquent",
'Pagination' => "https://laravel.com/docs/{$majorVersion}.x/pagination",
'Queue' => "https://laravel.com/docs/{$majorVersion}.x/queues",
'Routing' => "https://laravel.com/docs/{$majorVersion}.x/routing",
'Session' => "https://laravel.com/docs/{$majorVersion}.x/session",
'Validation' => "https://laravel.com/docs/{$majorVersion}.x/validation",
'View' => "https://laravel.com/docs/{$majorVersion}.x/views",
default => null,
};
}
protected function getType(?Throwable $throwable): ?string
{
if (! $throwable) {
return null;
}
if (str_contains($throwable::class, 'Illuminate')) {
return Str::between($throwable::class, 'Illuminate\\', '\\');
}
if (str_contains($throwable->getMessage(), 'Illuminate')) {
return explode('\\', Str::between($throwable->getMessage(), 'Illuminate\\', '\\'))[0];
}
return null;
}
}

View File

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

View File

@ -0,0 +1,80 @@
<?php
namespace Spatie\LaravelIgnition\Support;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\LivewireManager;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
class LivewireComponentParser
{
protected string $componentClass;
protected ReflectionClass $reflectionClass;
public static function create(string $componentAlias): self
{
return new self($componentAlias);
}
public function __construct(protected string $componentAlias)
{
$this->componentClass = app(LivewireManager::class)->getClass($this->componentAlias);
$this->reflectionClass = new ReflectionClass($this->componentClass);
}
public function getComponentClass(): string
{
return $this->componentClass;
}
public function getPropertyNamesLike(string $similar): Collection
{
$properties = collect($this->reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC))
// @phpstan-ignore-next-line
->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->class !== $this->reflectionClass->name)
->map(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->name);
$computedProperties = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC))
// @phpstan-ignore-next-line
->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name)
->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'get') && str_ends_with($reflectionMethod->name, 'Property'))
->map(fn (ReflectionMethod $reflectionMethod) => lcfirst(Str::of($reflectionMethod->name)->after('get')->before('Property')));
return $this->filterItemsBySimilarity(
$properties->merge($computedProperties),
$similar
);
}
public function getMethodNamesLike(string $similar): Collection
{
$methods = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC))
// @phpstan-ignore-next-line
->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name)
->map(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->name);
return $this->filterItemsBySimilarity($methods, $similar);
}
protected function filterItemsBySimilarity(Collection $items, string $similar): Collection
{
return $items
->map(function (string $name) use ($similar) {
similar_text($similar, $name, $percentage);
return ['match' => $percentage, 'value' => $name];
})
->sortByDesc('match')
->filter(function (array $item) {
return $item['match'] > 40;
})
->map(function (array $item) {
return $item['value'];
})
->values();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Spatie\LaravelIgnition\Support;
class RunnableSolutionsGuard
{
/**
* Check if runnable solutions are allowed based on the current
* environment and config.
*
* @return bool
*/
public static function check(): bool
{
if (! config('app.debug')) {
// Never run solutions in when debug mode is not enabled.
return false;
}
if (config('ignition.enable_runnable_solutions') !== null) {
// Allow enabling or disabling runnable solutions regardless of environment
// if the IGNITION_ENABLE_RUNNABLE_SOLUTIONS env var is explicitly set.
return config('ignition.enable_runnable_solutions');
}
if (! app()->environment('local') && ! app()->environment('development')) {
// Never run solutions on non-local environments. This avoids exposing
// applications that are somehow APP_ENV=production with APP_DEBUG=true.
return false;
}
return config('app.debug');
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Spatie\LaravelIgnition\Support;
use Illuminate\Support\Arr;
use Spatie\FlareClient\Report;
class SentReports
{
/** @var array<int, Report> */
protected array $reports = [];
public function add(Report $report): self
{
$this->reports[] = $report;
return $this;
}
/** @return array<int, Report> */
public function all(): array
{
return $this->reports;
}
/** @return array<int, string> */
public function uuids(): array
{
return array_map(fn (Report $report) => $report->trackingUuid(), $this->reports);
}
/** @return array<int, string> */
public function urls(): array
{
return array_map(function (string $trackingUuid) {
return "https://flareapp.io/tracked-occurrence/{$trackingUuid}";
}, $this->uuids());
}
public function latestUuid(): ?string
{
return Arr::last($this->reports)?->trackingUuid();
}
public function latestUrl(): ?string
{
return Arr::last($this->urls());
}
public function clear(): void
{
$this->reports = [];
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Spatie\LaravelIgnition\Support;
use Illuminate\Support\Collection;
class StringComparator
{
/**
* @param array<int|string, string> $strings
* @param string $input
* @param int $sensitivity
*
* @return string|null
*/
public static function findClosestMatch(array $strings, string $input, int $sensitivity = 4): ?string
{
$closestDistance = -1;
$closestMatch = null;
foreach ($strings as $string) {
$levenshteinDistance = levenshtein($input, $string);
if ($levenshteinDistance === 0) {
$closestMatch = $string;
$closestDistance = 0;
break;
}
if ($levenshteinDistance <= $closestDistance || $closestDistance < 0) {
$closestMatch = $string;
$closestDistance = $levenshteinDistance;
}
}
if ($closestDistance <= $sensitivity) {
return $closestMatch;
}
return null;
}
/**
* @param array<int, string> $strings
* @param string $input
*
* @return string|null
*/
public static function findSimilarText(array $strings, string $input): ?string
{
if (empty($strings)) {
return null;
}
return Collection::make($strings)
->sortByDesc(function (string $string) use ($input) {
similar_text($input, $string, $percentage);
return $percentage;
})
->first();
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Spatie\LaravelIgnition\Views;
use Illuminate\View\Compilers\BladeCompiler;
use Throwable;
class BladeSourceMapCompiler
{
protected BladeCompiler $bladeCompiler;
public function __construct()
{
$this->bladeCompiler = app('blade.compiler');
}
public function detectLineNumber(string $filename, int $compiledLineNumber): int
{
$map = $this->compileSourcemap((string)file_get_contents($filename));
return $this->findClosestLineNumberMapping($map, $compiledLineNumber);
}
protected function compileSourcemap(string $value): string
{
try {
$value = $this->addEchoLineNumbers($value);
$value = $this->addStatementLineNumbers($value);
$value = $this->addBladeComponentLineNumbers($value);
$value = $this->bladeCompiler->compileString($value);
return $this->trimEmptyLines($value);
} catch (Throwable $e) {
report($e);
return $value;
}
}
protected function addEchoLineNumbers(string $value): string
{
$echoPairs = [['{{', '}}'], ['{{{', '}}}'], ['{!!', '!!}']];
foreach ($echoPairs as $pair) {
// Matches {{ $value }}, {!! $value !!} and {{{ $value }}} depending on $pair
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $pair[0], $pair[1]);
if (preg_match_all($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) {
foreach (array_reverse($matches[0]) as $match) {
$position = mb_strlen(substr($value, 0, $match[1]));
$value = $this->insertLineNumberAtPosition($position, $value);
}
}
}
return $value;
}
protected function addStatementLineNumbers(string $value): string
{
// Matches @bladeStatements() like @if, @component(...), @etc;
$shouldInsertLineNumbers = preg_match_all(
'/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x',
$value,
$matches,
PREG_OFFSET_CAPTURE
);
if ($shouldInsertLineNumbers) {
foreach (array_reverse($matches[0]) as $match) {
$position = mb_strlen(substr($value, 0, $match[1]));
$value = $this->insertLineNumberAtPosition($position, $value);
}
}
return $value;
}
protected function addBladeComponentLineNumbers(string $value): string
{
// Matches the start of `<x-blade-component`
$shouldInsertLineNumbers = preg_match_all(
'/<\s*x[-:]([\w\-:.]*)/mx',
$value,
$matches,
PREG_OFFSET_CAPTURE
);
if ($shouldInsertLineNumbers) {
foreach (array_reverse($matches[0]) as $match) {
$position = mb_strlen(substr($value, 0, $match[1]));
$value = $this->insertLineNumberAtPosition($position, $value);
}
}
return $value;
}
protected function insertLineNumberAtPosition(int $position, string $value): string
{
$before = mb_substr($value, 0, $position);
$lineNumber = count(explode("\n", $before));
return mb_substr($value, 0, $position)."|---LINE:{$lineNumber}---|".mb_substr($value, $position);
}
protected function trimEmptyLines(string $value): string
{
$value = preg_replace('/^\|---LINE:([0-9]+)---\|$/m', '', $value);
return ltrim((string)$value, PHP_EOL);
}
protected function findClosestLineNumberMapping(string $map, int $compiledLineNumber): int
{
$map = explode("\n", $map);
// Max 20 lines between compiled and source line number.
// Blade components can span multiple lines and the compiled line number is often
// a couple lines below the source-mapped `<x-component>` code.
$maxDistance = 20;
$pattern = '/\|---LINE:(?P<line>[0-9]+)---\|/m';
$lineNumberToCheck = $compiledLineNumber - 1;
while (true) {
if ($lineNumberToCheck < $compiledLineNumber - $maxDistance) {
// Something wrong. Return the $compiledLineNumber (unless it's out of range)
return min($compiledLineNumber, count($map));
}
if (preg_match($pattern, $map[$lineNumberToCheck] ?? '', $matches)) {
return (int)$matches['line'];
}
$lineNumberToCheck--;
}
}
}

View File

@ -0,0 +1,177 @@
<?php
namespace Spatie\LaravelIgnition\Views;
use Exception;
use Illuminate\Contracts\View\Engine;
use Illuminate\Foundation\Application;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\View\Engines\PhpEngine;
use Illuminate\View\ViewException;
use ReflectionClass;
use ReflectionProperty;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\LaravelIgnition\Exceptions\ViewException as IgnitionViewException;
use Spatie\LaravelIgnition\Exceptions\ViewExceptionWithSolution;
use Throwable;
class ViewExceptionMapper
{
protected Engine $compilerEngine;
protected BladeSourceMapCompiler $bladeSourceMapCompiler;
protected array $knownPaths;
public function __construct(BladeSourceMapCompiler $bladeSourceMapCompiler)
{
$resolver = app('view.engine.resolver');
$this->compilerEngine = $resolver->resolve('blade');
$this->bladeSourceMapCompiler = $bladeSourceMapCompiler;
}
public function map(ViewException $viewException): IgnitionViewException
{
$baseException = $this->getRealException($viewException);
if ($baseException instanceof IgnitionViewException) {
return $baseException;
}
preg_match('/\(View: (?P<path>.*?)\)/', $viewException->getMessage(), $matches);
$compiledViewPath = $matches['path'];
$exception = $this->createException($baseException);
if ($baseException instanceof ProvidesSolution) {
/** @var ViewExceptionWithSolution $exception */
$exception->setSolution($baseException->getSolution());
}
$this->modifyViewsInTrace($exception);
$exception->setView($compiledViewPath);
$exception->setViewData($this->getViewData($exception));
return $exception;
}
protected function createException(Throwable $baseException): IgnitionViewException
{
$viewExceptionClass = $baseException instanceof ProvidesSolution
? ViewExceptionWithSolution::class
: IgnitionViewException::class;
$viewFile = $this->findCompiledView($baseException->getFile());
$file = $viewFile ?? $baseException->getFile();
$line = $viewFile ? $this->getBladeLineNumber($file, $baseException->getLine()) : $baseException->getLine();
return new $viewExceptionClass(
$baseException->getMessage(),
0,
1,
$file,
$line,
$baseException
);
}
protected function modifyViewsInTrace(IgnitionViewException $exception): void
{
$trace = Collection::make($exception->getPrevious()->getTrace())
->map(function ($trace) {
if ($originalPath = $this->findCompiledView(Arr::get($trace, 'file', ''))) {
$trace['file'] = $originalPath;
$trace['line'] = $this->getBladeLineNumber($trace['file'], $trace['line']);
}
return $trace;
})->toArray();
$traceProperty = new ReflectionProperty('Exception', 'trace');
$traceProperty->setAccessible(true);
$traceProperty->setValue($exception, $trace);
}
/**
* Look at the previous exceptions to find the original exception.
* This is usually the first Exception that is not a ViewException.
*/
protected function getRealException(Throwable $exception): Throwable
{
$rootException = $exception->getPrevious() ?? $exception;
while ($rootException instanceof ViewException && $rootException->getPrevious()) {
$rootException = $rootException->getPrevious();
}
return $rootException;
}
protected function findCompiledView(string $compiledPath): ?string
{
$this->knownPaths ??= $this->getKnownPaths();
return $this->knownPaths[$compiledPath] ?? null;
}
protected function getKnownPaths(): array
{
$compilerEngineReflection = new ReflectionClass($this->compilerEngine);
if (! $compilerEngineReflection->hasProperty('lastCompiled') && $compilerEngineReflection->hasProperty('engine')) {
$compilerEngine = $compilerEngineReflection->getProperty('engine');
$compilerEngine->setAccessible(true);
$compilerEngine = $compilerEngine->getValue($this->compilerEngine);
$lastCompiled = new ReflectionProperty($compilerEngine, 'lastCompiled');
$lastCompiled->setAccessible(true);
$lastCompiled = $lastCompiled->getValue($compilerEngine);
} else {
$lastCompiled = $compilerEngineReflection->getProperty('lastCompiled');
$lastCompiled->setAccessible(true);
$lastCompiled = $lastCompiled->getValue($this->compilerEngine);
}
$knownPaths = [];
foreach ($lastCompiled as $lastCompiledPath) {
$compiledPath = $this->compilerEngine->getCompiler()->getCompiledPath($lastCompiledPath);
$knownPaths[realpath($compiledPath ?? $lastCompiledPath)] = realpath($lastCompiledPath);
}
return $knownPaths;
}
protected function getBladeLineNumber(string $view, int $compiledLineNumber): int
{
return $this->bladeSourceMapCompiler->detectLineNumber($view, $compiledLineNumber);
}
protected function getViewData(Throwable $exception): array
{
foreach ($exception->getTrace() as $frame) {
if (Arr::get($frame, 'class') === PhpEngine::class) {
$data = Arr::get($frame, 'args.1', []);
return $this->filterViewData($data);
}
}
return [];
}
protected function filterViewData(array $data): array
{
// By default, Laravel views get two data keys:
// __env and app. We try to filter them out.
return array_filter($data, function ($value, $key) {
if ($key === 'app') {
return ! $value instanceof Application;
}
return $key !== '__env';
}, ARRAY_FILTER_USE_BOTH);
}
}

View File

@ -0,0 +1,24 @@
<?php
use Spatie\LaravelIgnition\Renderers\ErrorPageRenderer;
if (! function_exists('ddd')) {
function ddd()
{
$args = func_get_args();
if (count($args) === 0) {
throw new Exception('You should pass at least 1 argument to `ddd`');
}
call_user_func_array('dump', $args);
$renderer = app()->make(ErrorPageRenderer::class);
$exception = new Exception('Dump, Die, Debug');
$renderer->render($exception);
die();
}
}

View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Support\Facades\Route;
use Spatie\LaravelIgnition\Http\Controllers\ExecuteSolutionController;
use Spatie\LaravelIgnition\Http\Controllers\HealthCheckController;
use Spatie\LaravelIgnition\Http\Controllers\UpdateConfigController;
use Spatie\LaravelIgnition\Http\Middleware\RunnableSolutionsEnabled;
Route::group([
'as' => 'ignition.',
'prefix' => config('ignition.housekeeping_endpoint_prefix'),
'middleware' => [RunnableSolutionsEnabled::class],
], function () {
Route::get('health-check', HealthCheckController::class)->name('healthCheck');
Route::post('execute-solution', ExecuteSolutionController::class)
->name('executeSolution');
Route::post('update-config', UpdateConfigController::class)->name('updateConfig');
});