laravel-6 support

This commit is contained in:
RafficMohammed
2023-01-08 01:17:22 +05:30
parent 1a5c16ae4b
commit 774eed8b0e
4962 changed files with 279380 additions and 297961 deletions

4
vendor/facade/ignition/.babelrc vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": ["@babel/transform-runtime", "@babel/plugin-syntax-dynamic-import"]
}

View File

@@ -0,0 +1,100 @@
name: Run tests
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
php-tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
php: [8.0, 7.4, 7.3, 7.2]
laravel: [6.*, 5.8.*, 5.7.*, 5.6.*, 5.5.*]
dependency-version: [prefer-lowest, prefer-stable]
os: [ubuntu-latest, windows-latest]
include:
- laravel: 6.*
testbench: 4.*
- laravel: 5.8.*
testbench: 3.8.*
- laravel: 5.7.*
testbench: 3.7.*
- laravel: 5.6.*
testbench: 3.6.*
- laravel: 5.5.*
testbench: 3.5.*
exclude:
- laravel: 5.8.*
php: 8.0
- laravel: 5.7.*
php: 8.0
- laravel: 5.6.*
php: 8.0
- laravel: 5.5.*
php: 8.0
- laravel: 5.7.*
php: 7.4
- laravel: 5.6.*
php: 7.4
- laravel: 5.5.*
php: 7.4
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, fileinfo
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
- name: Execute tests
run: vendor/bin/phpunit
- name: Send Slack notification
uses: 8398a7/action-slack@v2
if: ${{ failure() && !! env.SLACK_WEBHOOK }}
with:
status: ${{ job.status }}
author_name: ${{ github.actor }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
js-tests:
runs-on: ubuntu-latest
name: JavaScript tests
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Install dependencies
run: yarn install --non-interactive
- name: Execute tests
run: yarn run jest
- name: Send Slack notification
uses: 8398a7/action-slack@v2
if: ${{ failure() && !! env.SLACK_WEBHOOK }}
with:
status: ${{ job.status }}
author_name: ${{ github.actor }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1 @@
resources/compiled

6
vendor/facade/ignition/.prettierrc vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 4
}

8
vendor/facade/ignition/.styleci.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
preset: laravel
disabled:
- single_class_element_per_statement
finder:
not-name:
- "GitConflictController.php"

282
vendor/facade/ignition/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,282 @@
# Changelog
All notable changes to `ignition` will be documented in this file
## 1.18.0 - 2021-08-02
- disable executing solutions on non-local environments or from non-local IP addresses on version 1.x (#404)
## 1.17.0
- add extra editors (#389)
## 1.16.5 - 2021-02-14
- fix CVE-2021-3129 for facade/ignition 1.16.x (Laravel 6) (#351)
## 1.16.4 - 2021-02-13
do not use, tagged on the wrong branch
## 1.16.3 - 2020-07-13
- do not use missing package solution provider by default (closes #179)
## 1.16.2 - 2020-07-12
- remove ability to fix variable names
## 1.16.0 - 2020-01-21
- add named routes (#197)
## 1.15.0 - 2020-01-21
- add exception to the bottom of the html (#230)
## 1.14.0 - 2020-01-06
- add indicator that solution is running (#212)
## 1.13.1 - 2020-01-02
- Remove external reference for icons (#134)
## 1.13.0 - 2019-11-27
- Allow custom grouping types
## 1.12.1 - 2019-11-25
- Detect multibyte position offsets when adding linenumbers to the blade view - Fixes #193
## 1.12.0 - 2019-11-14
- Add exception to html (#206)
- Add a clear exception when passing no parameters to ddd (#205)
- Ignore JS tests (#215)
- Fix share report route bug
## 1.11.2 - 2019-10-13
- simplify default Laravel installation (#198)
## 1.11.1 - 2019-10-08
- add conditional line number (#182)
## 1.11.0 - 2019-10-08
- add better error messages for missing validation rules (#125)
## 1.10.0 - 2019-10-07
- Add `ignition:make-solution` command
- Add default for query binding option (Fixes #183)
## 1.9.2 - 2019-10-04
- Fix service provider registration (Fixes #177)
## 1.9.1 - 2019-10-01
- collapse vendor frames on windows fix (#176)
## 1.9.0 - 2019-09-27
- add ability to send logs to flare
- add `ddd` function
## 1.8.4 - 2019-09-27
- Resolve configuration from the injected app instead of the helper ([#168](https://github.com/facade/ignition/pull/168))
## 1.8.3 - 2019-09-25
- Remove `select-none` from error message
- Change line clamp behaviour for longer error messages
## 1.8.2 - 2019-09-20
- fix for `TypeError: Cannot set property 'highlightState' of undefined`
## 1.8.1 - 2019-09-20
- Revert javascript assets via URL - Fixes #161
## 1.8.0 - 2019-09-18
- added solution for running Laravel Dusk in production ([#121](https://github.com/facade/ignition/pull/121))
- Automatically fix blade variable typos and optional variables ([#38](https://github.com/facade/ignition/pull/38))
## 1.7.1 - 2019-09-18
- Use url helper to generate housekeeping endpoints
## 1.7.0 - 2019-09-18
- Add the ability to define a query collector max value ([#153](https://github.com/facade/ignition/pull/153))
## 1.6.10 - 2019-09-18
- fix `__invoke` method name in solution ([#151](https://github.com/facade/ignition/pull/151))
## 1.6.9 - 2019-09-18
- Add noscript trace information - fixes [#146](https://github.com/facade/ignition/issues/146)
## 1.6.8 - 2019-09-18
- Use javascript content type for asset response - fixes [#149](https://github.com/facade/ignition/issues/149)
## 1.6.7 - 2019-09-18
- Load javascript assets via URL. Fixes [#16](https://github.com/facade/ignition/issues/16)
## 1.6.6 - 2019-09-16
- Prevent undefined index exception in `TestCommand`
## 1.6.5 - 2019-09-13
- Ignore invalid characters in JSON encoding. Fixes [#138](https://github.com/facade/ignition/issues/138)
## 1.6.4 - 2019-09-13
- add no-index on error page
## 1.6.3 - 2019-09-12
- Fix `RouteNotDefinedSolutionProvider` in Laravel 5
## 1.6.2 - 2019-09-12
- updated publishing tag from default config
## 1.6.1 - 2019-09-12
- Resolve configuration from the injected application instead of the helper - Fixes [#131](https://github.com/facade/ignition/issues/131)
## 1.6.0 - 2019-09-09
- add `RouteNotDefined` solution provider ([#113](https://github.com/facade/ignition/pull/113))
## 1.5.0 - 2019-09-09
- suggest running migrations when a column is missing ([#83](https://github.com/facade/ignition/pull/83))
## 1.4.19 - 2019-09-09
- Remove quotation from git commit url ([#89](https://github.com/facade/ignition/pull/89))
## 1.4.18 - 2019-09-09
- Fix open_basedir restriction when looking up config file. Fixes ([#120](https://github.com/facade/ignition/pull/120))
## 1.4.17 - 2019-09-06
- Remove Inter, Operator from font stack. Fixes [#74](https://github.com/facade/ignition/issues/74)
## 1.4.15 - 2019-09-05
- Use previous exception trace for view exceptions. Fixes [#107](https://github.com/facade/ignition/issues/107)
## 1.4.14 - 2019-09-05
- Use DIRECTORY_SEPARATOR to fix an issue with blade view lookups in Windows
## 1.4.13 - 2019-09-05
- Use Laravel style comments
## 1.4.12 - 2019-09-04
- Use a middleware to protect ignition routes ([#93](https://github.com/facade/ignition/pull/93))
## 1.4.11 - 2019-09-04
- Use exception line number as fallbacks for view errors
## 1.4.10 - 2019-09-04
- Wrap solution provider lookup in a try-catch block
## 1.4.9 - 2019-09-04
- Lookup the first exception when linking to Telescope
## 1.4.8 - 2019-09-04
- pass an empty string to query if no connection name is available - fixes [#86](https://github.com/facade/ignition/issues/86)
## 1.4.7 - 2019-09-04
- Match whoops minimum version constraint with Laravel 6
## 1.4.6 - 2019-09-04
- Use empty array for default ignored solution providers
## 1.4.5 - 2019-09-03
- fix for new Laravel 6 installs
## 1.4.4 - 2019-09-03
- Suggest default database name in Laravel 6
- Add void return type to FlareHandler::write()
## 1.4.3 - 2019-09-03
- allow monolog v2
## 1.4.2 - 2019-09-03
- style fixes
## 1.4.1 - 2019-09-03
- Change `remote-sites-path` and `local-sites-path` config keys to us snake case
## 1.4.0 - 2019-09-03
- add `enable_runnable_solutions` key to config file
## 1.3.0 - 2019-09-02
- add `MergeConflictSolutionProvider`
## 1.2.0 - 2019-09-02
- add `ignored_solution_providers` key to config file
## 1.1.1 - 2019-09-02
- Fixed context tab crash when not using git ([#24](https://github.com/facade/ignition/issues/24))
## 1.1.0 - 2019-09-02
- Fixed an error that removed the ability to register custom blade directives.
- Fixed an error that prevented solution execution in Laravel 5.5 and 5.6
- The "Share" button can now be disabled in the configuration file
- Fixes an error when trying to log `null` values
## 1.0.4 - 2019-09-02
- Check if the authenticated user has a `toArray` method available, before collecting user data
## 1.0.3 - 2019-09-02
- Corrected invalid link in config file
## 1.0.2 - 2019-09-02
- Fixed an error in the `DefaultDbNameSolutionProvider` that could cause an infinite loop in Laravel < 5.6.28
## 1.0.1 - 2019-08-31
- add support for L5.5 & 5.6 ([#21](https://github.com/facade/ignition/pull/21))
## 1.0.0 - 2019-08-30
- initial release

21
vendor/facade/ignition/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Facade <info@facade.company>
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.

27
vendor/facade/ignition/README.md vendored Normal file
View File

@@ -0,0 +1,27 @@
# Ignition: a beautiful error page for Laravel apps
[![Latest Version on Packagist](https://img.shields.io/packagist/v/facade/ignition.svg?style=flat-square)](https://packagist.org/packages/facade/ignition)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/facade/ignition/run-php-tests?label=Tests)
[![Quality Score](https://img.shields.io/scrutinizer/g/facade/ignition.svg?style=flat-square)](https://scrutinizer-ci.com/g/facade/ignition)
[![StyleCI](https://github.styleci.io/repos/204472210/shield?branch=master)](https://github.styleci.io/repos/204472210)
[![Total Downloads](https://img.shields.io/packagist/dt/facade/ignition.svg?style=flat-square)](https://packagist.org/packages/facade/ignition)
[Ignition](https://flareapp.io/docs/ignition-for-laravel/introduction) is a beautiful and customizable error page for Laravel applications running on Laravel 5.5 and newer. It is the default error page for all Laravel 6 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.
![Screenshot of ignition](https://facade.github.io/ignition/screenshot.png)
## Official Documentation
The official documentation for Ignition can be found on the [Flare website](https://flareapp.io/docs/ignition-for-laravel/installation).
### Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

76
vendor/facade/ignition/composer.json vendored Normal file
View File

@@ -0,0 +1,76 @@
{
"name": "facade/ignition",
"description": "A beautiful error page for Laravel applications.",
"keywords": [
"error",
"page",
"laravel",
"flare"
],
"homepage": "https://github.com/facade/ignition",
"license": "MIT",
"require": {
"php": "^7.1|^8.0",
"ext-json": "*",
"ext-mbstring": "*",
"facade/flare-client-php": "^1.3",
"facade/ignition-contracts": "^1.0",
"filp/whoops": "^2.4",
"illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0",
"monolog/monolog": "^1.12 || ^2.0",
"scrivo/highlight.php": "^9.15",
"symfony/console": "^3.4 || ^4.0",
"symfony/var-dumper": "^3.4 || ^4.0"
},
"require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2",
"orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
},
"suggest": {
"laravel/telescope": "^2.0"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"laravel": {
"providers": [
"Facade\\Ignition\\IgnitionServiceProvider"
],
"aliases": {
"Flare": "Facade\\Ignition\\Facades\\Flare"
}
}
},
"autoload": {
"psr-4": {
"Facade\\Ignition\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Facade\\Ignition\\Tests\\": "tests"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"format": [
"vendor/bin/php-cs-fixer fix"
],
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"support": {
"issues": "https://github.com/facade/ignition/issues",
"forum": "https://twitter.com/flareappio",
"source": "https://github.com/facade/ignition",
"docs": "https://flareapp.io/docs/ignition-for-laravel/introduction"
}
}

48
vendor/facade/ignition/config/flare.php vendored Normal file
View File

@@ -0,0 +1,48 @@
<?php
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'),
/*
|--------------------------------------------------------------------------
| Reporting Options
|--------------------------------------------------------------------------
|
| These options determine which information will be transmitted to Flare.
|
*/
'reporting' => [
'anonymize_ips' => true,
'collect_git_information' => true,
'report_queries' => true,
'maximum_number_of_collected_queries' => 200,
'report_query_bindings' => true,
'report_view_data' => true,
'grouping_type' => null,
],
/*
|--------------------------------------------------------------------------
| Reporting Log statements
|--------------------------------------------------------------------------
|
| If this setting is `false` log statements won't be send 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,126 @@
<?php
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"
|
*/
'editor' => env('IGNITION_EDITOR', 'phpstorm'),
/*
|--------------------------------------------------------------------------
| Theme
|--------------------------------------------------------------------------
|
| Here you may specify which theme Ignition should use.
|
| Supported: "light", "dark", "auto"
|
*/
'theme' => env('IGNITION_THEME', 'light'),
/*
|--------------------------------------------------------------------------
| 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),
/*
|--------------------------------------------------------------------------
| 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' => [
Facade\Ignition\SolutionProviders\MissingPackageSolutionProvider::class,
],
/*
|--------------------------------------------------------------------------
| Runnable Solutions
|--------------------------------------------------------------------------
|
| Some solutions that Ignition displays are runnable and can perform
| various tasks. Runnable solutions are enabled when your app has
| debug mode enabled. You may also fully disable this feature.
|
*/
'enable_runnable_solutions' => env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS', null),
/*
|--------------------------------------------------------------------------
| 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', ''),
'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',
];

65
vendor/facade/ignition/package.json vendored Normal file
View File

@@ -0,0 +1,65 @@
{
"private": true,
"scripts": {
"dev": "webpack --mode development --watch",
"build": "NODE_ENV=production webpack --mode production",
"format": "prettier --write 'resources/**/*.{css,js,ts,vue}'"
},
"dependencies": {
"git-url-parse": "^11.1.2",
"highlight.js": "^9.15.6",
"lodash": "^4.17.4",
"markdown-it": "^9.0.1",
"md5": "^2.2.1",
"sql-formatter": "^2.3.3"
},
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-typescript": "^7.3.3",
"@fullhuman/postcss-purgecss": "^1.1.0",
"@types/jest": "^24.0.15",
"@types/lodash": "^4.14.133",
"babel-loader": "^8.0.6",
"css-loader": "^3.0.0",
"husky": "^1.3.1",
"jest": "^24.8.0",
"lint-staged": "^8.1.5",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.6.0",
"prettier": "^1.16.4",
"style-loader": "^0.23.1",
"tailwindcss": "^1.0.4",
"typescript": "^3.5.2",
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.35.0",
"webpack-cli": "^3.3.5"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && yarn build && git add resources/compiled/ignition.js"
}
},
"lint-staged": {
"linters": {
"*.{css,js,ts,vue}": [
"yarn format",
"git add"
]
},
"ignore": [
"resources/compiled/**/*"
]
},
"jest": {
"testPathIgnorePatterns": [
"/node_modules/",
"/__helpers__/"
]
}
}

View File

@@ -0,0 +1,29 @@
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss')('./tailwind.config.js'),
require('postcss-preset-env')(),
process.env.NODE_ENV === 'production'
? purgecss({
content: [
'./resources/js/**/*.js',
'./resources/js/**/*.vue',
'./resources/views/errorPage.php',
],
extractors: [
{
extractor: class {
static extract(content) {
return content.match(/[a-zA-Z0-9-:_/]+/g) || [];
}
},
extensions: ['js', 'php', 'vue'],
},
],
whitelistPatterns: [/hljs/, /sf-dump/, /theme-dark/, /theme-auto/],
})
: '',
],
};

View File

@@ -0,0 +1,3 @@
compiled/*
!compiled/index.html
!compiled/ignition.js

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Vue App</title>
<link href="/flare.js" rel="preload" as="script"></head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/flare.js"></script></body>
</html>

View File

@@ -0,0 +1,63 @@
<!doctype html>
<html class="theme-<?=$config['theme']?>">
<!--
<?=$throwableString?>
-->
<head>
<!-- Hide dumps asap -->
<style>
pre.sf-dump {
display: none !important;
}
</style>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="robots" content="noindex, nofollow">
<title><?= $title ?></title>
<?php foreach ($styles as $script): ?>
<link rel="stylesheet" href="<?=$housekeepingEndpoint?>/styles/<?=$script?>">
<?php endforeach; ?>
</head>
<body class="scrollbar-lg">
<script>
window.data = <?=
$jsonEncode([
'report' => $report,
'config' => $config,
'solutions' => $solutions,
'telescopeUrl' => $telescopeUrl,
'shareEndpoint' => $shareEndpoint,
'defaultTab' => $defaultTab,
'defaultTabProps' => $defaultTabProps,
])
?>
window.tabs = <?=$tabs?>;
</script>
<noscript><pre><?=$throwableString?></pre></noscript>
<div id="app"></div>
<script><?= $getAssetContents('ignition.js') ?></script>
<script>
window.Ignition = window.ignite(window.data);
</script>
<?php foreach ($scripts as $script): ?>
<script src="<?=$housekeepingEndpoint?>/scripts/<?=$script?>"></script>
<?php endforeach; ?>
<script>
Ignition.start();
</script>
<!--
<?=$throwableString?>
-->
</body>
</html>

View File

@@ -0,0 +1,167 @@
<?php
namespace Facade\Ignition\Actions;
use Exception;
use Facade\FlareClient\Http\Client;
use Facade\FlareClient\Truncation\ReportTrimmer;
use Facade\Ignition\Exceptions\UnableToShareErrorException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class ShareReportAction
{
/** @var array */
protected $tabs;
/** @var \Facade\FlareClient\Http\Client */
protected $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function handle(array $report, array $tabs, ?string $lineSelection = null)
{
$this->tabs = $tabs;
$report = $this->filterReport($report);
try {
return $this->client->post('public-reports', [
'report' => $this->trimReport($report),
'tabs' => $tabs,
'lineSelection' => $lineSelection,
]);
} catch (Exception $exception) {
throw new UnableToShareErrorException($exception->getMessage());
}
}
public function filterReport(array $report): array
{
if (! $this->hasTab('stackTraceTab')) {
$report['stacktrace'] = array_slice($report['stacktrace'], 0, 1);
}
if (! $this->hasTab('debugTab')) {
$report['glows'] = [];
}
$report['context'] = $this->filterContextItems($report['context']);
return $report;
}
protected function hasTab(string $tab): bool
{
return in_array($tab, $this->tabs);
}
protected function filterContextItems(array $contextItems): array
{
if (! $this->hasTab('requestTab')) {
$contextItems = $this->removeRequestInformation($contextItems);
}
if (! $this->hasTab('appTab')) {
$contextItems = $this->removeAppInformation($contextItems);
}
if (! $this->hasTab('userTab')) {
$contextItems = $this->removeUserInformation($contextItems);
}
if (! $this->hasTab('contextTab')) {
$contextItems = $this->removeContextInformation($contextItems);
}
if (! $this->hasTab('debugTab')) {
$contextItems = $this->removeDebugInformation($contextItems);
}
return $contextItems;
}
protected function removeRequestInformation(array $contextItems): array
{
Arr::forget($contextItems, 'request');
Arr::forget($contextItems, 'request_data');
Arr::forget($contextItems, 'headers');
Arr::forget($contextItems, 'session');
Arr::forget($contextItems, 'cookies');
return $contextItems;
}
protected function removeAppInformation(array $contextItems): array
{
Arr::forget($contextItems, 'view');
Arr::forget($contextItems, 'route');
return $contextItems;
}
protected function removeUserInformation(array $contextItems): array
{
Arr::forget($contextItems, 'user');
Arr::forget($contextItems, 'request.ip');
Arr::forget($contextItems, 'request.useragent');
return $contextItems;
}
protected function removeContextInformation(array $contextItems): array
{
Arr::forget($contextItems, 'env');
Arr::forget($contextItems, 'git');
Arr::forget($contextItems, 'context');
Arr::forget($contextItems, $this->getCustomContextGroups($contextItems));
return $contextItems;
}
protected function removeDebugInformation(array $contextItems): array
{
Arr::forget($contextItems, 'dumps');
Arr::forget($contextItems, 'glows');
Arr::forget($contextItems, 'logs');
Arr::forget($contextItems, 'queries');
return $contextItems;
}
protected function getCustomContextGroups(array $contextItems): array
{
$predefinedContextItemGroups = [
'request',
'request_data',
'headers',
'session',
'cookies',
'view',
'queries',
'route',
'user',
'env',
'git',
'context',
'logs',
'dumps',
];
return Collection::make($contextItems)
->reject(function ($value, $group) use ($predefinedContextItemGroups) {
return in_array($group, $predefinedContextItemGroups);
})
->keys()
->toArray();
}
protected function trimReport(array $report): array
{
return (new ReportTrimmer())->trim($report);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Facade\Ignition\Commands;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class SolutionMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'ignition:make-solution';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new custom Ignition solution class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Solution';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->option('runnable')
? __DIR__.'/stubs/runnable-solution.stub'
: __DIR__.'/stubs/solution.stub';
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Solutions';
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['runnable', null, InputOption::VALUE_NONE, 'Create runnable solution'],
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Facade\Ignition\Commands;
use Exception;
use Illuminate\Config\Repository;
use Illuminate\Console\Command;
use Illuminate\Log\LogManager;
class TestCommand extends Command
{
protected $signature = 'flare:test';
protected $description = 'Send a test notification to Flare';
/** @var \Illuminate\Config\Repository */
protected $config;
public function handle(Repository $config)
{
$this->config = $config;
$this->checkFlareKey();
if (app()->make('log') instanceof LogManager) {
$this->checkFlareLogger();
}
$this->sendTestException();
}
protected function checkFlareKey()
{
$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()
{
$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`.');
}
$this->info('✅ The Flare logging driver was configured correctly.');
return $this;
}
protected function sendTestException()
{
$testException = new Exception('This is an exception to test if the integration with Flare works.');
try {
app('flare.client')->sendTestReport($testException);
$this->info(PHP_EOL);
} catch (Exception $exception) {
$this->warn('❌ We were unable to send an exception to Flare. Make sure that your key is correct and that you have a valid subscription. '.PHP_EOL.PHP_EOL.'For more info visit the docs on installing Flare in a Laravel project: https://flareapp.io/docs/ignition-for-laravel/introduction');
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 Facade\IgnitionContracts\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,23 @@
<?php
namespace DummyNamespace;
use Facade\IgnitionContracts\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 Facade\Ignition\Context;
use Facade\FlareClient\Context\ConsoleContext;
class LaravelConsoleContext extends ConsoleContext
{
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Facade\Ignition\Context;
use Facade\FlareClient\Context\ContextDetectorInterface;
use Facade\FlareClient\Context\ContextInterface;
use Illuminate\Http\Request;
class LaravelContextDetector implements ContextDetectorInterface
{
public function detectCurrentContext(): ContextInterface
{
if (app()->runningInConsole()) {
return new LaravelConsoleContext($_SERVER['argv'] ?? []);
}
return new LaravelRequestContext(app(Request::class));
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Facade\Ignition\Context;
use Facade\FlareClient\Context\RequestContext;
use Illuminate\Http\Request;
class LaravelRequestContext extends RequestContext
{
/** @var \Illuminate\Http\Request */
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function getUser(): array
{
$user = $this->request->user();
if (! $user) {
return [];
}
try {
if (method_exists($user, 'toFlare')) {
return $user->toFlare();
}
if (method_exists($user, 'toArray')) {
return $user->toArray();
}
} catch (\Throwable $e) {
return [];
}
return [];
}
public function getRoute(): array
{
$route = $this->request->route();
return [
'route' => optional($route)->getName(),
'routeParameters' => $this->getRouteParameters(),
'controllerAction' => optional($route)->getActionName(),
'middleware' => array_values(optional($route)->gatherMiddleware() ?? []),
];
}
protected function getRouteParameters(): array
{
try {
return collect(optional($this->request->route())->parameters ?? [])->toArray();
} catch (\Throwable $e) {
return [];
}
}
public function toArray(): array
{
$properties = parent::toArray();
$properties['route'] = $this->getRoute();
$properties['user'] = $this->getUser();
return $properties;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Facade\Ignition\DumpRecorder;
class Dump
{
/** @var string */
protected $htmlDump;
/** @var ?string */
protected $file;
/** @var ?int */
protected $lineNumber;
/** @var float */
protected $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);
}
public function toArray(): array
{
return [
'html_dump' => $this->htmlDump,
'file' => $this->file,
'line_number' => $this->lineNumber,
'microtime' => $this->microtime,
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Facade\Ignition\DumpRecorder;
use Symfony\Component\VarDumper\Cloner\VarCloner;
class DumpHandler
{
/** @var \Facade\Flare\DumpRecorder\DumpRecorder */
protected $dumpRecorder;
public function __construct(DumpRecorder $dumpRecorder)
{
$this->dumpRecorder = $dumpRecorder;
}
public function dump($value)
{
$data = (new VarCloner)->cloneVar($value);
$this->dumpRecorder->record($data);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Facade\Ignition\DumpRecorder;
use Illuminate\Foundation\Application;
use Illuminate\Support\Arr;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\HtmlDumper as BaseHtmlDumper;
use Symfony\Component\VarDumper\VarDumper;
class DumpRecorder
{
protected $dumps = [];
/** @var \Illuminate\Foundation\Application */
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function register(): self
{
$multiDumpHandler = new MultiDumpHandler();
$this->app->singleton(MultiDumpHandler::class, $multiDumpHandler);
$previousHandler = VarDumper::setHandler(function ($var) use ($multiDumpHandler) {
$multiDumpHandler->dump($var);
});
if ($previousHandler) {
$multiDumpHandler->addHandler($previousHandler);
} else {
$multiDumpHandler->addHandler($this->getDefaultHandler());
}
$multiDumpHandler->addHandler(function ($var) {
$this->app->make(DumpHandler::class)->dump($var);
});
return $this;
}
public function record(Data $data)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7);
$file = Arr::get($backtrace, '6.file');
$lineNumber = Arr::get($backtrace, '6.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;
}
protected function getDefaultHandler()
{
return function ($value) {
$data = (new VarCloner)->cloneVar($value);
$dumper = in_array(PHP_SAPI, ['cli', 'phpdbg']) ? new CliDumper : new BaseHtmlDumper;
$dumper->dump($data);
};
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Facade\Ignition\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
{
protected $dumpHeader = '';
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 parent::dump($data, true, [
'maxDepth' => 3,
'maxStringLength' => 160,
]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Facade\Ignition\DumpRecorder;
class MultiDumpHandler
{
/** @var array */
protected $handlers = [];
public function dump($value)
{
foreach ($this->handlers as $handler) {
$handler($value);
}
}
public function addHandler(callable $callable = null): self
{
$this->handlers[] = $callable;
return $this;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Facade\Ignition\ErrorPage;
use Facade\FlareClient\Report;
use Facade\Ignition\IgnitionConfig;
use Facade\IgnitionContracts\SolutionProviderRepository;
use Illuminate\Foundation\Application;
use Throwable;
class ErrorPageHandler
{
/** @var \Facade\Ignition\IgnitionConfig */
protected $ignitionConfig;
/** @var \Facade\Ignition\Facades\Flare */
protected $flareClient;
/** @var \Facade\Ignition\ErrorPage\Renderer */
protected $renderer;
/** @var \Facade\IgnitionContracts\SolutionProviderRepository */
protected $solutionProviderRepository;
public function __construct(
Application $app,
IgnitionConfig $ignitionConfig,
Renderer $renderer,
SolutionProviderRepository $solutionProviderRepository
) {
$this->flareClient = $app->make('flare.client');
$this->ignitionConfig = $ignitionConfig;
$this->renderer = $renderer;
$this->solutionProviderRepository = $solutionProviderRepository;
}
public function handle(Throwable $throwable, $defaultTab = null, $defaultTabProps = [])
{
$report = $this->flareClient->createReport($throwable);
$solutions = $this->solutionProviderRepository->getSolutionsForThrowable($throwable);
$viewModel = new ErrorPageViewModel(
$throwable,
$this->ignitionConfig,
$report,
$solutions
);
$viewModel->defaultTab($defaultTab, $defaultTabProps);
$this->renderException($viewModel);
}
public function handleReport(Report $report, $defaultTab = null, $defaultTabProps = [])
{
$viewModel = new ErrorPageViewModel(
$report->getThrowable(),
$this->ignitionConfig,
$report,
[]
);
$viewModel->defaultTab($defaultTab, $defaultTabProps);
$this->renderException($viewModel);
}
protected function renderException(ErrorPageViewModel $exceptionViewModel)
{
echo $this->renderer->render(
'errorPage',
$exceptionViewModel->toArray()
);
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace Facade\Ignition\ErrorPage;
use Closure;
use Exception;
use Facade\FlareClient\Report;
use Facade\Ignition\Ignition;
use Facade\Ignition\IgnitionConfig;
use Facade\Ignition\Solutions\SolutionTransformer;
use Illuminate\Contracts\Support\Arrayable;
use Laravel\Telescope\Http\Controllers\HomeController;
use Laravel\Telescope\IncomingExceptionEntry;
use Laravel\Telescope\Telescope;
use Throwable;
class ErrorPageViewModel implements Arrayable
{
/** @var \Throwable|null */
protected $throwable;
/** @var array */
protected $solutions;
/** @var \Facade\Ignition\IgnitionConfig */
protected $ignitionConfig;
/** @var \Facade\FlareClient\Report */
protected $report;
/** @var string */
protected $defaultTab;
/** @var array */
protected $defaultTabProps = [];
public function __construct(?Throwable $throwable, IgnitionConfig $ignitionConfig, Report $report, array $solutions)
{
$this->throwable = $throwable;
$this->ignitionConfig = $ignitionConfig;
$this->report = $report;
$this->solutions = $solutions;
}
public function throwableString(): string
{
if (! $this->throwable) {
return '';
}
return sprintf(
"%s: %s in file %s on line %d\n\n%s\n",
get_class($this->throwable),
$this->throwable->getMessage(),
$this->throwable->getFile(),
$this->throwable->getLine(),
$this->report->getThrowable()->getTraceAsString()
);
}
public function telescopeUrl(): ?string
{
try {
if (! class_exists(Telescope::class)) {
return null;
}
if (! count(Telescope::$entriesQueue)) {
return null;
}
$telescopeEntry = collect(Telescope::$entriesQueue)->first(function ($entry) {
return $entry instanceof IncomingExceptionEntry;
});
if (is_null($telescopeEntry)) {
return null;
}
$telescopeEntryId = (string) $telescopeEntry->uuid;
return url(action([HomeController::class, 'index'])."/exceptions/{$telescopeEntryId}");
} catch (Exception $exception) {
return null;
}
}
public function title(): string
{
return "🧨 {$this->report->getMessage()}";
}
public function config(): array
{
return $this->ignitionConfig->toArray();
}
public function solutions(): array
{
$solutions = [];
foreach ($this->solutions as $solution) {
$solutions[] = (new SolutionTransformer($solution))->toArray();
}
return $solutions;
}
protected function shareEndpoint(): string
{
try {
// use string notation as L5.5 and L5.6 don't support array notation yet
return action('\Facade\Ignition\Http\Controllers\ShareReportController');
} catch (Exception $exception) {
return '';
}
}
public function report(): array
{
return $this->report->toArray();
}
public function jsonEncode($data): string
{
$jsonOptions = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT;
if (version_compare(phpversion(), '7.2', '>=')) {
return json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | $jsonOptions);
}
return json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR | $jsonOptions);
}
public function getAssetContents(string $asset): string
{
$assetPath = __DIR__."/../../resources/compiled/{$asset}";
return file_get_contents($assetPath);
}
public function styles(): array
{
return array_keys(Ignition::styles());
}
public function scripts(): array
{
return array_keys(Ignition::scripts());
}
public function tabs(): string
{
return json_encode(Ignition::$tabs);
}
public function defaultTab(?string $defaultTab, ?array $defaultTabProps)
{
$this->defaultTab = $defaultTab ?? 'StackTab';
if ($defaultTabProps) {
$this->defaultTabProps = $defaultTabProps;
}
}
public function toArray(): array
{
return [
'throwableString' => $this->throwableString(),
'telescopeUrl' => $this->telescopeUrl(),
'shareEndpoint' => $this->shareEndpoint(),
'title' => $this->title(),
'config' => $this->config(),
'solutions' => $this->solutions(),
'report' => $this->report(),
'housekeepingEndpoint' => url(config('ignition.housekeeping_endpoint_prefix', '_ignition')),
'styles' => $this->styles(),
'scripts' => $this->scripts(),
'tabs' => $this->tabs(),
'jsonEncode' => Closure::fromCallable([$this, 'jsonEncode']),
'getAssetContents' => Closure::fromCallable([$this, 'getAssetContents']),
'defaultTab' => $this->defaultTab,
'defaultTabProps' => $this->defaultTabProps,
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Facade\Ignition\ErrorPage;
use Error;
use ErrorException;
use Whoops\Handler\Handler;
class IgnitionWhoopsHandler extends Handler
{
/** @var \Facade\Ignition\ErrorPage\ErrorPageHandler */
protected $errorPageHandler;
/** @var \Throwable */
protected $exception;
public function __construct(ErrorPageHandler $errorPageHandler)
{
$this->errorPageHandler = $errorPageHandler;
}
public function handle(): ?int
{
try {
$this->errorPageHandler->handle($this->exception);
} catch (Error $error) {
// Errors aren't caught by Whoops.
// Convert the error to an exception and throw again.
throw new ErrorException(
$error->getMessage(),
$error->getCode(),
1,
$error->getFile(),
$error->getLine(),
$error
);
}
return Handler::QUIT;
}
/** @param \Throwable $exception */
public function setException($exception): void
{
$this->exception = $exception;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Facade\Ignition\ErrorPage;
use Exception;
use Facade\Ignition\Exceptions\ViewException;
class Renderer
{
/** @var string */
protected $viewPath;
public function __construct(string $viewPath)
{
$this->viewPath = $this->formatPath($viewPath);
}
public function render(string $viewName, array $_data): string
{
ob_start();
$viewFile = "{$this->viewPath}/{$viewName}.php";
try {
extract((array) $_data, EXTR_OVERWRITE);
include $viewFile;
} catch (Exception $exception) {
$viewException = new ViewException($exception->getMessage());
$viewException->setView($viewFile);
$viewException->setViewData($_data);
throw $viewException;
}
return ob_get_clean();
}
protected function formatPath(string $path): string
{
return preg_replace('/(?:\/)+$/u', '', $path).'/';
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Facade\Ignition\Exceptions;
use Exception;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Monolog\Logger;
class InvalidConfig extends Exception implements ProvidesSolution
{
public static function invalidLogLevel(string $logLevel)
{
return new static("Invalid log level `{$logLevel}` specified.");
}
public function getSolution(): Solution
{
$validLogLevels = array_map(function (string $level) {
return strtolower($level);
}, array_keys(Logger::getLevels()));
$validLogLevelsString = implode(',', $validLogLevels);
return BaseSolution::create('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,9 @@
<?php
namespace Facade\Ignition\Exceptions;
use Exception;
class UnableToShareErrorException extends Exception
{
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Facade\Ignition\Exceptions;
use ErrorException;
use Facade\FlareClient\Contracts\ProvidesFlareContext;
use Facade\Ignition\DumpRecorder\HtmlDumper;
class ViewException extends ErrorException implements ProvidesFlareContext
{
/** @var array */
protected $viewData = [];
/** @var string */
protected $view = '';
public function setViewData(array $data)
{
$this->viewData = $data;
}
public function getViewData(): array
{
return $this->viewData;
}
public function setView(string $path)
{
$this->view = $path;
}
protected function dumpViewData($variable): string
{
return (new HtmlDumper())->dumpVariable($variable);
}
public function context(): array
{
$context = [
'view' => [
'view' => $this->view,
],
];
if (config('flare.reporting.report_view_data')) {
$context['view']['data'] = array_map([$this, 'dumpViewData'], $this->viewData);
}
return $context;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Facade\Ignition\Exceptions;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
class ViewExceptionWithSolution extends ViewException implements ProvidesSolution
{
/** @var Solution */
protected $solution;
public function setSolution(Solution $solution)
{
$this->solution = $solution;
}
public function getSolution(): Solution
{
return $this->solution;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Facade\Ignition\Facades;
use Illuminate\Support\Facades\Facade;
/**
* Class Flare.
*
* @method static void glow(string $name, string $messageLevel = \Facade\FlareClient\Enums\MessageLevels::INFO, array $metaData = [])
* @method static void context($key, $value)
* @method static void group(string $groupName, array $properties)
*
* @see \Facade\FlareClient\Flare
*/
class Flare extends Facade
{
protected static function getFacadeAccessor()
{
return 'flare.client';
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Facade\Ignition\Http\Controllers;
use Facade\Ignition\Http\Requests\ExecuteSolutionRequest;
use Facade\IgnitionContracts\SolutionProviderRepository;
use Illuminate\Foundation\Validation\ValidatesRequests;
class ExecuteSolutionController
{
use ValidatesRequests;
public function __invoke(
ExecuteSolutionRequest $request,
SolutionProviderRepository $solutionProviderRepository
) {
$this->ensureLocalEnvironment();
$this->ensureLocalRequest();
$solution = $request->getRunnableSolution();
$solution->run($request->get('parameters', []));
return response('');
}
public function ensureLocalEnvironment()
{
if (! app()->environment('local')) {
abort(403, "Runnable solutions are disabled in non-local environments. Please make sure `APP_ENV` is set correctly. Additionally please make sure `APP_DEBUG` is set to false on ANY production environment!");
}
}
public function ensureLocalRequest()
{
$ipIsPublic = filter_var(
request()->ip(),
FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
if ($ipIsPublic) {
abort(403, "Solutions can only be executed by requests from a local IP address. Please also make sure `APP_DEBUG` is set to false on ANY production environment.");
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Facade\Ignition\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,24 @@
<?php
namespace Facade\Ignition\Http\Controllers;
use Facade\Ignition\Ignition;
use Illuminate\Http\Request;
class ScriptController
{
public function __invoke(Request $request)
{
if (! isset(Ignition::scripts()[$request->script])) {
abort(404, 'Script not found');
}
return response(
file_get_contents(
Ignition::scripts()[$request->script]
),
200,
['Content-Type' => 'application/javascript']
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Facade\Ignition\Http\Controllers;
use Facade\Ignition\Actions\ShareReportAction;
use Facade\Ignition\Exceptions\UnableToShareErrorException;
use Facade\Ignition\Http\Requests\ShareReportRequest;
class ShareReportController
{
public function __invoke(ShareReportRequest $request, ShareReportAction $shareReportAction)
{
try {
return $shareReportAction->handle(json_decode($request->get('report'), true), $request->get('tabs'), $request->get('lineSelection'));
} catch (UnableToShareErrorException $exception) {
abort(500, 'Unable to share the error '.$exception->getMessage());
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Facade\Ignition\Http\Controllers;
use Facade\Ignition\Ignition;
use Illuminate\Http\Request;
class StyleController
{
public function __invoke(Request $request)
{
return response(
file_get_contents(Ignition::styles()[$request->style]),
200, ['Content-Type' => 'text/css']
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Facade\Ignition\Http\Middleware;
use Closure;
use Facade\Ignition\IgnitionConfig;
use Illuminate\Http\Request;
class IgnitionConfigValueEnabled
{
/** @var \Facade\Ignition\IgnitionConfig */
protected $ignitionConfig;
public function __construct(IgnitionConfig $ignitionConfig)
{
$this->ignitionConfig = $ignitionConfig;
}
public function handle(Request $request, Closure $next, string $value)
{
if (! $this->ignitionConfig->toArray()[$value]) {
abort(404);
}
return $next($request);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Facade\Ignition\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IgnitionEnabled
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (! $this->ignitionEnabled()) {
abort(404);
}
return $next($request);
}
protected function ignitionEnabled(): bool
{
return config('app.debug');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Facade\Ignition\Http\Requests;
use Facade\IgnitionContracts\RunnableSolution;
use Facade\IgnitionContracts\Solution;
use Facade\IgnitionContracts\SolutionProviderRepository;
use Illuminate\Foundation\Http\FormRequest;
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,17 @@
<?php
namespace Facade\Ignition\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ShareReportRequest extends FormRequest
{
public function rules(): array
{
return [
'report' => 'required',
'tabs' => 'required|array|min:1',
'lineSelection' => [],
];
}
}

43
vendor/facade/ignition/src/Ignition.php vendored Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace Facade\Ignition;
use Closure;
use Facade\Ignition\Tabs\Tab;
class Ignition
{
/** @var Closure[] */
public static $callBeforeShowingErrorPage = [];
/** @var array */
public static $tabs = [];
public static function tab(Tab $tab)
{
static::$tabs[] = $tab;
}
public static function styles(): array
{
return collect(static::$tabs)->flatMap(function ($tab) {
return $tab->styles;
})
->unique()
->toArray();
}
public static function scripts(): array
{
return collect(static::$tabs)->flatMap(function ($tab) {
return $tab->scripts;
})
->unique()
->toArray();
}
public static function registerAssets(Closure $callable)
{
static::$callBeforeShowingErrorPage[] = $callable;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Facade\Ignition;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
class IgnitionConfig implements Arrayable
{
/** @var array */
protected $options;
public function __construct(array $options = [])
{
$this->options = $this->mergeWithDefaultConfig($options);
}
public function getEditor(): ?string
{
return Arr::get($this->options, 'editor');
}
public function getRemoteSitesPath(): ?string
{
return Arr::get($this->options, 'remote_sites_path');
}
public function getLocalSitesPath(): ?string
{
return Arr::get($this->options, 'local_sites_path');
}
public function getTheme(): ?string
{
return Arr::get($this->options, 'theme');
}
public function getEnableShareButton(): bool
{
if (! app()->isBooted()) {
return false;
}
return Arr::get($this->options, 'enable_share_button', true);
}
public function getEnableRunnableSolutions(): bool
{
$enabled = Arr::get($this->options, 'enable_runnable_solutions', null);
if ($enabled === null) {
$enabled = config('app.debug');
}
return $enabled ?? false;
}
public function toArray(): array
{
return [
'editor' => $this->getEditor(),
'remoteSitesPath' => $this->getRemoteSitesPath(),
'localSitesPath' => $this->getLocalSitesPath(),
'theme' => $this->getTheme(),
'enableShareButton' => $this->getEnableShareButton(),
'enableRunnableSolutions' => $this->getEnableRunnableSolutions(),
'directorySeparator' => DIRECTORY_SEPARATOR,
];
}
protected function mergeWithDefaultConfig(array $options = []): array
{
return array_merge(config('ignition') ?: include __DIR__.'/../config/ignition.php', $options);
}
}

View File

@@ -0,0 +1,433 @@
<?php
namespace Facade\Ignition;
use Facade\FlareClient\Flare;
use Facade\FlareClient\Http\Client;
use Facade\Ignition\Commands\SolutionMakeCommand;
use Facade\Ignition\Commands\TestCommand;
use Facade\Ignition\Context\LaravelContextDetector;
use Facade\Ignition\DumpRecorder\DumpRecorder;
use Facade\Ignition\ErrorPage\IgnitionWhoopsHandler;
use Facade\Ignition\ErrorPage\Renderer;
use Facade\Ignition\Exceptions\InvalidConfig;
use Facade\Ignition\Http\Controllers\ExecuteSolutionController;
use Facade\Ignition\Http\Controllers\HealthCheckController;
use Facade\Ignition\Http\Controllers\ScriptController;
use Facade\Ignition\Http\Controllers\ShareReportController;
use Facade\Ignition\Http\Controllers\StyleController;
use Facade\Ignition\Http\Middleware\IgnitionConfigValueEnabled;
use Facade\Ignition\Http\Middleware\IgnitionEnabled;
use Facade\Ignition\Logger\FlareHandler;
use Facade\Ignition\LogRecorder\LogRecorder;
use Facade\Ignition\Middleware\AddDumps;
use Facade\Ignition\Middleware\AddEnvironmentInformation;
use Facade\Ignition\Middleware\AddGitInformation;
use Facade\Ignition\Middleware\AddLogs;
use Facade\Ignition\Middleware\AddQueries;
use Facade\Ignition\Middleware\AddSolutions;
use Facade\Ignition\Middleware\CustomizeGrouping;
use Facade\Ignition\Middleware\SetNotifierName;
use Facade\Ignition\QueryRecorder\QueryRecorder;
use Facade\Ignition\SolutionProviders\BadMethodCallSolutionProvider;
use Facade\Ignition\SolutionProviders\DefaultDbNameSolutionProvider;
use Facade\Ignition\SolutionProviders\IncorrectValetDbCredentialsSolutionProvider;
use Facade\Ignition\SolutionProviders\InvalidRouteActionSolutionProvider;
use Facade\Ignition\SolutionProviders\MergeConflictSolutionProvider;
use Facade\Ignition\SolutionProviders\MissingAppKeySolutionProvider;
use Facade\Ignition\SolutionProviders\MissingColumnSolutionProvider;
use Facade\Ignition\SolutionProviders\MissingImportSolutionProvider;
use Facade\Ignition\SolutionProviders\MissingPackageSolutionProvider;
use Facade\Ignition\SolutionProviders\RunningLaravelDuskInProductionProvider;
use Facade\Ignition\SolutionProviders\SolutionProviderRepository;
use Facade\Ignition\SolutionProviders\TableNotFoundSolutionProvider;
use Facade\Ignition\SolutionProviders\UndefinedVariableSolutionProvider;
use Facade\Ignition\SolutionProviders\UnknownValidationSolutionProvider;
use Facade\Ignition\SolutionProviders\ViewNotFoundSolutionProvider;
use Facade\Ignition\Views\Engines\CompilerEngine;
use Facade\Ignition\Views\Engines\PhpEngine;
use Facade\IgnitionContracts\SolutionProviderRepository as SolutionProviderRepositoryContract;
use Illuminate\Foundation\Application;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Log\LogManager;
use Illuminate\Queue\QueueManager;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\Engines\CompilerEngine as LaravelCompilerEngine;
use Illuminate\View\Engines\PhpEngine as LaravelPhpEngine;
use Monolog\Logger;
use Throwable;
use Whoops\Handler\HandlerInterface;
class IgnitionServiceProvider extends ServiceProvider
{
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/flare.php' => config_path('flare.php'),
], 'flare-config');
$this->publishes([
__DIR__.'/../config/ignition.php' => config_path('ignition.php'),
], 'ignition-config');
}
$this
->registerViewEngines()
->registerHousekeepingRoutes()
->registerLogHandler()
->registerCommands()
->setupQueue($this->app->queue);
$this->app->make(QueryRecorder::class)->register();
$this->app->make(LogRecorder::class)->register();
$this->app->make(DumpRecorder::class)->register();
}
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/flare.php', 'flare');
$this->mergeConfigFrom(__DIR__.'/../config/ignition.php', 'ignition');
$this
->registerSolutionProviderRepository()
->registerExceptionRenderer()
->registerWhoopsHandler()
->registerIgnitionConfig()
->registerFlare()
->registerLogRecorder()
->registerDumpCollector();
if (config('flare.reporting.report_queries')) {
$this->registerQueryRecorder();
}
if (config('flare.reporting.anonymize_ips')) {
$this->app->get('flare.client')->anonymizeIp();
}
$this->registerBuiltInMiddleware();
}
protected function registerViewEngines()
{
if (! $this->hasCustomViewEnginesRegistered()) {
return $this;
}
$this->app->make('view.engine.resolver')->register('php', function () {
return new PhpEngine();
});
$this->app->make('view.engine.resolver')->register('blade', function () {
return new CompilerEngine($this->app['blade.compiler']);
});
return $this;
}
protected function registerHousekeepingRoutes()
{
if ($this->app->runningInConsole()) {
return $this;
}
Route::group([
'as' => 'ignition.',
'prefix' => config('ignition.housekeeping_endpoint_prefix', '_ignition'),
'middleware' => [IgnitionEnabled::class],
], function () {
Route::get('health-check', HealthCheckController::class)->name('healthCheck');
Route::post('execute-solution', ExecuteSolutionController::class)
->middleware(IgnitionConfigValueEnabled::class.':enableRunnableSolutions')
->name('executeSolution');
Route::post('share-report', ShareReportController::class)
->middleware(IgnitionConfigValueEnabled::class.':enableShareButton')
->name('shareReport');
Route::get('scripts/{script}', ScriptController::class)->name('scripts');
Route::get('styles/{style}', StyleController::class)->name('styles');
});
return $this;
}
protected function registerSolutionProviderRepository()
{
$this->app->singleton(SolutionProviderRepositoryContract::class, function () {
$defaultSolutions = $this->getDefaultSolutions();
return new SolutionProviderRepository($defaultSolutions);
});
return $this;
}
protected function registerExceptionRenderer()
{
$this->app->bind(Renderer::class, function () {
return new Renderer(__DIR__.'/../resources/views/');
});
return $this;
}
protected function registerWhoopsHandler()
{
$this->app->bind(HandlerInterface::class, function (Application $app) {
return $app->make(IgnitionWhoopsHandler::class);
});
return $this;
}
protected function registerIgnitionConfig()
{
$this->app->singleton(IgnitionConfig::class, function () {
$options = [];
try {
if ($configPath = $this->getConfigFileLocation()) {
$options = require $configPath;
}
} catch (Throwable $e) {
// possible open_basedir restriction
}
return new IgnitionConfig($options);
});
return $this;
}
protected function registerFlare()
{
$this->app->singleton('flare.http', function () {
return new Client(
config('flare.key'),
config('flare.secret'),
config('flare.base_url', 'https://flareapp.io/api')
);
});
$this->app->alias('flare.http', Client::class);
$this->app->singleton('flare.client', function () {
$client = new Flare($this->app->get('flare.http'), new LaravelContextDetector, $this->app);
$client->applicationPath(base_path());
$client->stage(config('app.env'));
return $client;
});
$this->app->alias('flare.client', Flare::class);
return $this;
}
protected function registerLogHandler()
{
$this->app->singleton('flare.logger', function ($app) {
$handler = new FlareHandler($app->make('flare.client'));
$logLevelString = config('logging.channels.flare.level', 'error');
$logLevel = $this->getLogLevel($logLevelString);
$handler->setMinimumReportLogLevel($logLevel);
$logger = new Logger('Flare');
$logger->pushHandler($handler);
return $logger;
});
if ($this->app['log'] instanceof LogManager) {
Log::extend('flare', function ($app) {
return $app['flare.logger'];
});
} else {
$this->bindLogListener();
}
return $this;
}
protected function getLogLevel(string $logLevelString): int
{
$logLevel = Logger::getLevels()[strtoupper($logLevelString)] ?? null;
if (! $logLevel) {
throw InvalidConfig::invalidLogLevel($logLevelString);
}
return $logLevel;
}
protected function registerLogRecorder()
{
$logCollector = $this->app->make(LogRecorder::class);
$this->app->singleton(LogRecorder::class);
$this->app->instance(LogRecorder::class, $logCollector);
return $this;
}
protected function registerDumpCollector()
{
$dumpCollector = $this->app->make(DumpRecorder::class);
$this->app->singleton(DumpRecorder::class);
$this->app->instance(DumpRecorder::class, $dumpCollector);
return $this;
}
protected function registerCommands()
{
$this->app->bind('command.flare:test', TestCommand::class);
$this->app->bind('command.make:solution', SolutionMakeCommand::class);
if ($this->app['config']->get('flare.key')) {
$this->commands(['command.flare:test']);
}
if ($this->app['config']->get('ignition.register_commands', false)) {
$this->commands(['command.make:solution']);
}
return $this;
}
protected function registerQueryRecorder()
{
$queryCollector = $this->app->make(QueryRecorder::class);
$this->app->singleton(QueryRecorder::class);
$this->app->instance(QueryRecorder::class, $queryCollector);
return $this;
}
protected function registerBuiltInMiddleware()
{
$middleware = collect([
SetNotifierName::class,
AddEnvironmentInformation::class,
AddLogs::class,
AddDumps::class,
AddQueries::class,
AddSolutions::class,
])
->map(function (string $middlewareClass) {
return $this->app->make($middlewareClass);
});
if (config('flare.reporting.collect_git_information')) {
$middleware[] = (new AddGitInformation());
}
if (! is_null(config('flare.reporting.grouping_type'))) {
$middleware[] = new CustomizeGrouping(config('flare.reporting.grouping_type'));
}
foreach ($middleware as $singleMiddleware) {
$this->app->get('flare.client')->registerMiddleware($singleMiddleware);
}
return $this;
}
protected function getDefaultSolutions(): array
{
return [
IncorrectValetDbCredentialsSolutionProvider::class,
MissingAppKeySolutionProvider::class,
DefaultDbNameSolutionProvider::class,
BadMethodCallSolutionProvider::class,
TableNotFoundSolutionProvider::class,
MissingImportSolutionProvider::class,
MissingPackageSolutionProvider::class,
InvalidRouteActionSolutionProvider::class,
ViewNotFoundSolutionProvider::class,
UndefinedVariableSolutionProvider::class,
MergeConflictSolutionProvider::class,
RunningLaravelDuskInProductionProvider::class,
MissingColumnSolutionProvider::class,
UnknownValidationSolutionProvider::class,
];
}
protected function hasCustomViewEnginesRegistered()
{
$resolver = $this->app->make('view.engine.resolver');
if (! $resolver->resolve('php') instanceof LaravelPhpEngine) {
return false;
}
if (! $resolver->resolve('blade') instanceof LaravelCompilerEngine) {
return false;
}
return true;
}
protected function bindLogListener()
{
$this->app['log']->listen(function (MessageLogged $messageLogged) {
if (config('flare.key')) {
try {
$this->app['flare.logger']->log(
$messageLogged->level,
$messageLogged->message,
$messageLogged->context
);
} catch (Exception $exception) {
return;
}
}
});
}
protected function getConfigFileLocation(): ?string
{
$configFullPath = base_path().DIRECTORY_SEPARATOR.'.ignition';
if (file_exists($configFullPath)) {
return $configFullPath;
}
$configFullPath = Arr::get($_SERVER, 'HOME', '').DIRECTORY_SEPARATOR.'.ignition';
if (file_exists($configFullPath)) {
return $configFullPath;
}
return null;
}
protected function setupQueue(QueueManager $queue)
{
$queue->looping(function () {
$this->app->get('flare.client')->reset();
if (config('flare.reporting.report_queries')) {
$this->app->make(QueryRecorder::class)->reset();
}
$this->app->make(LogRecorder::class)->reset();
$this->app->make(DumpRecorder::class)->reset();
});
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Facade\Ignition\LogRecorder;
use Illuminate\Log\Events\MessageLogged;
class LogMessage
{
/** @var string */
protected $message;
/** @var array */
protected $context;
/** @var string */
protected $level;
/** @var float */
protected $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
);
}
public function toArray()
{
return [
'message' => $this->message,
'level' => $this->level,
'context' => $this->context,
'microtime' => $this->microtime,
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Facade\Ignition\LogRecorder;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\Events\MessageLogged;
class LogRecorder
{
/** @var \Facade\Flare\LogRecorder\LogMessage[] */
protected $logMessages = [];
/** @var \Illuminate\Contracts\Foundation\Application */
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function register(): self
{
$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);
}
public function getLogMessages(): array
{
return $this->toArray();
}
public function toArray(): array
{
$logMessages = [];
foreach ($this->logMessages as $log) {
$logMessages[] = $log->toArray();
}
return $logMessages;
}
protected function shouldIgnore($event): bool
{
if (! isset($event->context['exception'])) {
return false;
}
if (! $event->context['exception'] instanceof Exception) {
return false;
}
return true;
}
public function reset(): void
{
$this->logMessages = [];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Facade\Ignition\Logger;
use Facade\FlareClient\Flare;
use Facade\Ignition\Ignition;
use Facade\Ignition\Tabs\Tab;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use Throwable;
class FlareHandler extends AbstractProcessingHandler
{
/** @var \Facade\FlareClient\Flare */
protected $flare;
protected $minimumReportLogLevel = Logger::ERROR;
public function __construct(Flare $flare, $level = Logger::DEBUG, $bubble = true)
{
$this->flare = $flare;
parent::__construct($level, $bubble);
}
public function setMinimumReportLogLevel(int $level)
{
if (! in_array($level, Logger::getLevels())) {
throw new \InvalidArgumentException('The given minimum log level is not supported.');
}
$this->minimumReportLogLevel = $level;
}
protected function write(array $report): void
{
if (! $this->shouldReport($report)) {
return;
}
if ($this->hasException($report)) {
/** @var Throwable $throwable */
$throwable = $report['context']['exception'];
collect(Ignition::$tabs)
->each(function (Tab $tab) use ($throwable) {
$tab->beforeRenderingErrorPage($this->flare, $throwable);
});
$this->flare->report($report['context']['exception']);
return;
}
if (config('flare.send_logs_as_events')) {
if ($this->hasValidLogLevel($report)) {
$this->flare->reportMessage($report['message'], 'Log '.Logger::getLevelName($report['level']));
}
}
}
protected function shouldReport(array $report): bool
{
return $this->hasException($report) || $this->hasValidLogLevel($report);
}
protected function hasException(array $report): bool
{
$context = $report['context'];
return isset($context['exception']) && $context['exception'] instanceof Throwable;
}
protected function hasValidLogLevel(array $report): bool
{
return $report['level'] >= $this->minimumReportLogLevel;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
use Facade\Ignition\DumpRecorder\DumpRecorder;
class AddDumps
{
/** @var \Facade\Ignition\DumpRecorder\DumpRecorder */
protected $dumpRecorder;
public function __construct(DumpRecorder $dumpRecorder)
{
$this->dumpRecorder = $dumpRecorder;
}
public function handle(Report $report, $next)
{
$report->group('dumps', $this->dumpRecorder->getDumps());
return $next($report);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
class AddEnvironmentInformation
{
public function handle(Report $report, $next)
{
$report->frameworkVersion(app()->version());
$report->group('env', [
'laravel_version' => app()->version(),
'laravel_locale' => app()->getLocale(),
'laravel_config_cached' => app()->configurationIsCached(),
'php_version' => phpversion(),
]);
return $next($report);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
use Symfony\Component\Process\Process;
class AddGitInformation
{
public function handle(Report $report, $next)
{
$report->group('git', [
'hash' => $this->hash(),
'message' => $this->message(),
'tag' => $this->tag(),
'remote' => $this->remote(),
'isDirty' => ! $this->isClean(),
]);
return $next($report);
}
public function hash(): ?string
{
return $this->command("git log --pretty=format:'%H' -n 1");
}
public function message(): ?string
{
return $this->command("git log --pretty=format:'%s' -n 1");
}
public function tag(): ?string
{
return $this->command('git describe --tags --abbrev=0');
}
public function remote(): ?string
{
return $this->command('git config --get remote.origin.url');
}
public function isClean(): bool
{
return empty($this->command('git status -s'));
}
protected function command($command)
{
$process = (new \ReflectionClass(Process::class))->hasMethod('fromShellCommandline')
? Process::fromShellCommandline($command, base_path())
: new Process($command, base_path());
$process->run();
return trim($process->getOutput());
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
use Facade\Ignition\LogRecorder\LogRecorder;
class AddLogs
{
/** @var \Facade\Ignition\LogRecorder\LogRecorder */
protected $logRecorder;
public function __construct(LogRecorder $logRecorder)
{
$this->logRecorder = $logRecorder;
}
public function handle(Report $report, $next)
{
$report->group('logs', $this->logRecorder->getLogMessages());
return $next($report);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
use Facade\Ignition\QueryRecorder\QueryRecorder;
class AddQueries
{
/** @var \Facade\Ignition\QueryRecorder\QueryRecorder */
protected $queryRecorder;
public function __construct(QueryRecorder $queryRecorder)
{
$this->queryRecorder = $queryRecorder;
}
public function handle(Report $report, $next)
{
$report->group('queries', $this->queryRecorder->getQueries());
return $next($report);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
use Facade\IgnitionContracts\SolutionProviderRepository;
class AddSolutions
{
/** @var \Facade\IgnitionContracts\SolutionProviderRepository */
protected $solutionProviderRepository;
public function __construct(SolutionProviderRepository $solutionProviderRepository)
{
$this->solutionProviderRepository = $solutionProviderRepository;
}
public function handle(Report $report, $next)
{
if ($throwable = $report->getThrowable()) {
$solutions = $this->solutionProviderRepository->getSolutionsForThrowable($throwable);
foreach ($solutions as $solution) {
$report->addSolution($solution);
}
}
return $next($report);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Enums\GroupingTypes;
use Facade\FlareClient\Report;
class CustomizeGrouping
{
protected $groupingType;
public function __construct($groupingType)
{
$this->groupingType = $groupingType;
}
public function handle(Report $report, $next)
{
$report->groupByTopFrame();
if ($this->groupingType === GroupingTypes::EXCEPTION) {
$report->groupByException();
}
return $next($report);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Facade\Ignition\Middleware;
use Facade\FlareClient\Report;
class SetNotifierName
{
const NOTIFIER_NAME = 'Laravel Client';
public function handle(Report $report, $next)
{
$report->notifierName(static::NOTIFIER_NAME);
return $next($report);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Facade\Ignition\QueryRecorder;
use Illuminate\Database\Events\QueryExecuted;
class Query
{
/** @var string */
protected $sql;
/** @var float */
protected $time;
/** @var string */
protected $connectionName;
/** @var null|array */
protected $bindings;
/** @var float */
protected $microtime;
public static function fromQueryExecutedEvent(QueryExecuted $queryExecuted, bool $reportBindings = false)
{
return new static(
$queryExecuted->sql,
$queryExecuted->time,
$queryExecuted->connectionName ?? '',
$reportBindings ? $queryExecuted->bindings : null
);
}
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);
}
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,54 @@
<?php
namespace Facade\Ignition\QueryRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Events\QueryExecuted;
class QueryRecorder
{
/** @var \Facade\Ignition\QueryRecorder\Query|[] */
protected $queries = [];
/** @var \Illuminate\Contracts\Foundation\Application */
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function register()
{
$this->app['events']->listen(QueryExecuted::class, [$this, 'record']);
return $this;
}
public function record(QueryExecuted $queryExecuted)
{
$maximumQueries = $this->app['config']->get('flare.reporting.maximum_number_of_collected_queries', 200);
$reportBindings = $this->app['config']->get('flare.reporting.report_query_bindings', true);
$this->queries[] = Query::fromQueryExecutedEvent($queryExecuted, $reportBindings);
$this->queries = array_slice($this->queries, $maximumQueries * -1, $maximumQueries);
}
public function getQueries(): array
{
$queries = [];
foreach ($this->queries as $query) {
$queries[] = $query->toArray();
}
return $queries;
}
public function reset()
{
$this->queries = [];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use BadMethodCallException;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionMethod;
use Throwable;
class BadMethodCallSolutionProvider implements HasSolutionsForThrowable
{
protected const REGEX = '/([a-zA-Z\\\\]+)::([a-zA-Z]+)/m';
public function canSolve(Throwable $throwable): bool
{
if (! $throwable instanceof BadMethodCallException) {
return false;
}
if (is_null($this->getClassAndMethodFromExceptionMessage($throwable->getMessage()))) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
return [
BaseSolution::create('Bad Method Call')
->setSolutionDescription($this->getSolutionDescription($throwable)),
];
}
public function getSolutionDescription(Throwable $throwable): string
{
if (! $this->canSolve($throwable)) {
return '';
}
extract($this->getClassAndMethodFromExceptionMessage($throwable->getMessage()), EXTR_OVERWRITE);
$possibleMethod = $this->findPossibleMethod($class, $method);
return "Did you mean {$class}::{$possibleMethod->name}() ?";
}
protected function getClassAndMethodFromExceptionMessage(string $message): ?array
{
if (! preg_match(self::REGEX, $message, $matches)) {
return null;
}
return [
'class' => $matches[1],
'method' => $matches[2],
];
}
protected function findPossibleMethod(string $class, string $invalidMethodName)
{
return $this->getAvailableMethods($class)
->sortByDesc(function (ReflectionMethod $method) use ($invalidMethodName) {
similar_text($invalidMethodName, $method->name, $percentage);
return $percentage;
})->first();
}
protected function getAvailableMethods($class): Collection
{
$class = new ReflectionClass($class);
return Collection::make($class->getMethods());
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\SuggestUsingCorrectDbNameSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Facades\DB;
use Throwable;
class DefaultDbNameSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if ($this->canTryDatabaseConnection()) {
try {
DB::connection()->select('SELECT 1');
} catch (\Exception $e) {
return in_array(env('DB_DATABASE'), ['homestead', 'laravel']);
}
}
return false;
}
public function getSolutions(Throwable $throwable): array
{
return [new SuggestUsingCorrectDbNameSolution()];
}
protected function canTryDatabaseConnection()
{
return version_compare(app()->version(), '5.6.28', '>');
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\UseDefaultValetDbCredentialsSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Database\QueryException;
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($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 Facade\Ignition\SolutionProviders;
use Facade\Ignition\Support\ComposerClassMap;
use Facade\Ignition\Support\StringComparator;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Str;
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,76 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Str;
use ParseError;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Throwable;
class MergeConflictSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $throwable): bool
{
if (! ($throwable instanceof FatalThrowableError || $throwable instanceof ParseError)) {
return false;
}
if (! $this->hasMergeConflictExceptionMessage($throwable)) {
return false;
}
$file = file_get_contents($throwable->getFile());
if (strpos($file, '=======') === false) {
return false;
}
if (strpos($file, '>>>>>>>') === false) {
return false;
}
return true;
}
public function getSolutions(Throwable $throwable): array
{
$file = file_get_contents($throwable->getFile());
preg_match('/\>\>\>\>\>\>\> (.*?)\n/', $file, $matches);
$source = $matches[1];
$target = $this->getCurrentBranch(basename($throwable->getFile()));
return [
BaseSolution::create("Merge conflict from branch '$source' into $target")
->setSolutionDescription('You have a Git merge conflict. To undo your merge do `git reset --hard HEAD`'),
];
}
private function getCurrentBranch(string $directory): string
{
$branch = "'".trim(shell_exec("cd ${directory}; git branch | grep \\* | cut -d ' ' -f2"))."'";
if (! isset($branch) || $branch === "''") {
$branch = 'current branch';
}
return $branch;
}
protected function hasMergeConflictExceptionMessage(Throwable $throwable): bool
{
// For PHP 7.x and below
if (Str::startsWith($throwable->getMessage(), 'syntax error, unexpected \'<<\'')) {
return true;
}
// For PHP 8+
if (Str::startsWith($throwable->getMessage(), 'syntax error, unexpected token "<<"')) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\GenerateAppKeySolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use RuntimeException;
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 Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\RunMigrationsSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Database\QueryException;
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($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,48 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\SuggestImportSolution;
use Facade\Ignition\Support\ComposerClassMap;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Throwable;
class MissingImportSolutionProvider implements HasSolutionsForThrowable
{
/** @var string */
protected $foundClass;
/** @var \Facade\Ignition\Support\ComposerClassMap */
protected $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);
}
public function getSolutions(Throwable $throwable): array
{
return [new SuggestImportSolution($this->foundClass)];
}
protected function search(string $missingClass)
{
$this->foundClass = $this->composerClassMap->searchClassMap($missingClass);
if (is_null($this->foundClass)) {
$this->foundClass = $this->composerClassMap->searchPsrMaps($missingClass);
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\MissingPackageSolution;
use Facade\Ignition\Support\Packagist\Package;
use Facade\Ignition\Support\Packagist\Packagist;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Str;
use Throwable;
class MissingPackageSolutionProvider implements HasSolutionsForThrowable
{
/** @var \Facade\Ignition\Support\Packagist\Package|null */
protected $package;
public function canSolve(Throwable $throwable): bool
{
$pattern = '/Class \'([^\s]+)\' not found/m';
if (! preg_match($pattern, $throwable->getMessage(), $matches)) {
return false;
}
$class = $matches[1];
if (Str::startsWith($class, app()->getNamespace())) {
return false;
}
$this->package = $this->findPackageFromClassName($class);
return ! is_null($this->package);
}
public function getSolutions(Throwable $throwable): array
{
return [new MissingPackageSolution($this->package)];
}
protected function findPackageFromClassName(string $missingClassName): ?Package
{
if (! $package = $this->findComposerPackageForClassName($missingClassName)) {
return null;
}
return $package->hasNamespaceThatContainsClassName($missingClassName)
? $package
: null;
}
protected function findComposerPackageForClassName(string $className): ?Package
{
$packages = Packagist::findPackagesForClassName($className);
return $packages[0] ?? null;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Exceptions\ViewException;
use Facade\Ignition\Support\StringComparator;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use InvalidArgumentException;
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 (version_compare(Application::VERSION, '6.0.0', '>=')) {
if (! $throwable instanceof RouteNotFoundException) {
return false;
}
}
if (version_compare(Application::VERSION, '6.0.0', '<')) {
if (! $throwable instanceof InvalidArgumentException && ! $throwable instanceof ViewException) {
return false;
}
}
return preg_match(self::REGEX, $throwable->getMessage(), $matches);
}
public function getSolutions(Throwable $throwable): array
{
preg_match(self::REGEX, $throwable->getMessage(), $matches);
$missingRoute = $matches[1] ?? null;
$suggestedRoute = $this->findRelatedRoute($missingRoute);
if ($suggestedRoute) {
return [
BaseSolution::create("{$missingRoute} was not defined.")
->setSolutionDescription("Did you mean `{$suggestedRoute}`?"),
];
}
return [
BaseSolution::create("{$missingRoute} was not defined.")
->setSolutionDescription('Are you sure that the route is defined'),
];
}
protected function findRelatedRoute(string $missingRoute): ?string
{
Route::getRoutes()->refreshNameLookups();
return StringComparator::findClosestMatch(array_keys(Route::getRoutes()->getRoutesByName()), $missingRoute);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Exception;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\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('Laravel Dusk should not be run in production.')
->setSolutionDescription('Install the dependencies with the `--no-dev` flag.'),
BaseSolution::create('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,95 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Facade\IgnitionContracts\SolutionProviderRepository as SolutionProviderRepositoryContract;
use Illuminate\Support\Collection;
use Throwable;
class SolutionProviderRepository implements SolutionProviderRepositoryContract
{
/** @var \Illuminate\Support\Collection */
protected $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();
}
$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(function (string $solutionClass) {
return 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 Facade\Ignition\SolutionProviders;
use Facade\Ignition\Solutions\RunMigrationsSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Database\QueryException;
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($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,82 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Exceptions\ViewException;
use Facade\Ignition\Solutions\MakeViewVariableOptionalSolution;
use Facade\Ignition\Solutions\SuggestCorrectVariableNameSolution;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Throwable;
class UndefinedVariableSolutionProvider implements HasSolutionsForThrowable
{
private $variableName;
private $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 = [];
extract($this->getNameAndView($throwable));
if (! isset($variableName)) {
return [];
}
$solutions = $this->findCorrectVariableSolutions($throwable, $variableName, $viewFile);
$solutions[] = $this->findOptionalVariableSolution($variableName, $viewFile);
return $solutions;
}
protected function findCorrectVariableSolutions(Throwable $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(function ($var, $key) {
return $var['match'] > 40;
})->keys()->map(function ($suggestion) use ($variableName, $viewFile) {
return new SuggestCorrectVariableNameSolution($variableName, $viewFile, $suggestion);
})->map(function ($solution) {
return $solution->isRunnable()
? $solution
: BaseSolution::create($solution->getSolutionTitle())
->setSolutionDescription($solution->getSolutionActionDescription());
})->toArray();
}
protected function findOptionalVariableSolution(string $variableName, string $viewFile)
{
$optionalSolution = new MakeViewVariableOptionalSolution($variableName, $viewFile);
return $optionalSolution->isRunnable()
? $optionalSolution
: BaseSolution::create($optionalSolution->getSolutionTitle())
->setSolutionDescription($optionalSolution->getSolutionActionDescription());
}
protected function getNameAndView(Throwable $throwable): ?array
{
$pattern = '/Undefined variable: (.*?) \(View: (.*?)\)/';
preg_match($pattern, $throwable->getMessage(), $matches);
if (count($matches) === 3) {
[$string, $variableName, $viewFile] = $matches;
return compact('variableName', 'viewFile');
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use BadMethodCallException;
use Facade\Ignition\Support\StringComparator;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\Validator;
use ReflectionClass;
use ReflectionMethod;
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('Unknown Validation Rule')
->setSolutionDescription($this->getSolutionDescription($throwable)),
];
}
protected function getSolutionDescription(Throwable $throwable): string
{
$method = $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(function (string $extension) {
return 'validate'.Str::studly($extension);
});
return Collection::make($class->getMethods())
->filter(function (ReflectionMethod $method) {
return preg_match('/(validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/', $method->name);
})
->map(function (ReflectionMethod $method) {
return $method->name;
})
->merge($extensions);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Facade\Ignition\SolutionProviders;
use Facade\Ignition\Exceptions\ViewException;
use Facade\Ignition\Support\StringComparator;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\HasSolutionsForThrowable;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\View;
use InvalidArgumentException;
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 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("{$missingView} was not found.")
->setSolutionDescription("Did you mean `{$suggestedView}`?"),
];
}
return [
BaseSolution::create("{$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);
}
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(function (string $path) use ($extensions) {
return $this->getViewsInPath($path, $extensions);
})
->map(function (string $view) use ($namespace) {
return "{$namespace}::{$view}";
})
->toArray();
});
$viewsForViewPaths = collect($fileViewFinder->getPaths())
->flatMap(function (string $path) use ($extensions) {
return $this->getViewsInPath($path, $extensions);
});
return $viewsForHints->merge($viewsForViewPaths)->toArray();
}
protected function getViewsInPath(string $path, array $extensions): array
{
$filePatterns = array_map(function (string $extension) {
return "*.{$extension}";
}, $extensions);
$extensionsWithDots = array_map(function (string $extension) {
return ".{$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,46 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Illuminate\Support\Facades\Artisan;
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 = [])
{
Artisan::call('key:generate');
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
class MakeViewVariableOptionalSolution implements RunnableSolution
{
/** @var string */
private $variableName;
/** @var string */
private $viewFile;
public function __construct($variableName = null, $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
{
$path = str_replace(base_path().'/', '', $this->viewFile);
$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,
];
}
public function isRunnable(array $parameters = [])
{
return $this->makeOptional($this->getRunParameters()) !== false;
}
public function run(array $parameters = [])
{
$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;
}
public function makeOptional(array $parameters = [])
{
if (!$this->isSafePath($parameters['viewFile'])) {
return false;
}
$originalContents = 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;
}
protected function generateExpectedTokens(array $originalTokens, string $variableName): array
{
$expectedTokens = [];
foreach ($originalTokens as $key => $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,42 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\Ignition\Support\Packagist\Package;
use Facade\IgnitionContracts\Solution;
class MissingPackageSolution implements Solution
{
/** @var \Facade\Flare\Support\Packagist\Package */
protected $possiblePackage;
public function __construct(Package $possiblePackage)
{
$this->possiblePackage = $possiblePackage;
}
public function getSolutionTitle(): string
{
return 'A composer dependency is missing';
}
public function getSolutionDescription(): string
{
$output = [
'You might be missing a composer dependency.',
'A possible package that was found is `'.$this->possiblePackage->name.'`.',
'',
'See if this is the package that you need and install it via `composer require '.$this->possiblePackage->name.'`.',
];
return implode(PHP_EOL, $output);
}
public function getDocumentationLinks(): array
{
return [
'Git repository' => $this->possiblePackage->repository,
'Package on Packagist' => $this->possiblePackage->url,
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Illuminate\Support\Facades\Artisan;
class RunMigrationsSolution implements RunnableSolution
{
private $customTitle;
public function __construct($customTitle = '')
{
$this->customTitle = $customTitle;
}
public function getSolutionTitle(): string
{
return $this->customTitle;
}
public function getSolutionDescription(): string
{
return 'You might have forgotten to run your migrations. You can run your migrations using `php artisan migrate`.';
}
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 'Pressing the button below will try to run your migrations.';
}
public function getRunButtonText(): string
{
return 'Run migrations';
}
public function run(array $parameters = [])
{
Artisan::call('migrate');
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Facade\IgnitionContracts\Solution;
use Illuminate\Contracts\Support\Arrayable;
class SolutionTransformer implements Arrayable
{
/** @var \Facade\IgnitionContracts\Solution */
protected $solution;
public function __construct(Solution $solution)
{
$this->solution = $solution;
}
public function toArray(): array
{
$isRunnable = ($this->solution instanceof RunnableSolution);
return [
'class' => get_class($this->solution),
'title' => $this->solution->getSolutionTitle(),
'description' => $this->solution->getSolutionDescription(),
'links' => $this->solution->getDocumentationLinks(),
'is_runnable' => $isRunnable,
'run_button_text' => $isRunnable ? $this->solution->getRunButtonText() : '',
'run_parameters' => $isRunnable ? $this->solution->getRunParameters() : [],
'action_description' => $isRunnable ? $this->solution->getSolutionActionDescription() : '',
'execute_endpoint' => action('\Facade\Ignition\Http\Controllers\ExecuteSolutionController'),
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\Solution;
class SuggestCorrectVariableNameSolution implements Solution
{
/** @var string */
private $variableName;
/** @var string */
private $viewFile;
public function __construct($variableName = null, $viewFile = null, $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
{
$path = str_replace(base_path().'/', '', $this->viewFile);
return "Did you mean `$$this->suggested`?";
}
public function isRunnable(): bool
{
return false;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\Solution;
class SuggestImportSolution implements Solution
{
/** @var string */
protected $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,27 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\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.";
}
public function getDocumentationLinks(): array
{
return [
'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration',
];
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Facade\Ignition\Solutions;
use Facade\IgnitionContracts\RunnableSolution;
use Illuminate\Support\Str;
class UseDefaultValetDbCredentialsSolution implements RunnableSolution
{
public function getSolutionActionDescription(): string
{
return 'Pressing the button below 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 = [])
{
if (! file_exists(base_path('.env'))) {
return;
}
$this->ensureLineExists('DB_USERNAME', 'root');
$this->ensureLineExists('DB_PASSWORD', '');
}
protected function ensureLineExists(string $key, string $value)
{
$envPath = base_path('.env');
$envLines = array_map(function (string $envLine) use ($value, $key) {
return Str::startsWith($envLine, $key)
? "{$key}={$value}".PHP_EOL
: $envLine;
}, file($envPath));
file_put_contents($envPath, implode('', $envLines));
}
public function getDocumentationLinks(): array
{
return [];
}
public function getRunParameters(): 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,121 @@
<?php
namespace Facade\Ignition\Support;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
class ComposerClassMap
{
/** @var \Composer\Autoload\ClassLoader|FakeComposer */
protected $composer;
/** @var string */
protected $basePath;
public function __construct(?string $autoloaderPath = null)
{
$autoloaderPath = $autoloaderPath ?? base_path('/vendor/autoload.php');
if (file_exists($autoloaderPath)) {
$this->composer = require $autoloaderPath;
} else {
$this->composer = new FakeComposer();
}
$this->basePath = app_path();
}
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;
}
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) {
$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) {
$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, '', $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,21 @@
<?php
namespace Facade\Ignition\Support;
class FakeComposer
{
public function getClassMap()
{
return [];
}
public function getPrefixes()
{
return [];
}
public function getPrefixesPsr4()
{
return [];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Facade\Ignition\Support\Packagist;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class Package
{
/** @var string */
public $name;
/** @var string */
public $url;
/** @var string */
public $repository;
public function __construct(array $properties)
{
$this->name = $properties['name'];
$this->url = $properties['url'];
$this->repository = $properties['repository'];
}
public function hasNamespaceThatContainsClassName(string $className): bool
{
return $this->getNamespaces()->contains(function ($namespace) use ($className) {
return Str::startsWith(strtolower($className), strtolower($namespace));
});
}
protected function getNamespaces(): Collection
{
$details = json_decode(file_get_contents("https://packagist.org/packages/{$this->name}.json"), true);
return collect($details['package']['versions'])
->map(function ($version) {
return collect($version['autoload'] ?? [])
->map(function ($autoload) {
return array_keys($autoload);
})
->flatten();
})
->flatten()
->unique();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Facade\Ignition\Support\Packagist;
class Packagist
{
/**
* @param string $className
*
* @return \Facade\Flare\Support\Packagist\Package[]
*/
public static function findPackagesForClassName(string $className): array
{
$parts = explode('\\', $className);
$queryParts = array_splice($parts, 0, 2);
$url = 'https://packagist.org/search.json?q='.implode(' ', $queryParts);
try {
$packages = json_decode(file_get_contents($url));
} catch (\Exception $e) {
return [];
}
return array_map(function ($packageProperties) {
return new Package((array) $packageProperties);
}, $packages->results);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Facade\Ignition\Support;
use Illuminate\Support\Collection;
class StringComparator
{
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;
}
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();
}
}

75
vendor/facade/ignition/src/Tabs/Tab.php vendored Normal file
View File

@@ -0,0 +1,75 @@
<?php
namespace Facade\Ignition\Tabs;
use Facade\FlareClient\Flare;
use Illuminate\Support\Str;
use JsonSerializable;
use Throwable;
abstract class Tab implements JsonSerializable
{
public $scripts = [];
public $styles = [];
/** @var \Facade\Ignition\Facades\Flare */
protected $flare;
/** @var Throwable */
protected $throwable;
public function __construct()
{
$this->registerAssets();
}
public function name(): string
{
return Str::studly(class_basename(get_called_class()));
}
public function component(): string
{
return Str::snake(class_basename(get_called_class()), '-');
}
public function beforeRenderingErrorPage(Flare $flare, Throwable $throwable)
{
$this->flare = $flare;
$this->throwable = $throwable;
}
public function script(string $name, string $path)
{
$this->scripts[$name] = $path;
return $this;
}
public function style(string $name, string $path)
{
$this->styles[$name] = $path;
return $this;
}
abstract protected function registerAssets();
public function meta(): array
{
return [];
}
public function jsonSerialize()
{
return [
'title' => $this->name(),
'component' => $this->component(),
'props' => [
'meta' => $this->meta(),
],
];
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Facade\Ignition\Views\Compilers;
use Illuminate\View\Compilers\BladeCompiler;
class BladeSourceMapCompiler extends BladeCompiler
{
public function detectLineNumber(string $filename, int $exceptionLineNumber): int
{
$map = $this->compileString(file_get_contents($filename));
$map = explode("\n", $map);
$line = $map[$exceptionLineNumber - $this->getExceptionLineOffset()] ?? $exceptionLineNumber;
$pattern = '/\|---LINE:([0-9]+)---\|/m';
if (preg_match($pattern, $line, $matches)) {
return $matches[1];
}
return $exceptionLineNumber;
}
protected function getExceptionLineOffset(): int
{
/*
* Laravel 5.8.0- 5.8.9 added the view name as a comment in the compiled view on a new line.
* That's why the offset to detect the correct line number must be 2 instead of 1.
*/
if (version_compare(app()->version(), '5.8.0', '>=') &&
version_compare(app()->version(), '5.8.9', '<=')
) {
return 2;
}
return 1;
}
public function compileString($value)
{
try {
$value = $this->addEchoLineNumbers($value);
$value = $this->addStatementLineNumbers($value);
$value = parent::compileString($value);
return $this->trimEmptyLines($value);
} catch (\Exception $e) {
return $value;
}
}
protected function addEchoLineNumbers(string $value)
{
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->contentTags[0], $this->contentTags[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)
{
$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 insertLineNumberAtPosition(int $position, string $value)
{
$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)
{
$value = preg_replace('/^\|---LINE:([0-9]+)---\|$/m', '', $value);
return ltrim($value, PHP_EOL);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Facade\Ignition\Views\Concerns;
use Illuminate\Foundation\Application;
use Illuminate\Support\Collection;
use Illuminate\View\Engines\CompilerEngine;
trait CollectsViewExceptions
{
protected $lastCompiledData = [];
public function collectViewData($path, array $data): void
{
$this->lastCompiledData[] = [
'path' => $path,
'compiledPath' => $this->getCompiledPath($path),
'data' => $this->filterViewData($data),
];
}
public function filterViewData(array $data): array
{
// By default, Laravel views get two shared 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);
}
public function getCompiledViewData($compiledPath): array
{
$compiledView = $this->findCompiledView($compiledPath);
return $compiledView['data'] ?? [];
}
public function getCompiledViewName($compiledPath): string
{
$compiledView = $this->findCompiledView($compiledPath);
return $compiledView['path'] ?? $compiledPath;
}
protected function findCompiledView($compiledPath): ?array
{
return Collection::make($this->lastCompiledData)
->first(function ($compiledData) use ($compiledPath) {
$comparePath = $compiledData['compiledPath'];
return realpath(dirname($comparePath)).DIRECTORY_SEPARATOR.basename($comparePath) === $compiledPath;
});
}
protected function getCompiledPath($path): string
{
if ($this instanceof CompilerEngine) {
return $this->getCompiler()->getCompiledPath($path);
}
return $path;
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Facade\Ignition\Views\Engines;
use Exception;
use Facade\Ignition\Exceptions\ViewException;
use Facade\Ignition\Exceptions\ViewExceptionWithSolution;
use Facade\Ignition\Views\Compilers\BladeSourceMapCompiler;
use Facade\Ignition\Views\Concerns\CollectsViewExceptions;
use Facade\IgnitionContracts\ProvidesSolution;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use ReflectionProperty;
class CompilerEngine extends \Illuminate\View\Engines\CompilerEngine
{
use CollectsViewExceptions;
protected $currentPath = null;
/**
* Get the evaluated contents of the view.
*
* @param string $path
* @param array $data
*
* @return string
*/
public function get($path, array $data = [])
{
$this->currentPath = $path;
$this->collectViewData($path, $data);
return parent::get($path, $data);
}
/**
* Handle a view exception.
*
* @param \Exception $baseException
* @param int $obLevel
*
* @return void
*
* @throws \Exception
*/
protected function handleViewException(Exception $baseException, $obLevel)
{
while (ob_get_level() > $obLevel) {
ob_end_clean();
}
if ($baseException instanceof ViewException) {
throw $baseException;
}
$viewExceptionClass = ViewException::class;
if (in_array(ProvidesSolution::class, class_implements($baseException))) {
$viewExceptionClass = ViewExceptionWithSolution::class;
}
$exception = new $viewExceptionClass(
$this->getMessage($baseException),
0,
1,
$this->getCompiledViewName($baseException->getFile()),
$this->getBladeLineNumber($baseException->getFile(), $baseException->getLine()),
$baseException
);
if ($viewExceptionClass === ViewExceptionWithSolution::class) {
$exception->setSolution($baseException->getSolution());
}
$this->modifyViewsInTrace($exception);
$exception->setView($this->getCompiledViewName($baseException->getFile()));
$exception->setViewData($this->getCompiledViewData($baseException->getFile()));
throw $exception;
}
protected function getBladeLineNumber(string $compiledPath, int $exceptionLineNumber): int
{
$viewPath = $this->getCompiledViewName($compiledPath);
if (! $viewPath) {
return $exceptionLineNumber;
}
$sourceMapCompiler = new BladeSourceMapCompiler(app(Filesystem::class), 'not-needed');
return $sourceMapCompiler->detectLineNumber($viewPath, $exceptionLineNumber);
}
protected function modifyViewsInTrace(ViewException $exception)
{
$trace = Collection::make($exception->getPrevious()->getTrace())
->map(function ($trace) {
if ($compiledData = $this->findCompiledView(Arr::get($trace, 'file', ''))) {
$trace['file'] = $compiledData['path'];
$trace['line'] = $this->getBladeLineNumber($trace['file'], $trace['line']);
}
return $trace;
})->toArray();
$traceProperty = new ReflectionProperty('Exception', 'trace');
$traceProperty->setAccessible(true);
$traceProperty->setValue($exception, $trace);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Facade\Ignition\Views\Engines;
use Exception;
use Facade\Ignition\Exceptions\ViewException;
use Facade\Ignition\Views\Concerns\CollectsViewExceptions;
class PhpEngine extends \Illuminate\View\Engines\PhpEngine
{
use CollectsViewExceptions;
/**
* Get the evaluated contents of the view.
*
* @param string $path
* @param array $data
* @return string
*/
public function get($path, array $data = [])
{
$this->collectViewData($path, $data);
return parent::get($path, $data);
}
/**
* Handle a view exception.
*
* @param \Exception $baseException
* @param int $obLevel
*
* @return void
*
* @throws \Exception
*/
protected function handleViewException(Exception $baseException, $obLevel)
{
$exception = new ViewException($baseException->getMessage(), 0, 1, $baseException->getFile(), $baseException->getLine(), $baseException);
$exception->setView($this->getCompiledViewName($baseException->getFile()));
$exception->setViewData($this->getCompiledViewData($baseException->getFile()));
parent::handleViewException($exception, $obLevel);
}
}

29
vendor/facade/ignition/src/helpers.php vendored Normal file
View File

@@ -0,0 +1,29 @@
<?php
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);
$handler = app(\Facade\Ignition\ErrorPage\ErrorPageHandler::class);
$client = app()->make('flare.client');
$report = $client->createReportFromMessage('Dump, Die, Debug', 'info');
$handler->handleReport($report, 'DebugTab', [
'dump' => true,
'glow' => false,
'log' => false,
'query' => false,
]);
die();
}
}

Some files were not shown because too many files have changed in this diff Show More