vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
@@ -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));
}
}
@@ -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)
);
}
}
@@ -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'],
];
}
}
@@ -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";
}
}
@@ -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!');
}
}
@@ -0,0 +1,43 @@
<?php
namespace DummyNamespace;
use Spatie\ErrorSolutions\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 = [])
{
//
}
}
@@ -0,0 +1,19 @@
<?php
namespace DummyNamespace;
use Spatie\ErrorSolutions\Contracts\HasSolutionsForThrowable;
use Throwable;
class DummyClass implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
return false;
}
public function getSolutions(Throwable $throwable): array
{
return [];
}
}
@@ -0,0 +1,23 @@
<?php
namespace DummyNamespace;
use Spatie\ErrorSolutions\Contracts\Solution;
class DummyClass implements Solution
{
public function getSolutionTitle(): string
{
return '';
}
public function getSolutionDescription(): string
{
return '';
}
public function getDocumentationLinks(): array
{
return [];
}
}
@@ -0,0 +1,9 @@
<?php
namespace Spatie\LaravelIgnition\ContextProviders;
use Spatie\FlareClient\Context\ConsoleContextProvider;
class LaravelConsoleContextProvider extends ConsoleContextProvider
{
}
@@ -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');
}
}
@@ -0,0 +1,129 @@
<?php
namespace Spatie\LaravelIgnition\ContextProviders;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Livewire\LivewireManager;
use Livewire\Mechanisms\ComponentRegistry;
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<int, mixed> */
protected function getLivewireInformation(): array
{
if ($this->request->has('components')) {
$data = [];
foreach ($this->request->get('components') as $component) {
$snapshot = json_decode($component['snapshot'], true);
$class = app(ComponentRegistry::class)->getClass($snapshot['memo']['name']);
$data[] = [
'component_class' => $class ?? null,
'data' => $snapshot['data'],
'memo' => $snapshot['memo'],
'updates' => $this->resolveUpdates($component['updates']),
'calls' => $component['calls'],
];
}
return $data;
}
/** @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;
}
/** @phpstan-ignore-next-line */
$updates = $this->request->input('updates') ?? [];
/** @phpstan-ignore-next-line */
$updates = $this->request->input('updates') ?? [];
return [
[
'component_class' => $componentClass,
'component_alias' => $componentAlias,
'component_id' => $componentId,
'data' => $this->resolveData(),
'updates' => $this->resolveUpdates($updates),
],
];
}
/** @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 $updates): 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);
}
}
@@ -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;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use Spatie\ErrorSolutions\Contracts\BaseSolution;
use Spatie\ErrorSolutions\Contracts\ProvidesSolution;
use Spatie\ErrorSolutions\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.");
}
}
@@ -0,0 +1,31 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use Exception;
use Monolog\Level;
use Spatie\ErrorSolutions\Contracts\BaseSolution;
use Spatie\ErrorSolutions\Contracts\ProvidesSolution;
use Spatie\ErrorSolutions\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}.");
}
}
@@ -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;
}
}
@@ -0,0 +1,21 @@
<?php
namespace Spatie\LaravelIgnition\Exceptions;
use Spatie\ErrorSolutions\Contracts\ProvidesSolution;
use Spatie\ErrorSolutions\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;
}
}
+26
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);
}
}
@@ -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 $next($report);
}
$allContext = Context::all();
if (count($allContext)) {
$report->group('laravel_context', $allContext);
}
return $next($report);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Spatie\LaravelIgnition\FlareMiddleware;
use Closure;
use Spatie\Backtrace\Backtrace;
use Spatie\FlareClient\FlareMiddleware\FlareMiddleware;
use Spatie\FlareClient\Report;
use Throwable;
class AddExceptionHandledStatus implements FlareMiddleware
{
public function handle(Report $report, Closure $next)
{
$frames = Backtrace::create()->limit(40)->frames();
$frameCount = count($frames);
try {
foreach ($frames as $i => $frame) {
// Check first frame, probably Illuminate\Foundation\Exceptions\Handler::report()
// Next frame should be: Illuminate/Foundation/helpers.php::report()
if ($frame->method !== 'report') {
continue;
}
if ($frame->class === null) {
continue;
}
if ($i === $frameCount - 1) {
continue;
}
if ($frames[$i + 1]->class !== null) {
continue;
}
if ($frames[$i + 1]->method !== 'report') {
continue;
}
$report->handled();
break;
}
} catch (Throwable) {
// Do nothing
}
return $next($report);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,52 @@
<?php
namespace Spatie\LaravelIgnition\Http\Controllers;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Spatie\ErrorSolutions\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;
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Spatie\LaravelIgnition\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\ErrorSolutions\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;
}
}
@@ -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'],
];
}
}
@@ -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\ErrorSolutions\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract;
use Spatie\ErrorSolutions\SolutionProviderRepository;
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\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\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();
}
}
@@ -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,
];
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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,
]);
}
}
@@ -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;
}
}
@@ -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.');
}
}
@@ -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,
];
}
}
@@ -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;
}
}
@@ -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,
];
}
}
@@ -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;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Spatie\LaravelIgnition\Renderers;
use Spatie\ErrorSolutions\Contracts\SolutionProviderRepository;
use Spatie\FlareClient\Flare;
use Spatie\Ignition\Config\IgnitionConfig;
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);
}
}
@@ -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();
}
}
@@ -0,0 +1,61 @@
<?php
namespace Spatie\LaravelIgnition\Solutions\SolutionTransformers;
use Spatie\ErrorSolutions\Contracts\RunnableSolution;
use Spatie\ErrorSolutions\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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,11 @@
<?php
namespace Spatie\LaravelIgnition\Support;
class LaravelVersion
{
public static function major(): string
{
return explode('.', app()->version())[0];
}
}
@@ -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');
}
}
@@ -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 = [];
}
}
@@ -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--;
}
}
}
@@ -0,0 +1,190 @@
<?php
namespace Spatie\LaravelIgnition\Views;
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\ErrorSolutions\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
{
$viewIndex = null;
$trace = Collection::make($exception->getPrevious()->getTrace())
->map(function ($trace, $index) use (&$viewIndex) {
if ($originalPath = $this->findCompiledView(Arr::get($trace, 'file', ''))) {
$trace['file'] = $originalPath;
$trace['line'] = $this->getBladeLineNumber($trace['file'], $trace['line']);
if ($viewIndex === null) {
$viewIndex = $index;
}
}
return $trace;
})
->when(
$viewIndex !== null && str_ends_with($exception->getFile(), '.blade.php'),
fn (Collection $trace) => $trace->slice($viewIndex + 1) // Remove all traces before the view
)
->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);
}
}
+24
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();
}
}
+20
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');
});