diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2e7acaf5d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..41015da35 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto + +/.github export-ignore +/tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +changelog.md export-ignore +phpunit.xml.dist export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..41f9ae41c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +github: barryvdh +custom: ['https://fruitcake.nl'] + diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 000000000..27bcee3fb --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,4 @@ +template: | + ## What’s Changed + + $CHANGES diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..36cd06661 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,24 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - bug + - enhancement + - discussion +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. + + If this issue is still present on the latest version of this library on supported Laravel versions, + please let us know by replying to this issue so we can investigate further. + + Thank you for your contribution! Apologies for any delayed response on our side. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false +# Limit to only `issues` or `pulls` +only: issues diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 000000000..84492e314 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,41 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v6 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml new file mode 100644 index 000000000..2f78f9543 --- /dev/null +++ b/.github/workflows/run-integration-tests.yml @@ -0,0 +1,63 @@ +name: Integration Tests + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + schedule: + - cron: '0 0 * * *' + +jobs: + php-laravel-integration-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + strategy: + fail-fast: false + matrix: + php: [8.4, 8.3, 8.2, 8.1] + laravel: [12.*, 11.*, 10.*, 9.*] + exclude: + - laravel: 12.* + php: 8.1 + - laravel: 11.* + php: 8.1 + - laravel: 9.* + php: 8.4 + name: P${{ matrix.php }} - Laravel${{ matrix.laravel }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + path: src + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: | + composer create-project --prefer-dist laravel/laravel:${{ matrix.laravel }} --stability=dev --no-progress sample + cd sample + composer config minimum-stability dev + composer update --prefer-stable --prefer-dist --no-progress + - name: Add package from source + run: | + cd sample + sed -e 's|"type": "project",|&\n"repositories": [ { "type": "path", "url": "../src" } ],|' -i composer.json + composer require --dev "barryvdh/laravel-debugbar:*" + - name: Execute generate run + run: | + cd sample + mkdir -p "storage/debugbar/" && touch "storage/debugbar/foo.json" + php artisan debugbar:clear + - name: Check file count in logs + run: | + if [ `ls -1q "sample/storage/debugbar/" | wc -l` -gt 0 ];then exit 1;fi diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..8cf1eff4b --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,64 @@ +name: Unit Tests + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + schedule: + - cron: '0 0 * * *' + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + fail-fast: false + matrix: + php: [8.4, 8.3, 8.2, 8.1] + laravel: [^12, ^11, ^10, ^9] + dependency-version: [prefer-stable] + exclude: + - laravel: ^12 + php: 8.1 + - laravel: ^11 + php: 8.1 + - laravel: ^9 + php: 8.4 + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: composer:v2 + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress + + - name: Update Dusk Chromedriver + run: vendor/bin/dusk-updater detect --auto-update + + - name: Execute Unit Tests + run: composer test + + - name: Upload Failed Screenshots + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: tests/Browser/screenshots/* diff --git a/.github/workflows/update-changelog.yaml b/.github/workflows/update-changelog.yaml new file mode 100644 index 000000000..c7832e5ee --- /dev/null +++ b/.github/workflows/update-changelog.yaml @@ -0,0 +1,34 @@ +name: "Update Changelog" + +on: + release: + types: [released] + +jobs: + update: + runs-on: ubuntu-latest + + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the + # updated CHANGELOG back to the repository. + # https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/ + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.event.release.tag_name }} + release-notes: ${{ github.event.release.body }} + + - name: Commit updated CHANGELOG + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: ${{ github.event.release.target_commitish }} + commit_message: Update CHANGELOG + file_pattern: CHANGELOG.md diff --git a/.gitignore b/.gitignore index 2c1fc0c14..0f88edc4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ +/.idea +/build /vendor composer.phar composer.lock -.DS_Store \ No newline at end of file +.DS_Store +.phpunit* +/tests/Browser diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..7a278ac2d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,172 @@ +# Changelog + +## v3.16.0 - 2025-07-21 + +### What's Changed + +* Make all scalar config values configurable through environment variables by @wimski in https://github.com/barryvdh/laravel-debugbar/pull/1784 +* Check if file exists on FilesystemStorage by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1790 +* Bump php-debugbar by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1791 +* Fix counter tests by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1792 +* `$group` arg support on TimelineCollectors methods by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1789 +* Collect other eloquent model events by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1781 +* Add new cache events on CacheCollector by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1773 +* Exclude events on EventCollector by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1786 +* Use `addWarning` on warnings, silenced errors, notices by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1767 +* Do not rely on DB::connection() to get information in query collector by @cweiske in https://github.com/barryvdh/laravel-debugbar/pull/1779 +* Trace file for Gate checks(GateCollector) by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1770 +* Fix support for PDOExceptions by @LukeTowers in https://github.com/barryvdh/laravel-debugbar/pull/1752 +* Time measure on cache events by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1794 +* fix debugbar for Lumen usage by @flibidi67 in https://github.com/barryvdh/laravel-debugbar/pull/1796 +* Custom path for Inertia views by @joaopms in https://github.com/barryvdh/laravel-debugbar/pull/1797 +* Better contrast in dark theme titles. by @angeljqv in https://github.com/barryvdh/laravel-debugbar/pull/1798 + +### New Contributors + +* @wimski made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1784 +* @cweiske made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1779 +* @flibidi67 made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1796 +* @joaopms made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1797 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.4...v3.16.0 + +## v3.15.4 - 2025-04-16 + +### What's Changed + +* Remove html `` tag from route on clockwork by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1777 +* Fix default for capturing dd/dump by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1783 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.3...v3.15.4 + +## v3.15.3 - 2025-04-08 + +### What's Changed + +* Add condition for implemented query grammar by @rikwillems in https://github.com/barryvdh/laravel-debugbar/pull/1757 +* Collect dumps on message collector by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1759 +* Fix `capture_dumps` option on laravel `dd();` by @parallels999 in https://github.com/barryvdh/laravel-debugbar/pull/1762 +* Preserve laravel error handler by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1760 +* Fix `Trying to access array offset on false on LogsCollector.php` by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1763 +* Update css theme for views widget by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1768 +* Fix laravel-debugbar.css on query widget by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1765 +* Use htmlvardumper if available on CacheCollector by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1766 +* Update QueryCollector.php fix issue #1775 by @Mathias-DS in https://github.com/barryvdh/laravel-debugbar/pull/1776 +* Better grouping the events count by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1774 + +### New Contributors + +* @rikwillems made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1757 +* @Mathias-DS made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1776 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.2...v3.15.3 + +## v3.15.2 - 2025-02-25 + +### What's Changed + +* Fix empty tabs on clockwork by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1750 +* fix: Ignore info query statements in Clockwork converter by @boserup in https://github.com/barryvdh/laravel-debugbar/pull/1749 +* Check if request controller is string by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1751 + +### New Contributors + +* @boserup made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1749 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.1...v3.15.2 + +## v3.15.1 - 2025-02-24 + +### What's Changed + +* Hide more empty tabs by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1742 +* Always show application by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1745 +* Add conflict with old debugbar by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1746 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.0...v3.15.1 + +## v3.15.0 - 2025-02-21 + +### What's Changed + +* Add middleware to web to save session by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1710 +* Check web middleware by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1712 +* Add special `dev` to composer keywords by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1713 +* Removed extra sentence by @cheack in https://github.com/barryvdh/laravel-debugbar/pull/1714 +* Hide empty tabs by default by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1711 +* Combine route info with Request by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1720 +* fix: The log is not processed correctly when it consists of multiple lines. by @uniho in https://github.com/barryvdh/laravel-debugbar/pull/1721 +* [WIP] Use php-debugbar dark theme, move to variables by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1717 +* Remove openhandler overrides by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1723 +* Drop Lumen And Laravel 9 by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1725 +* Use tooltip for Laravel collector by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1724 +* Add more data to timeline by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1726 +* Laravel version preview as repo branch name by @angeljqv in https://github.com/barryvdh/laravel-debugbar/pull/1727 +* Laravel 12 support by @jonnott in https://github.com/barryvdh/laravel-debugbar/pull/1730 +* Preview action_name on request tooltip by @angeljqv in https://github.com/barryvdh/laravel-debugbar/pull/1728 +* Map tooltips by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1732 +* Add back L9 by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1734 +* Fix tooltip url by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1735 +* Show request status as badge by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1736 +* Fix request badge by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1737 +* Use Laravel ULID for key by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1738 +* defer datasets by config option by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1739 +* Reorder request tab by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1740 +* Defer config by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1741 + +### New Contributors + +* @cheack made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1714 +* @angeljqv made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1727 +* @jonnott made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1730 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.10...v3.15.0 + +## v3.14.10 - 2024-12-23 + +### What's Changed + +* Fix Debugbar spelling inconsistencies by @ralphjsmit in https://github.com/barryvdh/laravel-debugbar/pull/1626 +* Fix Visual Explain confirm message by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1709 + +### New Contributors + +* @ralphjsmit made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1626 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.9...v3.14.10 + +## v3.14.9 - 2024-11-25 + +### What's Changed + +* Fix custom prototype array by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1706 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.8...v3.14.9 + +## v3.14.8 - 2024-11-25 + +### What's Changed + +* Add fix + failing test for custom array prototype by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1705 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.7...v3.14.8 + +## v3.14.7 - 2024-11-14 + +### What's Changed + +* Make better use of query tab space by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1694 +* Do not open query details on text selecting by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1693 +* Add (initial) support for PHP 8.4 by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1631 +* More warnings by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1696 +* Fix sql-duplicate highlight by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1699 +* ci: Use GitHub Actions V4 by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1700 +* Fix "Uncaught TypeError: is not iterable" by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1701 +* Fix Exception when QueryCollector softLimit exceeded by @johnkary in https://github.com/barryvdh/laravel-debugbar/pull/1702 +* Test soft/hard limit queries by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1703 + +### New Contributors + +* @johnkary made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1702 + +**Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.6...v3.14.7 diff --git a/LICENSE b/LICENSE index 1b751326e..f724f7c29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2013-2014 Barry vd. Heuvel +Copyright (C) 2013-present Barry vd. Heuvel 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 @@ -16,4 +16,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..c6138bc29 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues to `barryvdh@gmail.com` diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 072b56141..000000000 --- a/changelog.md +++ /dev/null @@ -1,86 +0,0 @@ -# Changelog for Laravel Debugbar - -## 1.8.4 (2014-10-31) - -- Add Redis/PDO storage options - -## 1.8.3 (2014-11-23) - -- Base EventCollector on TimeData Collector - -## 1.8.2 (2014-11-18) - -- Use XHR handler instead of jQuery handler - -## 1.8.1 (2014-11-14) - -- Fix compatability with Symfony 2.3 (Laravel 4.) - -## 1.8.0 (2014-10-31) - -- Fix L5 compatability -- add hints + explain options to QueryLogger -- update to Debugbar 1.10.x -- new ViewCollector layout with more information - -## 1.7.7 (2014-09-15) - -- Make it compatible with Laravel 5.0-dev -- Allow anonymous function as `enabled` setting (for IP checks etc) -- Escape query bindings, to prevent executing of scripts/html - -## 1.7.6 (2014-09-12) - -- Fix reflash bug -- Fix caching of debugbar assets - -## 1.7.5 (2014-09-12) - -- Reflash data for all debugbar requests - -## 1.7.4 (2014-09-08) - -- Rename assets routes to prevent Nginx conflicts - -## 1.7.3 (2014-09-05) - -- Add helper functions (debug(), add/start/stop_measure() and measure() -- Collect data on responses that are not redirect/ajax/html also. - -## 1.7.2 (2014-09-04) - -- Fix 4.0 compatibility (problem with Controller namespace) -- Give deprecation notice instead of publishing assets. - -## 1.7.1 (2014-09-03) - -- Deprecated `debugbar:publish` command in favor of AssetController -- Fixed issue with detecting absolute paths in Windows - -## 1.7.0 (2014-09-03) - -- Use AssetController instead of publishing assets to the public folder. -- Inline fonts + images to base64 Data-URI -- Use PSR-4 file structure - -## 1.6.8 (2014-08-27) - -- Change OpenHandler layout -- Add backtrace option for query origin - -## 1.6.7 (2014-08-09) - -- Add Twig extensions for better integration with rcrowe/TwigBridge - -## 1.6.6 (2014-07-08) - -- Check if Requests wantsJSON instead of only isXmlHttpRequest -- Make sure closure for timing is run, even when disabled - -## 1.6.5 (2014-06-24) - -- Add Laravel style - -## 1.6.4 (2014-06-15) - -- Work on non-UTF-8 handling \ No newline at end of file diff --git a/composer.json b/composer.json index 0ff6a94e0..622cea34b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,14 @@ { "name": "barryvdh/laravel-debugbar", "description": "PHP Debugbar integration for Laravel", - "keywords": ["laravel", "debugbar", "profiler", "debug", "webprofiler"], + "keywords": [ + "laravel", + "debugbar", + "profiler", + "debug", + "webprofiler", + "dev" + ], "license": "MIT", "authors": [ { @@ -10,10 +17,18 @@ } ], "require": { - "php": ">=5.5.9", - "illuminate/support": "5.1.*|5.2.*", - "symfony/finder": "~2.7|~3.0", - "maximebf/debugbar": "~1.11.0|~1.12.0" + "php": "^8.1", + "php-debugbar/php-debugbar": "^2.2.4", + "illuminate/routing": "^9|^10|^11|^12", + "illuminate/session": "^9|^10|^11|^12", + "illuminate/support": "^9|^10|^11|^12", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^7|^8|^9|^10", + "phpunit/phpunit": "^9.5.10|^10|^11", + "squizlabs/php_codesniffer": "^3.5" }, "autoload": { "psr-4": { @@ -23,9 +38,29 @@ "src/helpers.php" ] }, + "autoload-dev": { + "psr-4": { + "Barryvdh\\Debugbar\\Tests\\": "tests" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "3.16-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + } } + }, + "scripts": { + "check-style": "phpcs", + "fix-style": "phpcbf", + "test": "phpunit" } } diff --git a/config/debugbar.php b/config/debugbar.php index 406692f79..2d86208c3 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -1,6 +1,6 @@ null, + 'enabled' => env('DEBUGBAR_ENABLED', null), + 'hide_empty_tabs' => env('DEBUGBAR_HIDE_EMPTY_TABS', true), // Hide tabs until they have content + 'except' => [ + 'telescope*', + 'horizon*', + ], /* |-------------------------------------------------------------------------- | Storage settings |-------------------------------------------------------------------------- | - | DebugBar stores data for session/ajax requests. + | Debugbar stores data for session/ajax requests. | You can disable this, so the debugbar stores data in headers/session, | but this can cause problems with large data collectors. | By default, file storage (in the storage folder) is used. Redis and PDO | can also be used. For PDO, run the package migrations first. | + | Warning: Enabling storage.open will allow everyone to access previous + | request, do not enable open storage in publicly available environments! + | Specify a callback if you want to limit based on IP or authentication. + | Leaving it to null will allow localhost only. */ - 'storage' => array( - 'enabled' => true, - 'driver' => 'file', // redis, file, pdo, custom - 'path' => storage_path('debugbar'), // For file driver - 'connection' => null, // Leave null for default connection (Redis/PDO) - 'provider' => '' // Instance of StorageInterface for custom driver - ), + 'storage' => [ + 'enabled' => env('DEBUGBAR_STORAGE_ENABLED', true), + 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. + 'driver' => env('DEBUGBAR_STORAGE_DRIVER', 'file'), // redis, file, pdo, socket, custom + 'path' => env('DEBUGBAR_STORAGE_PATH', storage_path('debugbar')), // For file driver + 'connection' => env('DEBUGBAR_STORAGE_CONNECTION', null), // Leave null for default connection (Redis/PDO) + 'provider' => env('DEBUGBAR_STORAGE_PROVIDER', ''), // Instance of StorageInterface for custom driver + 'hostname' => env('DEBUGBAR_STORAGE_HOSTNAME', '127.0.0.1'), // Hostname to use with the "socket" driver + 'port' => env('DEBUGBAR_STORAGE_PORT', 2304), // Port to use with the "socket" driver + ], + + /* + |-------------------------------------------------------------------------- + | Editor + |-------------------------------------------------------------------------- + | + | Choose your preferred editor to use when clicking file name. + | + | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", + | "vscode-insiders-remote", "vscodium", "textmate", "emacs", + | "sublime", "atom", "nova", "macvim", "idea", "netbeans", + | "xdebug", "espresso" + | + */ + + 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), + + /* + |-------------------------------------------------------------------------- + | 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 Debugbar 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//Code", "C:\Users\\Documents\Code" + | + */ + + 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'), + 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')), /* |-------------------------------------------------------------------------- @@ -42,13 +98,13 @@ | Vendor files are included by default, but can be set to false. | This can also be set to 'js' or 'css', to only include javascript or css vendor files. | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) - | and for js: jquery and and highlight.js + | and for js: jquery and highlight.js | So if you want syntax highlighting, set it to true. | jQuery is set to not conflict with existing jQuery scripts. | */ - 'include_vendors' => true, + 'include_vendors' => env('DEBUGBAR_INCLUDE_VENDORS', true), /* |-------------------------------------------------------------------------- @@ -58,9 +114,32 @@ | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), | you can use this option to disable sending the data through the headers. | + | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. + | + | Note for your request to be identified as ajax requests they must either send the header + | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. + | + | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. + | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. + | + | You can defer loading the dataset, so it will be loaded with ajax after the request is done. (Experimental) */ - 'capture_ajax' => true, + 'capture_ajax' => env('DEBUGBAR_CAPTURE_AJAX', true), + 'add_ajax_timing' => env('DEBUGBAR_ADD_AJAX_TIMING', false), + 'ajax_handler_auto_show' => env('DEBUGBAR_AJAX_HANDLER_AUTO_SHOW', true), + 'ajax_handler_enable_tab' => env('DEBUGBAR_AJAX_HANDLER_ENABLE_TAB', true), + 'defer_datasets' => env('DEBUGBAR_DEFER_DATASETS', false), + /* + |-------------------------------------------------------------------------- + | Custom Error Handler for Deprecated warnings + |-------------------------------------------------------------------------- + | + | When enabled, the Debugbar shows deprecated warnings for Symfony components + | in the Messages tab. + | + */ + 'error_handler' => env('DEBUGBAR_ERROR_HANDLER', false), /* |-------------------------------------------------------------------------- @@ -71,7 +150,7 @@ | Extension, without the server-side code. It uses Debugbar collectors instead. | */ - 'clockwork' => false, + 'clockwork' => env('DEBUGBAR_CLOCKWORK', false), /* |-------------------------------------------------------------------------- @@ -82,28 +161,33 @@ | */ - 'collectors' => array( - 'phpinfo' => true, // Php version - 'messages' => true, // Messages - 'time' => true, // Time Datalogger - 'memory' => true, // Memory usage - 'exceptions' => true, // Exception displayer - 'log' => true, // Logs from Monolog (merged in messages if enabled) - 'db' => true, // Show database (PDO) queries and bindings - 'views' => true, // Views with their data - 'route' => true, // Current route information - 'laravel' => false, // Laravel version and environment - 'events' => false, // All events fired - 'default_request' => false, // Regular or special Symfony request logger - 'symfony_request' => true, // Only one can be enabled.. - 'mail' => true, // Catch mail messages - 'logs' => false, // Add the latest log messages - 'files' => false, // Show the included files - 'config' => false, // Display config settings - 'auth' => false, // Display Laravel authentication status - 'gate' => false, // Display Laravel Gate checks - 'session' => true, // Display session data - ), + 'collectors' => [ + 'phpinfo' => env('DEBUGBAR_COLLECTORS_PHPINFO', false), // Php version + 'messages' => env('DEBUGBAR_COLLECTORS_MESSAGES', true), // Messages + 'time' => env('DEBUGBAR_COLLECTORS_TIME', true), // Time Datalogger + 'memory' => env('DEBUGBAR_COLLECTORS_MEMORY', true), // Memory usage + 'exceptions' => env('DEBUGBAR_COLLECTORS_EXCEPTIONS', true), // Exception displayer + 'log' => env('DEBUGBAR_COLLECTORS_LOG', true), // Logs from Monolog (merged in messages if enabled) + 'db' => env('DEBUGBAR_COLLECTORS_DB', true), // Show database (PDO) queries and bindings + 'views' => env('DEBUGBAR_COLLECTORS_VIEWS', true), // Views with their data + 'route' => env('DEBUGBAR_COLLECTORS_ROUTE', false), // Current route information + 'auth' => env('DEBUGBAR_COLLECTORS_AUTH', false), // Display Laravel authentication status + 'gate' => env('DEBUGBAR_COLLECTORS_GATE', true), // Display Laravel Gate checks + 'session' => env('DEBUGBAR_COLLECTORS_SESSION', false), // Display session data + 'symfony_request' => env('DEBUGBAR_COLLECTORS_SYMFONY_REQUEST', true), // Only one can be enabled.. + 'mail' => env('DEBUGBAR_COLLECTORS_MAIL', true), // Catch mail messages + 'laravel' => env('DEBUGBAR_COLLECTORS_LARAVEL', true), // Laravel version and environment + 'events' => env('DEBUGBAR_COLLECTORS_EVENTS', false), // All events fired + 'default_request' => env('DEBUGBAR_COLLECTORS_DEFAULT_REQUEST', false), // Regular or special Symfony request logger + 'logs' => env('DEBUGBAR_COLLECTORS_LOGS', false), // Add the latest log messages + 'files' => env('DEBUGBAR_COLLECTORS_FILES', false), // Show the included files + 'config' => env('DEBUGBAR_COLLECTORS_CONFIG', false), // Display config settings + 'cache' => env('DEBUGBAR_COLLECTORS_CACHE', false), // Display cache events + 'models' => env('DEBUGBAR_COLLECTORS_MODELS', true), // Display models + 'livewire' => env('DEBUGBAR_COLLECTORS_LIVEWIRE', true), // Display Livewire (when available) + 'jobs' => env('DEBUGBAR_COLLECTORS_JOBS', false), // Display dispatched jobs + 'pennant' => env('DEBUGBAR_COLLECTORS_PENNANT', false), // Display Pennant feature flags + ], /* |-------------------------------------------------------------------------- @@ -114,57 +198,142 @@ | */ - 'options' => array( - 'auth' => array( - 'show_name' => false, // Also show the users name/email in the debugbar - ), - 'db' => array( - 'with_params' => true, // Render SQL with the parameters substituted - 'timeline' => false, // Add the queries to the timeline - 'backtrace' => false, // EXPERIMENTAL: Use a backtrace to find the origin of the query in your files. - 'explain' => array( // EXPERIMENTAL: Show EXPLAIN output on queries - 'enabled' => false, - 'types' => array('SELECT'), // array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); for MySQL 5.6.3+ - ), - 'hints' => true, // Show hints for common mistakes - ), - 'mail' => array( - 'full_log' => false - ), - 'views' => array( - 'data' => false, //Note: Can slow down the application, because the data can be quite large.. - ), - 'route' => array( - 'label' => true // show complete route on bar - ), - 'logs' => array( - 'file' => null - ), - ), + 'options' => [ + 'time' => [ + 'memory_usage' => env('DEBUGBAR_OPTIONS_TIME_MEMORY_USAGE', false), // Calculated by subtracting memory start and end, it may be inaccurate + ], + 'messages' => [ + 'trace' => env('DEBUGBAR_OPTIONS_MESSAGES_TRACE', true), // Trace the origin of the debug message + 'capture_dumps' => env('DEBUGBAR_OPTIONS_MESSAGES_CAPTURE_DUMPS', false), // Capture laravel `dump();` as message + ], + 'memory' => [ + 'reset_peak' => env('DEBUGBAR_OPTIONS_MEMORY_RESET_PEAK', false), // run memory_reset_peak_usage before collecting + 'with_baseline' => env('DEBUGBAR_OPTIONS_MEMORY_WITH_BASELINE', false), // Set boot memory usage as memory peak baseline + 'precision' => (int) env('DEBUGBAR_OPTIONS_MEMORY_PRECISION', 0), // Memory rounding precision + ], + 'auth' => [ + 'show_name' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_NAME', true), // Also show the users name/email in the debugbar + 'show_guards' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_GUARDS', true), // Show the guards that are used + ], + 'gate' => [ + 'trace' => false, // Trace the origin of the Gate checks + ], + 'db' => [ + 'with_params' => env('DEBUGBAR_OPTIONS_WITH_PARAMS', true), // Render SQL with the parameters substituted + 'exclude_paths' => [ // Paths to exclude entirely from the collector + //'vendor/laravel/framework/src/Illuminate/Session', // Exclude sessions queries + ], + 'backtrace' => env('DEBUGBAR_OPTIONS_DB_BACKTRACE', true), // Use a backtrace to find the origin of the query in your files. + 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) + 'timeline' => env('DEBUGBAR_OPTIONS_DB_TIMELINE', false), // Add the queries to the timeline + 'duration_background' => env('DEBUGBAR_OPTIONS_DB_DURATION_BACKGROUND', true), // Show shaded background on each query relative to how long it took to execute. + 'explain' => [ // Show EXPLAIN output on queries + 'enabled' => env('DEBUGBAR_OPTIONS_DB_EXPLAIN_ENABLED', false), + ], + 'hints' => env('DEBUGBAR_OPTIONS_DB_HINTS', false), // Show hints for common mistakes + 'show_copy' => env('DEBUGBAR_OPTIONS_DB_SHOW_COPY', true), // Show copy button next to the query, + 'only_slow_queries' => env('DEBUGBAR_OPTIONS_DB_ONLY_SLOW_QUERIES', true), // Only track queries that last longer than `slow_threshold` + 'slow_threshold' => env('DEBUGBAR_OPTIONS_DB_SLOW_THRESHOLD', false), // Max query execution time (ms). Exceeding queries will be highlighted + 'memory_usage' => env('DEBUGBAR_OPTIONS_DB_MEMORY_USAGE', false), // Show queries memory usage + 'soft_limit' => (int) env('DEBUGBAR_OPTIONS_DB_SOFT_LIMIT', 100), // After the soft limit, no parameters/backtrace are captured + 'hard_limit' => (int) env('DEBUGBAR_OPTIONS_DB_HARD_LIMIT', 500), // After the hard limit, queries are ignored + ], + 'mail' => [ + 'timeline' => env('DEBUGBAR_OPTIONS_MAIL_TIMELINE', true), // Add mails to the timeline + 'show_body' => env('DEBUGBAR_OPTIONS_MAIL_SHOW_BODY', true), + ], + 'views' => [ + 'timeline' => env('DEBUGBAR_OPTIONS_VIEWS_TIMELINE', true), // Add the views to the timeline + 'data' => env('DEBUGBAR_OPTIONS_VIEWS_DATA', false), // True for all data, 'keys' for only names, false for no parameters. + 'group' => (int) env('DEBUGBAR_OPTIONS_VIEWS_GROUP', 50), // Group duplicate views. Pass value to auto-group, or true/false to force + 'inertia_pages' => env('DEBUGBAR_OPTIONS_VIEWS_INERTIA_PAGES', 'js/Pages'), // Path for Inertia views + 'exclude_paths' => [ // Add the paths which you don't want to appear in the views + 'vendor/filament' // Exclude Filament components by default + ], + ], + 'route' => [ + 'label' => env('DEBUGBAR_OPTIONS_ROUTE_LABEL', true), // Show complete route on bar + ], + 'session' => [ + 'hiddens' => [], // Hides sensitive values using array paths + ], + 'symfony_request' => [ + 'label' => env('DEBUGBAR_OPTIONS_SYMFONY_REQUEST_LABEL', true), // Show route on bar + 'hiddens' => [], // Hides sensitive values using array paths, example: request_request.password + ], + 'events' => [ + 'data' => env('DEBUGBAR_OPTIONS_EVENTS_DATA', false), // Collect events data, listeners + 'excluded' => [], // Example: ['eloquent.*', 'composing', Illuminate\Cache\Events\CacheHit::class] + ], + 'logs' => [ + 'file' => env('DEBUGBAR_OPTIONS_LOGS_FILE', null), + ], + 'cache' => [ + 'values' => env('DEBUGBAR_OPTIONS_CACHE_VALUES', true), // Collect cache values + ], + ], /* |-------------------------------------------------------------------------- | Inject Debugbar in Response |-------------------------------------------------------------------------- | - | Usually, the debugbar is added just before , by listening to the + | Usually, the debugbar is added just before , by listening to the | Response after the App is done. If you disable this, you have to add them | in your template yourself. See http://phpdebugbar.com/docs/rendering.html | */ - 'inject' => true, + 'inject' => env('DEBUGBAR_INJECT', true), /* |-------------------------------------------------------------------------- - | DebugBar route prefix + | Debugbar route prefix |-------------------------------------------------------------------------- | - | Sometimes you want to set route prefix to be used by DebugBar to load + | Sometimes you want to set route prefix to be used by Debugbar to load | its resources from. Usually the need comes from misconfigured web server or | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 | */ - 'route_prefix' => '_debugbar', + 'route_prefix' => env('DEBUGBAR_ROUTE_PREFIX', '_debugbar'), -); + /* + |-------------------------------------------------------------------------- + | Debugbar route middleware + |-------------------------------------------------------------------------- + | + | Additional middleware to run on the Debugbar routes + */ + 'route_middleware' => [], + + /* + |-------------------------------------------------------------------------- + | Debugbar route domain + |-------------------------------------------------------------------------- + | + | By default Debugbar route served from the same domain that request served. + | To override default domain, specify it as a non-empty value. + */ + 'route_domain' => env('DEBUGBAR_ROUTE_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | Debugbar theme + |-------------------------------------------------------------------------- + | + | Switches between light and dark theme. If set to auto it will respect system preferences + | Possible values: auto, light, dark + */ + 'theme' => env('DEBUGBAR_THEME', 'auto'), + + /* + |-------------------------------------------------------------------------- + | Backtrace stack limit + |-------------------------------------------------------------------------- + | + | By default, the Debugbar limits the number of frames returned by the 'debug_backtrace()' function. + | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. + */ + 'debug_backtrace_limit' => (int) env('DEBUGBAR_DEBUG_BACKTRACE_LIMIT', 50), +]; diff --git a/src/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php b/database/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php similarity index 86% rename from src/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php rename to database/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php index cb6487076..90daa081e 100644 --- a/src/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php +++ b/database/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php @@ -1,16 +1,15 @@ string('id'); @@ -27,15 +26,14 @@ public function up() $table->index('meta_uri'); $table->index('meta_ip'); $table->index('meta_method'); - }); + }); } + /** * Reverse the migrations. - * - * @return void */ public function down() { Schema::drop('phpdebugbar'); } -} +}; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 000000000..2f3b8fe03 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,16 @@ + + + config + src + tests + + src/Resources/* + *.blade.php + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 000000000..182cfd815 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + tests + + + + + src/ + + + + + + + + + + + + + + + diff --git a/readme.md b/readme.md index de3b59054..edb7d523f 100644 --- a/readme.md +++ b/readme.md @@ -1,19 +1,23 @@ -## Laravel Debugbar -[![Packagist License](https://poser.pugx.org/barryvdh/laravel-debugbar/license.png)](http://choosealicense.com/licenses/mit/) -[![Latest Stable Version](https://poser.pugx.org/barryvdh/laravel-debugbar/version.png)](https://packagist.org/packages/barryvdh/laravel-debugbar) -[![Total Downloads](https://poser.pugx.org/barryvdh/laravel-debugbar/d/total.png)](https://packagist.org/packages/barryvdh/laravel-debugbar) - -### For Laravel 4, please use the [1.8 branch](https://github.com/barryvdh/laravel-debugbar/tree/1.8)! - -This is a package to integrate [PHP Debug Bar](http://phpdebugbar.com/) with Laravel 5. +## Debugbar for Laravel +![Unit Tests](https://github.com/barryvdh/laravel-debugbar/workflows/Unit%20Tests/badge.svg) +[![Packagist License](https://img.shields.io/badge/Licence-MIT-blue)](http://choosealicense.com/licenses/mit/) +[![Latest Stable Version](https://img.shields.io/packagist/v/barryvdh/laravel-debugbar?label=Stable)](https://packagist.org/packages/barryvdh/laravel-debugbar) +[![Total Downloads](https://img.shields.io/packagist/dt/barryvdh/laravel-debugbar?label=Downloads)](https://packagist.org/packages/barryvdh/laravel-debugbar) +[![Fruitcake](https://img.shields.io/badge/Powered%20By-Fruitcake-b2bc35.svg)](https://fruitcake.nl/) + +This is a package to integrate [PHP Debug Bar](https://github.com/php-debugbar/php-debugbar) with Laravel. It includes a ServiceProvider to register the debugbar and attach it to the output. You can publish assets and configure it through Laravel. It bootstraps some Collectors to work with Laravel and implements a couple custom DataCollectors, specific for Laravel. -It is configured to display Redirects and (jQuery) Ajax Requests. (Shown in a dropdown) +It is configured to display Redirects and Ajax/Livewire Requests. (Shown in a dropdown) Read [the documentation](http://phpdebugbar.com/docs/) for more configuration options. -![Screenshot](https://cloud.githubusercontent.com/assets/973269/4270452/740c8c8c-3ccb-11e4-8d9a-5a9e64f19351.png) +![Debugbar Dark Mode screenshot](https://github.com/barryvdh/laravel-debugbar/assets/973269/6600837a-8b2d-4acb-ab0c-158c9ca5439c) -Note: Use the DebugBar only in development. It can slow the application down (because it has to gather data). So when experiencing slowness, try disabling some of the collectors. +> [!CAUTION] +> Use the DebugBar only in development. Do not use Debugbar on publicly accessible websites, as it will leak information from stored requests (by design). + +> [!WARNING] +> It can also slow the application down (because it has to gather and render data). So when experiencing slowness, try disabling some of the collectors. This package includes some custom collectors: - QueryCollector: Show all queries, including binding + timing @@ -25,10 +29,11 @@ This package includes some custom collectors: - LogsCollector: Show the latest log entries from the storage logs. (disabled by default) - FilesCollector: Show the files that are included/required by PHP. (disabled by default) - ConfigCollector: Display the values from the config files. (disabled by default) + - CacheCollector: Display all cache events. (disabled by default) Bootstraps the following collectors for Laravel: - LogCollector: Show all Log messages - - SwiftMailCollector and SwiftLogCollector for Mail + - SymfonyMailCollector for Mail And the default collectors: - PhpInfoCollector @@ -37,47 +42,65 @@ And the default collectors: - MemoryCollector - ExceptionsCollector -It also provides a Facade interface for easy logging Messages, Exceptions and Time +It also provides a facade interface (`Debugbar`) for easy logging Messages, Exceptions and Time ## Installation -Require this package with composer: +Require this package with composer. It is recommended to only require the package for development. -``` -composer require barryvdh/laravel-debugbar +```shell +composer require barryvdh/laravel-debugbar --dev ``` -After updating composer, add the ServiceProvider to the providers array in config/app.php +Laravel uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider. + +The Debugbar will be enabled when `APP_DEBUG` is `true`. + > If you use a catch-all/fallback route, make sure you load the Debugbar ServiceProvider before your own App ServiceProviders. -### Laravel 5.x: +### Laravel without auto-discovery: -``` +If you don't use auto-discovery, add the ServiceProvider to the providers list. For Laravel 11 or newer, add the ServiceProvider in bootstrap/providers.php. For Laravel 10 or older, add the ServiceProvider in config/app.php. + +```php Barryvdh\Debugbar\ServiceProvider::class, ``` -If you want to use the facade to log messages, add this to your facades in app.php: +If you want to use the facade to log messages, add this within the `register` method of `app/Providers/AppServiceProvider.php` class: -``` -'Debugbar' => Barryvdh\Debugbar\Facade::class, +```php +public function register(): void +{ + $loader = \Illuminate\Foundation\AliasLoader::getInstance(); + $loader->alias('Debugbar', \Barryvdh\Debugbar\Facades\Debugbar::class); +} ``` - -The profiler is enabled by default, if you have app.debug=true. You can override that in the config (`debugbar.enabled`). See more options in `config/debugbar.php` +The profiler is enabled by default, if you have APP_DEBUG=true. You can override that in the config (`debugbar.enabled`) or by setting `DEBUGBAR_ENABLED` in your `.env`. See more options in `config/debugbar.php` You can also set in your config if you want to include/exclude the vendor files also (FontAwesome, Highlight.js and jQuery). If you already use them in your site, set it to false. -You can also only display the js of css vendors, by setting it to 'js' or 'css'. (Highlight.js requires both css + js, so set to `true` for syntax highlighting) +You can also only display the js or css vendors, by setting it to 'js' or 'css'. (Highlight.js requires both css + js, so set to `true` for syntax highlighting) -Copy the package config to your local config with the publish command: +#### Copy the package config to your local config with the publish command: -``` +```shell php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider" ``` +### Laravel with Octane: + +Make sure to add LaravelDebugbar to your flush list in `config/octane.php`. + +```php + 'flush' => [ + \Barryvdh\Debugbar\LaravelDebugbar::class, + ], +``` + ### Lumen: For Lumen, register a different Provider in `bootstrap/app.php`: -``` +```php if (env('APP_DEBUG')) { $app->register(Barryvdh\Debugbar\LumenServiceProvider::class); } @@ -85,7 +108,7 @@ if (env('APP_DEBUG')) { To change the configuration, copy the file to your config folder and enable it: -``` +```php $app->configure('debugbar'); ``` @@ -117,7 +140,7 @@ Or log exceptions: try { throw new Exception('foobar'); } catch (Exception $e) { - Debugbar::addException($e); + Debugbar::addThrowable($e); } ``` @@ -127,6 +150,9 @@ There are also helper functions available for the most common calls: // All arguments will be dumped as a debug message debug($var1, $someString, $intValue, $object); +// `$collection->debug()` will return the collection and dump it as a debug message. Like `$collection->dump()` +collect([$var1, $someString])->debug(); + start_measure('render','Time for rendering'); stop_measure('render'); add_measure('now', LARAVEL_START, microtime(true)); @@ -164,6 +190,12 @@ You can enable or disable the debugbar during run time. NB. Once enabled, the collectors are added (and could produce extra overhead), so if you want to use the debugbar in production, disable in the config and only enable when needed. +## Storage + +Debugbar remembers previous requests, which you can view using the Browse button on the right. This will only work if you enable `debugbar.storage.open` in the config. +Make sure you only do this on local development, because otherwise other people will be able to view previous requests. +In general, Debugbar should only be used locally or at least restricted by IP. +It's possible to pass a callback, which will receive the Request object, so you can determine access to the OpenHandler storage. ## Twig Integration @@ -180,15 +212,19 @@ Add the following extensions to your TwigBridge config/extensions.php (or regist The Dump extension will replace the [dump function](http://twig.sensiolabs.org/doc/functions/dump.html) to output variables using the DataFormatter. The Debug extension adds a `debug()` function which passes variables to the Message Collector, instead of showing it directly in the template. It dumps the arguments, or when empty; all context variables. -``` +```twig {{ debug() }} {{ debug(user, categories) }} ``` The Stopwatch extension adds a [stopwatch tag](http://symfony.com/blog/new-in-symfony-2-4-a-stopwatch-tag-for-twig) similar to the one in Symfony/Silex Twigbridge. -``` +```twig {% stopwatch "foo" %} …some things that gets timed {% endstopwatch %} ``` + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=barryvdh/laravel-debugbar&type=Date)](https://www.star-history.com/#barryvdh/laravel-debugbar&Date) diff --git a/src/Console/ClearCommand.php b/src/Console/ClearCommand.php index 2ceaf7212..abafae915 100644 --- a/src/Console/ClearCommand.php +++ b/src/Console/ClearCommand.php @@ -1,4 +1,6 @@ -debugbar->boot(); - + if ($storage = $this->debugbar->getStorage()) { - $storage->clear(); + try { + $storage->clear(); + } catch (\InvalidArgumentException $e) { + // hide InvalidArgumentException if storage location does not exist + if (strpos($e->getMessage(), 'does not exist') === false) { + throw $e; + } + } $this->info('Debugbar Storage cleared!'); } else { $this->error('No Debugbar Storage found..'); diff --git a/src/Console/PublishCommand.php b/src/Console/PublishCommand.php deleted file mode 100644 index 84479d156..000000000 --- a/src/Console/PublishCommand.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @deprecated No longer needed because of the AssetController - */ -class PublishCommand extends Command -{ - /** - * The console command name. - * - * @var string - */ - protected $name = 'debugbar:publish'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Publish the Debugbar assets'; - - /** - * Execute the console command. - * - * @return void - */ - public function fire() - { - $this->info( - 'NOTICE: Since laravel-debugbar 1.7.x, publishing assets is no longer necessary. The assets in public/packages/barryvdh/laravel-debugbar and maximebf/php-debugbar can be safely removed.' - ); - } -} diff --git a/src/Controllers/AssetController.php b/src/Controllers/AssetController.php index 5076ddf70..646ab44ab 100644 --- a/src/Controllers/AssetController.php +++ b/src/Controllers/AssetController.php @@ -1,4 +1,6 @@ -dumpAssetsToString('js'); $response = new Response( - $content, 200, array( + $content, + 200, + [ 'Content-Type' => 'text/javascript', - ) + ] ); return $this->cacheResponse($response); @@ -36,9 +40,11 @@ public function css() $content = $renderer->dumpAssetsToString('css'); $response = new Response( - $content, 200, array( + $content, + 200, + [ 'Content-Type' => 'text/css', - ) + ] ); return $this->cacheResponse($response); diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php index 904747d48..3d2f15f7d 100644 --- a/src/Controllers/BaseController.php +++ b/src/Controllers/BaseController.php @@ -1,9 +1,13 @@ -debugbar = $debugbar; - if ($request->hasSession()){ + if ($request->hasSession()) { $request->session()->reflash(); } + + $this->middleware(function ($request, $next) { + if (class_exists(Telescope::class)) { + Telescope::stopRecording(); + } + return $next($request); + }); } } @@ -30,9 +41,9 @@ public function __construct(Request $request, LaravelDebugbar $debugbar) { $this->debugbar = $debugbar; - if ($request->hasSession()){ + if ($request->hasSession()) { $request->session()->reflash(); } } } -} \ No newline at end of file +} diff --git a/src/Controllers/CacheController.php b/src/Controllers/CacheController.php new file mode 100644 index 000000000..c347700b6 --- /dev/null +++ b/src/Controllers/CacheController.php @@ -0,0 +1,28 @@ +tags($tags); + } else { + unset($tags); + } + + $success = $cache->forget($key); + + return response()->json(compact('success')); + } +} diff --git a/src/Controllers/OpenHandlerController.php b/src/Controllers/OpenHandlerController.php index 479d88280..4b7449e0c 100644 --- a/src/Controllers/OpenHandlerController.php +++ b/src/Controllers/OpenHandlerController.php @@ -1,28 +1,69 @@ -debugbar; + $open = config('debugbar.storage.open'); - if (!$debugbar->isEnabled()) { - $this->app->abort('500', 'Debugbar is not enabled'); + if (is_callable($open)) { + return call_user_func($open, [$request]); } - $openHandler = new OpenHandler($debugbar); + if (is_string($open) && class_exists($open)) { + return method_exists($open, 'resolve') ? $open::resolve($request) : false; + } - $data = $openHandler->handle(null, false, false); + if (is_bool($open)) { + return $open; + } + + // Allow localhost request when not explicitly allowed/disallowed + if (in_array($request->ip(), ['127.0.0.1', '::1'], true)) { + return true; + } + + return false; + } + + public function handle(Request $request) + { + if ($request->input('op') === 'get' || $this->isStorageOpen($request)) { + $openHandler = new OpenHandler($this->debugbar); + $data = $openHandler->handle($request->input(), false, false); + } else { + $data = [ + [ + 'datetime' => date("Y-m-d H:i:s"), + 'id' => null, + 'ip' => $request->getClientIp(), + 'method' => 'ERROR', + 'uri' => '!! To enable public access to previous requests, set debugbar.storage.open to true in your config, or enable DEBUGBAR_OPEN_STORAGE if you did not publish the config. !!', + 'utime' => microtime(true), + ] + ]; + } return new Response( - $data, 200, array( + $data, + 200, + [ 'Content-Type' => 'application/json' - ) + ] ); } @@ -33,21 +74,14 @@ public function handle() * @return mixed * @throws \DebugBar\DebugBarException */ - public function clockwork($id) + public function clockwork(Request $request, $id) { $request = [ 'op' => 'get', 'id' => $id, ]; - $debugbar = $this->debugbar; - - if (!$debugbar->isEnabled()) { - $this->app->abort('500', 'Debugbar is not enabled'); - } - - $openHandler = new OpenHandler($debugbar); - + $openHandler = new OpenHandler($this->debugbar); $data = $openHandler->handle($request, false, false); // Convert to Clockwork diff --git a/src/Controllers/QueriesController.php b/src/Controllers/QueriesController.php new file mode 100644 index 000000000..314a9b1c2 --- /dev/null +++ b/src/Controllers/QueriesController.php @@ -0,0 +1,47 @@ +json([ + 'success' => false, + 'message' => 'EXPLAIN is currently disabled in the Debugbar.', + ], 400); + } + + try { + $explain = new Explain(); + + if ($request->json('mode') === 'visual') { + return response()->json([ + 'success' => true, + 'data' => $explain->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')), + ]); + } + + return response()->json([ + 'success' => true, + 'data' => $explain->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')), + 'visual' => $explain->isVisualExplainSupported($request->json('connection')) ? [ + 'confirm' => $explain->confirmVisualExplain($request->json('connection')), + ] : null, + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } +} diff --git a/src/Controllers/TelescopeController.php b/src/Controllers/TelescopeController.php new file mode 100644 index 000000000..5fb9943bb --- /dev/null +++ b/src/Controllers/TelescopeController.php @@ -0,0 +1,23 @@ +find($uuid); + $result = $storage->get('request', (new EntryQueryOptions())->batchId($entry->batchId))->first(); + + return redirect(config('telescope.domain') . '/' . config('telescope.path') . '/requests/' . $result->id); + } +} diff --git a/src/DataCollector/AuthCollector.php b/src/DataCollector/AuthCollector.php deleted file mode 100644 index 2f8b99008..000000000 --- a/src/DataCollector/AuthCollector.php +++ /dev/null @@ -1,115 +0,0 @@ -auth = $auth; - } - - /** - * Set to show the users name/email - * @param bool $showName - */ - public function setShowName($showName) - { - $this->showName = (bool) $showName; - } - - /** - * @{inheritDoc} - */ - public function collect() - { - try { - $user = $this->auth->user(); - } catch (\Exception $e) { - $user = null; - } - return $this->getUserInformation($user); - } - - /** - * Get displayed user information - * @param \Illuminate\Auth\UserInterface $user - * @return array - */ - protected function getUserInformation($user = null) - { - // Defaults - if (is_null($user)) { - return array( - 'name' => 'Guest', - 'user' => array('guest' => true), - ); - } - - // The default auth identifer is the ID number, which isn't all that - // useful. Try username and email. - $identifier = $user->getAuthIdentifier(); - if (is_numeric($identifier)) { - try { - if ($user->username) { - $identifier = $user->username; - } elseif ($user->email) { - $identifier = $user->email; - } - } catch (\Exception $e) { - } - } - - return array( - 'name' => $identifier, - 'user' => $user instanceof Arrayable ? $user->toArray() : $user, - ); - } - - /** - * @{inheritDoc} - */ - public function getName() - { - return 'auth'; - } - - /** - * @{inheritDoc} - */ - public function getWidgets() - { - $widgets = array( - 'auth' => array( - 'icon' => 'lock', - 'widget' => 'PhpDebugBar.Widgets.VariableListWidget', - 'map' => 'auth.user', - 'default' => '{}' - ) - ); - if ($this->showName) { - $widgets['auth.name'] = array( - 'icon' => 'user', - 'tooltip' => 'Auth status', - 'map' => 'auth.name', - 'default' => '', - ); - } - return $widgets; - } -} diff --git a/src/DataCollector/CacheCollector.php b/src/DataCollector/CacheCollector.php new file mode 100644 index 000000000..059514725 --- /dev/null +++ b/src/DataCollector/CacheCollector.php @@ -0,0 +1,140 @@ + ['hit', RetrievingKey::class], + CacheMissed::class => ['missed', RetrievingKey::class], + CacheFlushed::class => ['flushed', CacheFlushing::class], + CacheFlushFailed::class => ['flush_failed', CacheFlushing::class], + KeyWritten::class => ['written', WritingKey::class], + KeyWriteFailed::class => ['write_failed', WritingKey::class], + KeyForgotten::class => ['forgotten', ForgettingKey::class], + KeyForgetFailed::class => ['forget_failed', ForgettingKey::class], + ]; + + public function __construct($requestStartTime, $collectValues) + { + parent::__construct($requestStartTime); + + $this->collectValues = $collectValues; + } + + public function onCacheEvent($event) + { + $class = get_class($event); + $params = get_object_vars($event); + $label = $this->classMap[$class][0]; + + if (isset($params['value'])) { + if ($this->collectValues) { + if ($this->isHtmlVarDumperUsed()) { + $params['value'] = $this->getVarDumper()->renderVar($params['value']); + } else { + $params['value'] = htmlspecialchars($this->getDataFormatter()->formatVar($params['value'])); + } + } else { + unset($params['value']); + } + } + + if (!empty($params['key'] ?? null) && in_array($label, ['hit', 'written'])) { + $params['delete'] = route('debugbar.cache.delete', [ + 'key' => urlencode($params['key']), + 'tags' => !empty($params['tags']) ? json_encode($params['tags']) : '', + ]); + } + + $time = microtime(true); + $startHashKey = $this->getEventHash($this->classMap[$class][1] ?? '', $params); + $startTime = $this->eventStarts[$startHashKey] ?? $time; + $this->addMeasure($label . "\t" . ($params['key'] ?? ''), $startTime, $time, $params); + } + + public function onStartCacheEvent($event) + { + $startHashKey = $this->getEventHash(get_class($event), get_object_vars($event)); + $this->eventStarts[$startHashKey] = microtime(true); + } + + private function getEventHash(string $class, array $params): string + { + unset($params['value']); + + return $class . ':' . substr(hash('sha256', json_encode($params)), 0, 12); + } + + public function subscribe(Dispatcher $dispatcher) + { + foreach (array_keys($this->classMap) as $eventClass) { + $dispatcher->listen($eventClass, [$this, 'onCacheEvent']); + } + + $startEvents = array_unique(array_filter(array_map( + fn ($values) => $values[1] ?? null, + array_values($this->classMap) + ))); + + foreach ($startEvents as $eventClass) { + $dispatcher->listen($eventClass, [$this, 'onStartCacheEvent']); + } + } + + public function collect() + { + $data = parent::collect(); + $data['nb_measures'] = $data['count'] = count($data['measures']); + + return $data; + } + + public function getName() + { + return 'cache'; + } + + public function getWidgets() + { + return [ + 'cache' => [ + 'icon' => 'clipboard', + 'widget' => 'PhpDebugBar.Widgets.LaravelCacheWidget', + 'map' => 'cache', + 'default' => '{}', + ], + 'cache:badge' => [ + 'map' => 'cache.nb_measures', + 'default' => 'null', + ], + ]; + } +} diff --git a/src/DataCollector/EventCollector.php b/src/DataCollector/EventCollector.php index ab3fefdc4..5dfff86ba 100644 --- a/src/DataCollector/EventCollector.php +++ b/src/DataCollector/EventCollector.php @@ -1,37 +1,56 @@ exporter = new ValueExporter(); + $this->collectValues = $collectValues; + $this->excludedEvents = $excludedEvents; + $this->setDataFormatter(new SimpleFormatter()); } - public function onWildcardEvent() + public function onWildcardEvent($name = null, $data = []) { - $name = $this->events->firing(); - $time = microtime(true); + $currentTime = microtime(true); + $eventClass = explode(':', $name)[0]; + + foreach ($this->excludedEvents as $excludedEvent) { + if (Str::is($excludedEvent, $eventClass)) { + return; + } + } + + if (! $this->collectValues) { + $this->addMeasure($name, $currentTime, $currentTime, [], null, $eventClass); - // Get the arguments passed to the event - $params = $this->prepareParams(func_get_args()); + return; + } + + $params = $this->prepareParams($data); // Find all listeners for the current event foreach ($this->events->getListeners($name) as $i => $listener) { - // Check if it's an object + method name if (is_array($listener) && count($listener) > 1 && is_object($listener[0])) { list($class, $method) = $listener; @@ -55,31 +74,32 @@ public function onWildcardEvent() // Format the closure to a readable format $filename = ltrim(str_replace(base_path(), '', $reflector->getFileName()), '/'); - $listener = $reflector->getName() . ' (' . $filename . ':' . $reflector->getStartLine() . '-' . $reflector->getEndLine() . ')'; + $lines = $reflector->getStartLine() . '-' . $reflector->getEndLine(); + $listener = $reflector->getName() . ' (' . $filename . ':' . $lines . ')'; } else { // Not sure if this is possible, but to prevent edge cases - $listener = $this->formatVar($listener); + $listener = $this->getDataFormatter()->formatVar($listener); } $params['listeners.' . $i] = $listener; } - $this->addMeasure($name, $time, $time, $params); + $this->addMeasure($name, $currentTime, $currentTime, $params, null, $eventClass); } public function subscribe(Dispatcher $events) { $this->events = $events; - $events->listen('*', array($this, 'onWildcardEvent')); + $events->listen('*', [$this, 'onWildcardEvent']); } protected function prepareParams($params) { - $data = array(); + $data = []; foreach ($params as $key => $value) { if (is_object($value) && Str::is('Illuminate\*\Events\*', get_class($value))) { $value = $this->prepareParams(get_object_vars($value)); } - $data[$key] = htmlentities($this->exporter->exportValue($value), ENT_QUOTES, 'UTF-8', false); + $data[$key] = htmlentities($this->getDataFormatter()->formatVar($value), ENT_QUOTES, 'UTF-8', false); } return $data; @@ -88,7 +108,7 @@ protected function prepareParams($params) public function collect() { $data = parent::collect(); - $data['nb_measures'] = count($data['measures']); + $data['nb_measures'] = $data['count'] = count($data['measures']); return $data; } @@ -100,17 +120,17 @@ public function getName() public function getWidgets() { - return array( - "events" => array( + return [ + "events" => [ "icon" => "tasks", "widget" => "PhpDebugBar.Widgets.TimelineWidget", "map" => "event", "default" => "{}", - ), - 'events:badge' => array( + ], + 'events:badge' => [ 'map' => 'event.nb_measures', 'default' => 0, - ), - ); + ], + ]; } } diff --git a/src/DataCollector/FilesCollector.php b/src/DataCollector/FilesCollector.php index 6b2b963c2..f6180f2bf 100644 --- a/src/DataCollector/FilesCollector.php +++ b/src/DataCollector/FilesCollector.php @@ -4,18 +4,18 @@ use DebugBar\DataCollector\DataCollector; use DebugBar\DataCollector\Renderable; -use Illuminate\Contracts\Foundation\Application; +use Illuminate\Container\Container; class FilesCollector extends DataCollector implements Renderable { - /** @var \Illuminate\Contracts\Foundation\Application */ + /** @var \Illuminate\Container\Container */ protected $app; protected $basePath; /** - * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Illuminate\Container\Container $app */ - public function __construct(Application $app = null) + public function __construct(?Container $app = null) { $this->app = $app; $this->basePath = base_path(); @@ -29,39 +29,40 @@ public function collect() $files = $this->getIncludedFiles(); $compiled = $this->getCompiledFiles(); - $included = array(); - $alreadyCompiled = array(); + $included = []; + $alreadyCompiled = []; foreach ($files as $file) { // Skip the files from Debugbar, they are only loaded for Debugging and confuse the output. // Of course some files are stil always loaded (ServiceProvider, Facade etc) - if (strpos($file, 'vendor/maximebf/debugbar/src') !== false || strpos( + if ( + strpos($file, 'vendor/maximebf/debugbar/src') !== false || strpos( $file, 'vendor/barryvdh/laravel-debugbar/src' ) !== false ) { continue; } elseif (!in_array($file, $compiled)) { - $included[] = array( + $included[] = [ 'message' => "'" . $this->stripBasePath($file) . "',", // Use PHP syntax so we can copy-paste to compile config file. 'is_string' => true, - ); + ]; } else { - $alreadyCompiled[] = array( + $alreadyCompiled[] = [ 'message' => "* '" . $this->stripBasePath($file) . "',", - // Mark with *, so know they are compiled anyways. + // Mark with *, so know they are compiled anyway. 'is_string' => true, - ); + ]; } } // First the included files, then those that are going to be compiled. $messages = array_merge($included, $alreadyCompiled); - return array( + return [ 'messages' => $messages, 'count' => count($included), - ); + ]; } /** @@ -91,7 +92,7 @@ protected function getCompiledFiles() return array_merge($core, $app['config']['compile']); } } - return array(); + return []; } /** @@ -111,18 +112,18 @@ protected function stripBasePath($path) public function getWidgets() { $name = $this->getName(); - return array( - "$name" => array( + return [ + "$name" => [ "icon" => "files-o", "widget" => "PhpDebugBar.Widgets.MessagesWidget", "map" => "$name.messages", "default" => "{}" - ), - "$name:badge" => array( + ], + "$name:badge" => [ "map" => "$name.count", "default" => "null" - ) - ); + ] + ]; } /** diff --git a/src/DataCollector/GateCollector.php b/src/DataCollector/GateCollector.php index 1c343cf72..ebb5a781e 100644 --- a/src/DataCollector/GateCollector.php +++ b/src/DataCollector/GateCollector.php @@ -2,36 +2,181 @@ namespace Barryvdh\Debugbar\DataCollector; +use Barryvdh\Debugbar\DataFormatter\SimpleFormatter; use DebugBar\DataCollector\MessagesCollector; +use Illuminate\Auth\Access\Response; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Routing\Router; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Illuminate\Support\Str; /** - * Collector for Laravel's Auth provider + * Collector for Laravel's gate checks */ class GateCollector extends MessagesCollector { + /** @var int */ + protected $backtraceLimit = 15; + + /** @var array */ + protected $reflection = []; + + /** @var \Illuminate\Routing\Router */ + protected $router; + /** * @param Gate $gate */ - public function __construct(Gate $gate) + public function __construct(Gate $gate, Router $router) { parent::__construct('gate'); + $this->router = $router; + $this->setDataFormatter(new SimpleFormatter()); + $gate->after(function ($user, $ability, $result, $arguments = []) { + $this->addCheck($user, $ability, $result, $arguments); + }); + } + + /** + * {@inheritDoc} + */ + protected function customizeMessageHtml($messageHtml, $message) + { + $pos = strpos((string) $messageHtml, 'array:5'); + if ($pos !== false) { + + $name = $message['ability'] .' ' . $message['target'] ?? ''; - if (method_exists($gate, 'after')) { - $gate->after([$this, 'addCheck']); + $messageHtml = substr_replace($messageHtml, $name, $pos, 7); } + + return parent::customizeMessageHtml($messageHtml, $message); } - public function addCheck(Authenticatable $user, $ability, $result, $arguments = []) + public function addCheck($user, $ability, $result, $arguments = []) { + $userKey = 'user'; + $userId = null; + + if ($user) { + $userKey = Str::snake(class_basename($user)); + $userId = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user->getKey(); + } + $label = $result ? 'success' : 'error'; + if ($result instanceof Response) { + $label = $result->allowed() ? 'success' : 'error'; + } + + $target = null; + if (isset($arguments[0])) { + if ($arguments[0] instanceof Model) { + $model = $arguments[0]; + if ($model->getKeyName() && isset($model[$model->getKeyName()])) { + $target = get_class($model) . '(' . $model->getKeyName() . '=' . $model->getKey() . ')'; + } else { + $target = get_class($model); + } + } else if (is_string($arguments[0])) { + $target = $arguments[0]; + } + } + $this->addMessage([ 'ability' => $ability, + 'target' => $target, 'result' => $result, - 'user' => $user->getAuthIdentifier(), - 'arguments' => $arguments, + $userKey => $userId, + 'arguments' => $this->getDataFormatter()->formatVar($arguments), ], $label, false); } + + /** + * @param array $stacktrace + * + * @return array + */ + protected function getStackTraceItem($stacktrace) + { + foreach ($stacktrace as $i => $trace) { + if (!isset($trace['file'])) { + continue; + } + + if (str_ends_with($trace['file'], 'Illuminate/Routing/ControllerDispatcher.php')) { + $trace = $this->findControllerFromDispatcher($trace); + } elseif (str_starts_with($trace['file'], storage_path())) { + $hash = pathinfo($trace['file'], PATHINFO_FILENAME); + + if ($file = $this->findViewFromHash($hash)) { + $trace['file'] = $file; + } + } + + if ($this->fileIsInExcludedPath($trace['file'])) { + continue; + } + + return $trace; + } + + return $stacktrace[0]; + } + + /** + * Find the route action file + * + * @param array $trace + * @return array + */ + protected function findControllerFromDispatcher($trace) + { + /** @var \Closure|string|array $action */ + $action = $this->router->current()->getAction('uses'); + + if (is_string($action)) { + [$controller, $method] = explode('@', $action); + + $reflection = new \ReflectionMethod($controller, $method); + $trace['file'] = $reflection->getFileName(); + $trace['line'] = $reflection->getStartLine(); + } elseif ($action instanceof \Closure) { + $reflection = new \ReflectionFunction($action); + $trace['file'] = $reflection->getFileName(); + $trace['line'] = $reflection->getStartLine(); + } + + return $trace; + } + + /** + * Find the template name from the hash. + * + * @param string $hash + * @return null|array + */ + protected function findViewFromHash($hash) + { + $finder = app('view')->getFinder(); + + if (isset($this->reflection['viewfinderViews'])) { + $property = $this->reflection['viewfinderViews']; + } else { + $reflection = new \ReflectionClass($finder); + $property = $reflection->getProperty('views'); + $property->setAccessible(true); + $this->reflection['viewfinderViews'] = $property; + } + + $xxh128Exists = in_array('xxh128', hash_algos()); + + foreach ($property->getValue($finder) as $name => $path) { + if (($xxh128Exists && hash('xxh128', 'v2' . $path) == $hash) || sha1('v2' . $path) == $hash) { + return $path; + } + } + } } diff --git a/src/DataCollector/IlluminateRouteCollector.php b/src/DataCollector/IlluminateRouteCollector.php deleted file mode 100644 index 425dfa4de..000000000 --- a/src/DataCollector/IlluminateRouteCollector.php +++ /dev/null @@ -1,143 +0,0 @@ -router = $router; - } - - /** - * {@inheritDoc} - */ - public function collect() - { - $route = $this->router->current(); - return $this->getRouteInformation($route); - } - - /** - * Get the route information for a given route. - * - * @param \Illuminate\Routing\Route $route - * @return array - */ - protected function getRouteInformation($route) - { - if (!is_a($route, 'Illuminate\Routing\Route')) { - return array(); - } - $uri = head($route->methods()) . ' ' . $route->uri(); - $action = $route->getAction(); - - $result = array( - 'uri' => $uri ?: '-', - ); - - $result = array_merge($result, $action); - - - if (isset($action['controller']) && strpos($action['controller'], '@') !== false) { - list($controller, $method) = explode('@', $action['controller']); - if(class_exists($controller) && method_exists($controller, $method)) { - $reflector = new \ReflectionMethod($controller, $method); - } - unset($result['uses']); - } elseif (isset($action['uses']) && $action['uses'] instanceof \Closure) { - $reflector = new \ReflectionFunction($action['uses']); - $result['uses'] = $this->formatVar($result['uses']); - } - - if (isset($reflector)) { - $filename = ltrim(str_replace(base_path(), '', $reflector->getFileName()), '/'); - $result['file'] = $filename . ':' . $reflector->getStartLine() . '-' . $reflector->getEndLine(); - } - - if ($middleware = $this->getMiddleware($route)) { - $result['middleware'] = $middleware; - } - - - - return $result; - } - - /** - * Get middleware - * - * @param \Illuminate\Routing\Route $route - * @return string - */ - protected function getMiddleware($route) - { - $middleware = array_keys($route->middleware()); - - return implode(', ', $middleware); - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'route'; - } - - /** - * {@inheritDoc} - */ - public function getWidgets() - { - $widgets = array( - "route" => array( - "icon" => "share", - "widget" => "PhpDebugBar.Widgets.VariableListWidget", - "map" => "route", - "default" => "{}" - ) - ); - if (Config::get('debugbar.options.route.label', true)) { - $widgets['currentroute'] = array( - "icon" => "share", - "tooltip" => "Route", - "map" => "route.uri", - "default" => "" - ); - } - return $widgets; - } - - /** - * Display the route information on the console. - * - * @param array $routes - * @return void - */ - protected function displayRoutes(array $routes) - { - $this->table->setHeaders($this->headers)->setRows($routes); - - $this->table->render($this->getOutput()); - } -} diff --git a/src/DataCollector/JobsCollector.php b/src/DataCollector/JobsCollector.php new file mode 100644 index 000000000..10de9f85e --- /dev/null +++ b/src/DataCollector/JobsCollector.php @@ -0,0 +1,63 @@ +listen(\Illuminate\Queue\Events\JobQueued::class, function ($event) { + $class = get_class($event->job); + $this->jobs[$class] = ($this->jobs[$class] ?? 0) + 1; + $this->count++; + }); + } + + public function collect() + { + ksort($this->jobs, SORT_NUMERIC); + + return ['data' => array_reverse($this->jobs), 'count' => $this->count]; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'jobs'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + return [ + "jobs" => [ + "icon" => "briefcase", + "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", + "map" => "jobs.data", + "default" => "{}" + ], + 'jobs:badge' => [ + 'map' => 'jobs.count', + 'default' => 0 + ] + ]; + } +} diff --git a/src/DataCollector/LaravelCollector.php b/src/DataCollector/LaravelCollector.php index 19cfc2373..7c9a53a04 100644 --- a/src/DataCollector/LaravelCollector.php +++ b/src/DataCollector/LaravelCollector.php @@ -4,19 +4,17 @@ use DebugBar\DataCollector\DataCollector; use DebugBar\DataCollector\Renderable; +use Illuminate\Contracts\Foundation\Application as ApplicationContract; use Illuminate\Foundation\Application; +use Illuminate\Support\Str; class LaravelCollector extends DataCollector implements Renderable { - /** @var \Illuminate\Foundation\Application $app */ - protected $app; - /** * @param Application $app */ - public function __construct(Application $app = null) + public function __construct(protected ApplicationContract $laravel) { - $this->app = $app; } /** @@ -24,16 +22,21 @@ public function __construct(Application $app = null) */ public function collect() { - // Fallback if not injected - $app = $this->app ?: app(); - - return array( - "version" => $app::VERSION, - "environment" => $app->environment(), - "locale" => $app->getLocale(), - ); + return [ + "version" => Str::of($this->laravel->version())->explode('.')->first() . '.x', + 'tooltip' => [ + 'Laravel Version' => $this->laravel->version(), + 'PHP Version' => phpversion(), + 'Environment' => $this->laravel->environment(), + 'Debug Mode' => config('app.debug') ? 'Enabled' : 'Disabled', + 'URL' => Str::of(config('app.url'))->replace(['http://', 'https://'], ''), + 'Timezone' => config('app.timezone'), + 'Locale' => config('app.locale'), + ] + ]; } + /** * {@inheritDoc} */ @@ -47,25 +50,16 @@ public function getName() */ public function getWidgets() { - return array( - "version" => array( - "icon" => "github", - "tooltip" => "Version", + return [ + "version" => [ + "icon" => "laravel phpdebugbar-fab", "map" => "laravel.version", "default" => "" - ), - "environment" => array( - "icon" => "desktop", - "tooltip" => "Environment", - "map" => "laravel.environment", - "default" => "" - ), - "locale" => array( - "icon" => "flag", - "tooltip" => "Current locale", - "map" => "laravel.locale", - "default" => "", - ), - ); + ], + "version:tooltip" => [ + "map" => "laravel.tooltip", + "default" => "{}" + ], + ]; } } diff --git a/src/DataCollector/LivewireCollector.php b/src/DataCollector/LivewireCollector.php new file mode 100644 index 000000000..c70a4201b --- /dev/null +++ b/src/DataCollector/LivewireCollector.php @@ -0,0 +1,103 @@ +getData()['_instance']; + + // Create a unique name for each component + $key = $component->getName() . ' #' . $component->id; + + $data = [ + 'data' => $component->getPublicPropertiesDefinedBySubClass(), + ]; + + if ($request->request->get('id') == $component->id) { + $data['oldData'] = $request->request->get('data'); + $data['actionQueue'] = $request->request->get('actionQueue'); + } + + $data['name'] = $component->getName(); + $data['view'] = $view->name(); + $data['component'] = get_class($component); + $data['id'] = $component->id; + + $this->data[$key] = $this->formatVar($data); + }); + + Livewire::listen('render', function (Component $component) use ($request) { + // Create an unique name for each compoent + $key = $component->getName() . ' #' . $component->getId(); + + $data = [ + 'data' => $component->all(), + ]; + + if ($request->request->get('id') == $component->getId()) { + $data['oldData'] = $request->request->get('data'); + $data['actionQueue'] = $request->request->get('actionQueue'); + } + + $data['name'] = $component->getName(); + $data['component'] = get_class($component); + $data['id'] = $component->getId(); + + $this->data[$key] = $this->formatVar($data); + }); + } + + public function collect() + { + return ['data' => $this->data, 'count' => count($this->data)]; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'livewire'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + return [ + "livewire" => [ + "icon" => "bolt", + "widget" => "PhpDebugBar.Widgets.VariableListWidget", + "map" => "livewire.data", + "default" => "{}" + ], + 'livewire:badge' => [ + 'map' => 'livewire.count', + 'default' => 0 + ] + ]; + } +} diff --git a/src/DataCollector/LogsCollector.php b/src/DataCollector/LogsCollector.php index 15518e0cf..1872f767e 100644 --- a/src/DataCollector/LogsCollector.php +++ b/src/DataCollector/LogsCollector.php @@ -1,7 +1,9 @@ getLogsFile(); - $this->getStorageLogs($path); - } + $paths = Arr::wrap($path ?: [ + storage_path('logs/laravel.log'), + storage_path('logs/laravel-' . date('Y-m-d') . '.log'), // for daily driver + ]); - /** - * Get the path to the logs file - * - * @return string - */ - public function getLogsFile() - { - // default daily rotating logs (Laravel 5.0) - $path = storage_path() . '/logs/laravel-' . date('Y-m-d') . '.log'; - - // single file logs - if (!file_exists($path)) { - $path = storage_path() . '/logs/laravel.log'; + foreach ($paths as $path) { + $this->getStorageLogs($path); } - - return $path; } /** @@ -51,9 +41,16 @@ public function getStorageLogs($path) //Load the latest lines, guessing about 15x the number of log entries (for stack traces etc) $file = implode("", $this->tailFile($path, $this->lines)); + $basename = basename($path); foreach ($this->getLogs($file) as $log) { - $this->addMessage($log['header'] . $log['stack'], $log['level'], false); + $this->messages[] = [ + 'message' => trim($log['header'] . $log['stack']), + 'label' => $log['level'], + 'time' => substr($log['header'], 1, 19), + 'collector' => $basename, + 'is_string' => false, + ]; } } @@ -70,7 +67,7 @@ protected function tailFile($file, $lines) $linecounter = $lines; $pos = -2; $beginning = false; - $text = array(); + $text = []; while ($linecounter > 0) { $t = " "; while ($t != "\n") { @@ -103,30 +100,36 @@ protected function tailFile($file, $lines) */ public function getLogs($file) { - $pattern = "/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\].*/"; + $pattern = "/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\](?:(?!\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\])[\s\S])*/"; $log_levels = $this->getLevels(); // There has GOT to be a better way of doing this... preg_match_all($pattern, $file, $headings); - $log_data = preg_split($pattern, $file); + $log_data = preg_split($pattern, $file) ?: []; - $log = array(); + $log = []; foreach ($headings as $h) { for ($i = 0, $j = count($h); $i < $j; $i++) { foreach ($log_levels as $ll) { if (strpos(strtolower($h[$i]), strtolower('.' . $ll))) { - $log[] = array('level' => $ll, 'header' => $h[$i], 'stack' => $log_data[$i]); + $log[] = ['level' => $ll, 'header' => $h[$i], 'stack' => $log_data[$i] ?? '']; } } } } - $log = array_reverse($log); - return $log; } + /** + * @return array + */ + public function getMessages() + { + return array_reverse(parent::getMessages()); + } + /** * Get the log levels from psr/log. * Based on https://github.com/mikemand/logviewer/blob/master/src/Kmd/Logviewer/Logviewer.php by mikemand diff --git a/src/DataCollector/ModelsCollector.php b/src/DataCollector/ModelsCollector.php new file mode 100644 index 000000000..913877548 --- /dev/null +++ b/src/DataCollector/ModelsCollector.php @@ -0,0 +1,66 @@ +listen('eloquent.retrieved:*', function ($event, $models) { + foreach (array_filter($models) as $model) { + $class = get_class($model); + $this->models[$class] = ($this->models[$class] ?? 0) + 1; + $this->count++; + } + }); + } + + public function collect() + { + ksort($this->models, SORT_NUMERIC); + + return ['data' => array_reverse($this->models), 'count' => $this->count]; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'models'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + return [ + "models" => [ + "icon" => "cubes", + "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", + "map" => "models.data", + "default" => "{}" + ], + 'models:badge' => [ + 'map' => 'models.count', + 'default' => 0 + ] + ]; + } +} diff --git a/src/DataCollector/MultiAuthCollector.php b/src/DataCollector/MultiAuthCollector.php index 0cb67200b..32f4b416e 100644 --- a/src/DataCollector/MultiAuthCollector.php +++ b/src/DataCollector/MultiAuthCollector.php @@ -2,38 +2,85 @@ namespace Barryvdh\Debugbar\DataCollector; +use DebugBar\DataCollector\DataCollector; +use DebugBar\DataCollector\Renderable; +use Illuminate\Auth\Recaller; +use Illuminate\Auth\SessionGuard; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Contracts\Auth\Guard; +use Illuminate\Support\Str; +use Illuminate\Contracts\Support\Arrayable; + /** * Collector for Laravel's Auth provider */ -class MultiAuthCollector extends AuthCollector +class MultiAuthCollector extends DataCollector implements Renderable { /** @var array $guards */ protected $guards; + /** @var \Illuminate\Auth\AuthManager */ + protected $auth; + + /** @var bool */ + protected $showName = false; + + /** @var bool */ + protected $showGuardsData = true; + /** * @param \Illuminate\Auth\AuthManager $auth * @param array $guards */ public function __construct($auth, $guards) { - parent::__construct($auth); + $this->auth = $auth; $this->guards = $guards; } + /** + * Set to show the users name/email + * @param bool $showName + */ + public function setShowName($showName) + { + $this->showName = (bool) $showName; + } + + /** + * Set to hide the guards tab, and show only name + * @param bool $showGuardsData + */ + public function setShowGuardsData($showGuardsData) + { + $this->showGuardsData = (bool) $showGuardsData; + } /** * @{inheritDoc} */ public function collect() { - $data = []; + $data = [ + 'guards' => [], + ]; $names = ''; - foreach($this->guards as $guardName) { - $user = $this->auth->guard($guardName)->user(); - $data['guards'][$guardName] = $this->getUserInformation($user); - if(!is_null($user)) { - $names .= $guardName . ": " . $data['guards'][$guardName]['name'] . ', '; + foreach ($this->guards as $guardName => $config) { + try { + $guard = $this->auth->guard($guardName); + if ($this->hasUser($guard)) { + $user = $guard->user(); + + if (!is_null($user)) { + $data['guards'][$guardName] = $this->getUserInformation($user); + $names .= $guardName . ": " . $data['guards'][$guardName]['name'] . ', '; + } + } else { + $data['guards'][$guardName] = null; + } + } catch (\Exception $e) { + continue; } } @@ -44,31 +91,90 @@ public function collect() } $data['names'] = rtrim($names, ', '); + if (!$this->showGuardsData) { + unset($data['guards']); + } return $data; } - + + private function hasUser(Guard $guard) + { + if (method_exists($guard, 'hasUser')) { + return $guard->hasUser(); + } + + return false; + } + + /** + * Get displayed user information + * @param \Illuminate\Auth\UserInterface $user + * @return array + */ + protected function getUserInformation($user = null) + { + // Defaults + if (is_null($user)) { + return [ + 'name' => 'Guest', + 'user' => ['guest' => true], + ]; + } + + // The default auth identifer is the ID number, which isn't all that + // useful. Try username, email and name. + $identifier = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user->getKey(); + if (is_numeric($identifier) || Str::isUuid($identifier) || Str::isUlid($identifier)) { + try { + if (isset($user->username)) { + $identifier = $user->username; + } elseif (isset($user->email)) { + $identifier = $user->email; + } elseif (isset($user->name)) { + $identifier = Str::limit($user->name, 24); + } + } catch (\Throwable $e) { + } + } + + return [ + 'name' => $identifier, + 'user' => $user instanceof Arrayable ? $user->toArray() : $user, + ]; + } + + /** + * @{inheritDoc} + */ + public function getName() + { + return 'auth'; + } + /** * @{inheritDoc} */ public function getWidgets() { - $widgets = array( - "auth" => array( + $widgets = []; + + if ($this->showGuardsData) { + $widgets["auth"] = [ "icon" => "lock", "widget" => "PhpDebugBar.Widgets.VariableListWidget", "map" => "auth.guards", - "default" => "{}" - ) - ); + "default" => "{}", + ]; + } if ($this->showName) { - $widgets['auth.name'] = array( + $widgets['auth.name'] = [ 'icon' => 'user', 'tooltip' => 'Auth status', 'map' => 'auth.names', 'default' => '', - ); + ]; } return $widgets; diff --git a/src/DataCollector/PennantCollector.php b/src/DataCollector/PennantCollector.php new file mode 100644 index 000000000..ecc58eb4c --- /dev/null +++ b/src/DataCollector/PennantCollector.php @@ -0,0 +1,57 @@ +manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function collect() + { + $store = $this->manager->store(Config::get('pennant.default')); + + return $store->values($store->stored()); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'pennant'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + return [ + "pennant" => [ + "icon" => "flag", + "widget" => "PhpDebugBar.Widgets.VariableListWidget", + "map" => "pennant", + "default" => "{}" + ] + ]; + } +} diff --git a/src/DataCollector/QueryCollector.php b/src/DataCollector/QueryCollector.php index e25c74387..b6922276f 100644 --- a/src/DataCollector/QueryCollector.php +++ b/src/DataCollector/QueryCollector.php @@ -2,8 +2,11 @@ namespace Barryvdh\Debugbar\DataCollector; +use Barryvdh\Debugbar\Support\Explain; use DebugBar\DataCollector\PDO\PDOCollector; use DebugBar\DataCollector\TimeDataCollector; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; /** * Collects data about SQL statements executed with PDO @@ -11,22 +14,51 @@ class QueryCollector extends PDOCollector { protected $timeCollector; - protected $queries = array(); + protected $queries = []; + protected $queryCount = 0; + protected $transactionEventsCount = 0; + protected $infoStatements = 0; + protected $softLimit = null; + protected $hardLimit = null; + protected $lastMemoryUsage; protected $renderSqlWithParams = false; protected $findSource = false; + protected $middleware = []; + protected $durationBackground = true; protected $explainQuery = false; - protected $explainTypes = array('SELECT'); // array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); for MySQL 5.6.3+ + protected $explainTypes = ['SELECT']; // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ protected $showHints = false; + protected $showCopyButton = false; protected $reflection = []; + protected $excludePaths = []; + protected $backtraceExcludePaths = [ + '/vendor/laravel/framework/src/Illuminate/Support', + '/vendor/laravel/framework/src/Illuminate/Database', + '/vendor/laravel/framework/src/Illuminate/Events', + '/vendor/laravel/framework/src/Illuminate/Collections', + '/vendor/october/rain', + '/vendor/barryvdh/laravel-debugbar', + ]; /** * @param TimeDataCollector $timeCollector */ - public function __construct(TimeDataCollector $timeCollector = null) + public function __construct(?TimeDataCollector $timeCollector = null) { $this->timeCollector = $timeCollector; } + /** + * @param int|null $softLimit After the soft limit, no parameters/backtrace are captured + * @param int|null $hardLimit After the hard limit, queries are ignored + * @return void + */ + public function setLimits(?int $softLimit, ?int $hardLimit): void + { + $this->softLimit = $softLimit; + $this->hardLimit = $hardLimit; + } + /** * Renders the SQL of traced statements with params embedded * @@ -48,14 +80,51 @@ public function setShowHints($enabled = true) $this->showHints = $enabled; } + /** + * Show or hide copy button next to the queries + * + * @param boolean $enabled + */ + public function setShowCopyButton($enabled = true) + { + $this->showCopyButton = $enabled; + } + /** * Enable/disable finding the source * - * @param bool $value + * @param bool|int $value + * @param array $middleware */ - public function setFindSource($value = true) + public function setFindSource($value, array $middleware) + { + $this->findSource = $value; + $this->middleware = $middleware; + } + + public function mergeExcludePaths(array $excludePaths) { - $this->findSource = (bool) $value; + $this->excludePaths = array_merge($this->excludePaths, $excludePaths); + } + + /** + * Set additional paths to exclude from the backtrace + * + * @param array $excludePaths Array of file paths to exclude from backtrace + */ + public function mergeBacktraceExcludePaths(array $excludePaths) + { + $this->backtraceExcludePaths = array_merge($this->backtraceExcludePaths, $excludePaths); + } + + /** + * Enable/disable the shaded duration background on queries + * + * @param bool $enabled + */ + public function setDurationBackground($enabled = true) + { + $this->durationBackground = $enabled; } /** @@ -67,94 +136,89 @@ public function setFindSource($value = true) public function setExplainSource($enabled, $types) { $this->explainQuery = $enabled; - if($types){ - $this->explainTypes = $types; - } + } + + public function startMemoryUsage() + { + $this->lastMemoryUsage = memory_get_usage(false); } /** * - * @param string $query - * @param array $bindings - * @param float $time - * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\Events\QueryExecuted $query */ - public function addQuery($query, $bindings, $time, $connection) + public function addQuery($query) { - $explainResults = array(); - $time = $time / 1000; + $this->queryCount++; + + if ($this->hardLimit && $this->queryCount > $this->hardLimit) { + return; + } + + $limited = $this->softLimit && $this->queryCount > $this->softLimit; + + $sql = (string) $query->sql; + $time = $query->time / 1000; $endTime = microtime(true); $startTime = $endTime - $time; - $hints = $this->performQueryAnalysis($query); - - $pdo = $connection->getPdo(); - $bindings = $connection->prepareBindings($bindings); + $hints = $this->performQueryAnalysis($sql); - // Run EXPLAIN on this query (if needed) - if ($this->explainQuery && preg_match('/^('.implode($this->explainTypes).') /i', $query)) { - $statement = $pdo->prepare('EXPLAIN ' . $query); - $statement->execute($bindings); - $explainResults = $statement->fetchAll(\PDO::FETCH_CLASS); - } + $pdo = null; + try { + $pdo = $query->connection->getPdo(); - $bindings = $this->checkBindings($bindings); - if (!empty($bindings) && $this->renderSqlWithParams) { - foreach ($bindings as $binding) { - $query = preg_replace('/\?/', $pdo->quote($binding), $query, 1); + if(! ($pdo instanceof \PDO)) { + $pdo = null; } + } catch (\Throwable $e) { + // ignore error for non-pdo laravel drivers } - $source = null; - if ($this->findSource) { + $source = []; + + if (!$limited && $this->findSource) { try { $source = $this->findSource(); } catch (\Exception $e) { } } - $this->queries[] = array( - 'query' => $query, - 'bindings' => $this->escapeBindings($bindings), + $bindings = match (true) { + $limited && filled($query->bindings) => [], + default => $query->connection->prepareBindings($query->bindings), + }; + + $this->queries[] = [ + 'query' => $sql, + 'type' => 'query', + 'bindings' => $bindings, + 'start' => $startTime, 'time' => $time, + 'memory' => $this->lastMemoryUsage ? memory_get_usage(false) - $this->lastMemoryUsage : 0, 'source' => $source, - 'explain' => $explainResults, - 'connection' => $connection->getDatabaseName(), - 'hints' => $this->showHints ? $hints : null, - ); + 'connection' => $query->connection, + 'driver' => $query->connection->getConfig('driver'), + 'hints' => ($this->showHints && !$limited) ? $hints : null, + 'show_copy' => $this->showCopyButton, + ]; if ($this->timeCollector !== null) { - $this->timeCollector->addMeasure($query, $startTime, $endTime); + $this->timeCollector->addMeasure(Str::limit($sql, 100), $startTime, $endTime, [], 'db', 'Database Query'); } } /** - * Check bindings for illegal (non UTF-8) strings, like Binary data. + * Mimic mysql_real_escape_string * - * @param $bindings - * @return mixed + * @param string $value + * @return string */ - protected function checkBindings($bindings) + protected function emulateQuote($value) { - foreach ($bindings as &$binding) { - if (is_string($binding) && !mb_check_encoding($binding, 'UTF-8')) { - $binding = '[BINARY DATA]'; - } - } - return $bindings; - } + $search = ["\\", "\x00", "\n", "\r", "'", '"', "\x1a"]; + $replace = ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"]; - /** - * Make the bindings safe for outputting. - * - * @param array $bindings - * @return array - */ - protected function escapeBindings($bindings) - { - foreach ($bindings as &$binding) { - $binding = htmlentities($binding, ENT_QUOTES, 'UTF-8', false); - } - return $bindings; + return "'" . str_replace($search, $replace, (string) $value) . "'"; } /** @@ -168,18 +232,19 @@ protected function escapeBindings($bindings) * @version $Id$ * @access public * @param string $query - * @return string + * @return string[] */ protected function performQueryAnalysis($query) { - $hints = array(); + // @codingStandardsIgnoreStart + $hints = []; if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $query)) { $hints[] = 'Use SELECT * only if you need all columns from table'; } if (preg_match('/ORDER BY RAND()/i', $query)) { $hints[] = 'ORDER BY RAND() is slow, try to avoid if you can. - You can read this - or this'; + You can read this + or this'; } if (strpos($query, '!=') !== false) { $hints[] = 'The != operator is not standard. Use the <> operator to test for inequality instead.'; @@ -191,42 +256,130 @@ protected function performQueryAnalysis($query) $hints[] = 'LIMIT without ORDER BY causes non-deterministic results, depending on the query execution plan'; } if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $query, $matches)) { - $hints[] = 'An argument has a leading wildcard character: ' . $matches[1]. '. - The predicate with this argument is not sargable and cannot use an index if one exists.'; + $hints[] = 'An argument has a leading wildcard character: ' . $matches[1] . '. + The predicate with this argument is not sargable and cannot use an index if one exists.'; } - return implode("
", $hints); + return $hints; + + // @codingStandardsIgnoreEnd } - + /** - * Use a backtrace to search for the origin of the query. + * Use a backtrace to search for the origins of the query. + * + * @return array */ protected function findSource() { - $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); - foreach ($traces as $trace) { - if (isset($trace['class']) && isset($trace['file']) && strpos( - $trace['file'], - DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR - ) === false - ) { - if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) { - list($file, $line) = $this->getTwigInfo($trace); - } elseif (strpos($trace['file'], storage_path()) !== false) { - $hash = pathinfo($trace['file'], PATHINFO_FILENAME); - $line = isset($trace['line']) ? $trace['line'] : '?'; - - if ($name = $this->findViewFromHash($hash)) { - return 'view::' . $name . ':' . $line; - } - return 'view::' . $hash . ':' . $line; + $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, app('config')->get('debugbar.debug_backtrace_limit', 50)); + + $sources = []; + + foreach ($stack as $index => $trace) { + $sources[] = $this->parseTrace($index, $trace); + } + + return array_slice(array_filter($sources), 0, is_int($this->findSource) ? $this->findSource : 5); + } + + /** + * Parse a trace element from the backtrace stack. + * + * @param int $index + * @param array $trace + * @return object|bool + */ + protected function parseTrace($index, array $trace) + { + $frame = (object) [ + 'index' => $index, + 'namespace' => null, + 'name' => null, + 'file' => null, + 'line' => $trace['line'] ?? '1', + ]; + + if (isset($trace['function']) && $trace['function'] == 'substituteBindings') { + $frame->name = 'Route binding'; + + return $frame; + } + + if ( + isset($trace['class']) && + isset($trace['file']) && + !$this->fileIsInExcludedPath($trace['file']) + ) { + $frame->file = $trace['file']; + + if (isset($trace['object']) && is_a($trace['object'], '\Twig\Template')) { + list($frame->file, $frame->line) = $this->getTwigInfo($trace); + } elseif (strpos($frame->file, storage_path()) !== false) { + $hash = pathinfo($frame->file, PATHINFO_FILENAME); + + if ($frame->name = $this->findViewFromHash($hash)) { + $frame->file = $frame->name[1]; + $frame->name = $frame->name[0]; + } else { + $frame->name = $hash; + } + + $frame->namespace = 'view'; + + return $frame; + } elseif (strpos($frame->file, 'Middleware') !== false) { + $frame->name = $this->findMiddlewareFromFile($frame->file); + + if ($frame->name) { + $frame->namespace = 'middleware'; } else { - $file = $trace['file']; - $line = isset($trace['line']) ? $trace['line'] : '?'; + $frame->name = $this->normalizeFilePath($frame->file); } - return $this->normalizeFilename($file) . ':' . $line; - } elseif (isset($trace['function']) && $trace['function'] == 'Illuminate\Routing\{closure}') { - return 'Route binding'; + return $frame; + } + + $frame->name = $this->normalizeFilePath($frame->file); + + return $frame; + } + + + return false; + } + + /** + * Check if the given file is to be excluded from analysis + * + * @param string $file + * @return bool + */ + protected function fileIsInExcludedPath($file) + { + $normalizedPath = str_replace('\\', '/', $file); + + foreach ($this->backtraceExcludePaths as $excludedPath) { + if (strpos($normalizedPath, $excludedPath) !== false) { + return true; + } + } + + return false; + } + + /** + * Find the middleware alias from the file. + * + * @param string $file + * @return string|null + */ + protected function findMiddlewareFromFile($file) + { + $filename = pathinfo($file, PATHINFO_FILENAME); + + foreach ($this->middleware as $alias => $class) { + if (!is_null($class) && !is_null($filename) && strpos($class, $filename) !== false) { + return $alias; } } } @@ -235,7 +388,7 @@ protected function findSource() * Find the template name from the hash. * * @param string $hash - * @return null|string + * @return null|array */ protected function findViewFromHash($hash) { @@ -250,9 +403,11 @@ protected function findViewFromHash($hash) $this->reflection['viewfinderViews'] = $property; } - foreach ($property->getValue($finder) as $name => $path){ - if (sha1($path) == $hash || md5($path) == $hash) { - return $name; + $xxh128Exists = in_array('xxh128', hash_algos()); + + foreach ($property->getValue($finder) as $name => $path) { + if (($xxh128Exists && hash('xxh128', 'v2' . $path) == $hash) || sha1('v2' . $path) == $hash) { + return [$name, $path]; } } } @@ -270,34 +425,55 @@ protected function getTwigInfo($trace) if (isset($trace['line'])) { foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) { if ($codeLine <= $trace['line']) { - return array($file, $templateLine); + return [$file, $templateLine]; } } } - return array($file, -1); + return [$file, -1]; } /** - * Shorten the path by removing the relative links and base dir - * - * @param string $path - * @return string + * Collect a database transaction event. + * @param string $event + * @param \Illuminate\Database\Connection $connection + * @return array */ - protected function normalizeFilename($path) + public function collectTransactionEvent($event, $connection) { - if (file_exists($path)) { - $path = realpath($path); + $this->transactionEventsCount++; + $source = []; + + if ($this->findSource) { + try { + $source = $this->findSource(); + } catch (\Exception $e) { + } } - return str_replace(base_path(), '', $path); + + $this->queries[] = [ + 'query' => $event, + 'type' => 'transaction', + 'bindings' => [], + 'start' => microtime(true), + 'time' => 0, + 'memory' => 0, + 'source' => $source, + 'connection' => $connection, + 'driver' => $connection->getConfig('driver'), + 'hints' => null, + 'show_copy' => false, + ]; } - + /** * Reset the queries. */ public function reset() { - $this->queries = array(); + $this->queries = []; + $this->queryCount = 0; + $this->infoStatements = 0 ; } /** @@ -306,58 +482,125 @@ public function reset() public function collect() { $totalTime = 0; + $totalMemory = 0; $queries = $this->queries; - $statements = array(); + $statements = []; foreach ($queries as $query) { + $source = reset($query['source']); + $normalizedPath = is_object($source) ? $this->normalizeFilePath($source->file ?: '') : ''; + if ($query['type'] != 'transaction' && Str::startsWith($normalizedPath, $this->excludePaths)) { + continue; + } + $totalTime += $query['time']; + $totalMemory += $query['memory']; - $bindings = $query['bindings']; - if($query['hints']){ - $bindings['hints'] = $query['hints']; + $connectionName = $query['connection']->getDatabaseName(); + if (str_ends_with($connectionName, '.sqlite')) { + $connectionName = $this->normalizeFilePath($connectionName); } - $statements[] = array( - 'sql' => $this->formatSql($query['query']), - 'params' => (object) $bindings, + $canExplainQuery = match (true) { + in_array($query['driver'], ['mariadb', 'mysql', 'pgsql']) => $query['bindings'] !== null && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $query['query']), + default => false, + }; + + $statements[] = [ + 'sql' => $this->getSqlQueryToDisplay($query), + 'type' => $query['type'], + 'params' => [], + 'bindings' => $query['bindings'] ?? [], + 'hints' => $query['hints'], + 'show_copy' => $query['show_copy'], + 'backtrace' => array_values($query['source']), + 'start' => $query['start'] ?? null, 'duration' => $query['time'], - 'duration_str' => $this->formatDuration($query['time']), - 'stmt_id' => $query['source'], - 'connection' => $query['connection'], - ); - - //Add the results from the explain as new rows - foreach($query['explain'] as $explain){ - $statements[] = array( - 'sql' => ' - EXPLAIN #' . $explain->id . ': `' . $explain->table . '` (' . $explain->select_type . ')', - 'params' => $explain, - 'row_count' => $explain->rows, - 'stmt_id' => $explain->id, - ); + 'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']), + 'slow' => $this->slowThreshold && $this->slowThreshold <= $query['time'], + 'memory' => $query['memory'], + 'memory_str' => $query['memory'] ? $this->getDataFormatter()->formatBytes($query['memory']) : null, + 'filename' => $this->getDataFormatter()->formatSource($source, true), + 'source' => $source, + 'xdebug_link' => is_object($source) ? $this->getXdebugLink($source->file ?: '', $source->line) : null, + 'connection' => $connectionName, + 'explain' => $this->explainQuery && $canExplainQuery ? [ + 'url' => route('debugbar.queries.explain'), + 'driver' => $query['driver'], + 'connection' => $query['connection']->getName(), + 'query' => $query['query'], + 'hash' => (new Explain())->hash($query['connection']->getName(), $query['query'], $query['bindings']), + ] : null, + ]; + } + + if ($this->durationBackground) { + if ($totalTime > 0) { + // For showing background measure on Queries tab + $start_percent = 0; + + foreach ($statements as $i => $statement) { + if (!isset($statement['duration'])) { + continue; + } + + $width_percent = $statement['duration'] / $totalTime * 100; + + $statements[$i] = array_merge($statement, [ + 'start_percent' => round($start_percent, 3), + 'width_percent' => round($width_percent, 3), + ]); + + $start_percent += $width_percent; + } } } - $data = array( - 'nb_statements' => count($queries), + if ($this->softLimit && $this->hardLimit && ($this->queryCount > $this->softLimit && $this->queryCount > $this->hardLimit)) { + array_unshift($statements, [ + 'sql' => '# Query soft and hard limit for Debugbar are reached. Only the first ' . $this->softLimit . ' queries show details. Queries after the first ' . $this->hardLimit . ' are ignored. Limits can be raised in the config (debugbar.options.db.soft/hard_limit).', + 'type' => 'info', + ]); + $statements[] = [ + 'sql' => '... ' . ($this->queryCount - $this->hardLimit) . ' additional queries are executed but now shown because of Debugbar query limits. Limits can be raised in the config (debugbar.options.db.soft/hard_limit)', + 'type' => 'info', + ]; + $this->infoStatements+= 2; + } elseif ($this->hardLimit && $this->queryCount > $this->hardLimit) { + array_unshift($statements, [ + 'sql' => '# Query hard limit for Debugbar is reached after ' . $this->hardLimit . ' queries, additional ' . ($this->queryCount - $this->hardLimit) . ' queries are not shown.. Limits can be raised in the config (debugbar.options.db.hard_limit)', + 'type' => 'info', + ]); + $statements[] = [ + 'sql' => '... ' . ($this->queryCount - $this->hardLimit) . ' additional queries are executed but now shown because of Debugbar query limits. Limits can be raised in the config (debugbar.options.db.hard_limit)', + 'type' => 'info', + ]; + $this->infoStatements+= 2; + } elseif ($this->softLimit && $this->queryCount > $this->softLimit) { + array_unshift($statements, [ + 'sql' => '# Query soft limit for Debugbar is reached after ' . $this->softLimit . ' queries, additional ' . ($this->queryCount - $this->softLimit) . ' queries only show the query. Limits can be raised in the config (debugbar.options.db.soft_limit)', + 'type' => 'info', + ]); + $this->infoStatements++; + } + + $visibleStatements = count($statements) - $this->infoStatements; + + $data = [ + 'count' => $visibleStatements, + 'nb_statements' => $this->queryCount, + 'nb_visible_statements' => $visibleStatements, + 'nb_excluded_statements' => $this->queryCount + $this->transactionEventsCount - $visibleStatements, 'nb_failed_statements' => 0, 'accumulated_duration' => $totalTime, 'accumulated_duration_str' => $this->formatDuration($totalTime), + 'memory_usage' => $totalMemory, + 'memory_usage_str' => $totalMemory ? $this->getDataFormatter()->formatBytes($totalMemory) : null, 'statements' => $statements - ); + ]; return $data; } - /** - * Removes extra spaces at the beginning and end of the SQL query and its lines. - * - * @param string $sql - * @return string - */ - protected function formatSql($sql) - { - return trim(preg_replace("/\s*\n\s*/", "\n", $sql)); - } - /** * {@inheritDoc} */ @@ -371,17 +614,68 @@ public function getName() */ public function getWidgets() { - return array( - "queries" => array( - "icon" => "inbox", - "widget" => "PhpDebugBar.Widgets.SQLQueriesWidget", + return [ + "queries" => [ + "icon" => "database", + "widget" => "PhpDebugBar.Widgets.LaravelQueriesWidget", "map" => "queries", "default" => "[]" - ), - "queries:badge" => array( + ], + "queries:badge" => [ "map" => "queries.nb_statements", "default" => 0 - ) - ); + ] + ]; + } + + private function getSqlQueryToDisplay(array $query): string + { + $sql = $query['query']; + if ($query['type'] === 'query' && $this->renderSqlWithParams && $query['connection']->getQueryGrammar() instanceof \Illuminate\Database\Query\Grammars\Grammar && method_exists($query['connection']->getQueryGrammar(), 'substituteBindingsIntoRawSql')) { + try { + $sql = $query['connection']->getQueryGrammar()->substituteBindingsIntoRawSql($sql, $query['bindings'] ?? []); + return $this->getDataFormatter()->formatSql($sql); + } catch (\Throwable $e) { + // Continue using the old substitute + } + } + + if ($query['type'] === 'query' && $this->renderSqlWithParams) { + $bindings = $this->getDataFormatter()->checkBindings($query['bindings']); + if (!empty($bindings)) { + $pdo = null; + try { + $pdo = $query->connection->getPdo(); + } catch (\Throwable) { + // ignore error for non-pdo laravel drivers + } + + foreach ($bindings as $key => $binding) { + // This regex matches placeholders only, not the question marks, + // nested in quotes, while we iterate through the bindings + // and substitute placeholders by suitable values. + $regex = is_numeric($key) + ? "/(?quote((string) $binding); + } catch (\Exception $e) { + $binding = $this->emulateQuote($binding); + } + } else { + $binding = $this->emulateQuote($binding); + } + } + + $sql = preg_replace($regex, addcslashes($binding, '$'), $sql, 1); + } + } + } + + return $this->getDataFormatter()->formatSql($sql); } } diff --git a/src/DataCollector/RequestCollector.php b/src/DataCollector/RequestCollector.php new file mode 100644 index 000000000..de372d18f --- /dev/null +++ b/src/DataCollector/RequestCollector.php @@ -0,0 +1,333 @@ + + * + */ +class RequestCollector extends DataCollector implements DataCollectorInterface, Renderable +{ + /** @var \Symfony\Component\HttpFoundation\Request $request */ + protected $request; + /** @var \Symfony\Component\HttpFoundation\Response $response */ + protected $response; + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + protected $session; + /** @var string|null */ + protected $currentRequestId; + /** @var array */ + protected $hiddens; + + /** + * Create a new SymfonyRequestCollector + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response + * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session + * @param string|null $currentRequestId + * @param array $hiddens + */ + public function __construct($request, $response, $session = null, $currentRequestId = null, $hiddens = []) + { + $this->request = $request; + $this->response = $response; + $this->session = $session; + $this->currentRequestId = $currentRequestId; + $this->hiddens = array_merge($hiddens, [ + 'request_request.password', + 'request_headers.php-auth-pw.0', + ]); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'request'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + $widgets = [ + "request" => [ + "icon" => "tags", + "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", + "map" => "request.data", + "order" => -100, + "default" => "{}" + ], + 'request:badge' => [ + "map" => "request.badge", + "default" => "null" + ] + ]; + + if (Config::get('debugbar.options.request.label', true)) { + $widgets['currentrequest'] = [ + "icon" => "share", + "map" => "request.data.uri", + "link" => "request", + "default" => "" + ]; + $widgets['currentrequest:tooltip'] = [ + "map" => "request.tooltip", + "default" => "{}" + ]; + } + + return $widgets; + } + + /** + * {@inheritdoc} + */ + public function collect() + { + $request = $this->request; + $response = $this->response; + + $responseHeaders = $response->headers->all(); + $cookies = []; + foreach ($response->headers->getCookies() as $cookie) { + $cookies[] = $this->getCookieHeader( + $cookie->getName(), + $cookie->getValue(), + $cookie->getExpiresTime(), + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + $cookie->isHttpOnly() + ); + } + if (count($cookies) > 0) { + $responseHeaders['Set-Cookie'] = $cookies; + } + + $statusCode = $response->getStatusCode(); + $startTime = defined('LARAVEL_START') ? LARAVEL_START : $request->server->get('REQUEST_TIME_FLOAT'); + $query = $request->getQueryString(); + $htmlData = []; + + $data = [ + 'status' => $statusCode . ' ' . (isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : ''), + 'duration' => $startTime ? $this->formatDuration(microtime(true) - $startTime) : null, + 'peak_memory' => $this->formatBytes(memory_get_peak_usage(true), 1), + ]; + + if ($request instanceof Request) { + + if ($route = $request->route()) { + $htmlData += $this->getRouteInformation($route); + } + + $fulLUrl = $request->fullUrl(); + $data += [ + 'full_url' => strlen($fulLUrl) > 100 ? [$fulLUrl] : $fulLUrl, + ]; + } + + if ($response instanceof RedirectResponse) { + $data['response'] = 'Redirect to ' . $response->getTargetUrl(); + } + + $data += [ + 'response' => $response->headers->get('Content-Type') ? $response->headers->get( + 'Content-Type' + ) : 'text/html', + 'request_format' => $request->getRequestFormat(), + 'request_query' => $request->query->all(), + 'request_request' => $request->request->all(), + 'request_headers' => $request->headers->all(), + 'request_cookies' => $request->cookies->all(), + 'response_headers' => $responseHeaders, + ]; + + if ($this->session) { + $data['session_attributes'] = $this->session->all(); + } + + if (isset($data['request_headers']['authorization'][0])) { + $data['request_headers']['authorization'][0] = substr($data['request_headers']['authorization'][0], 0, 12) . '******'; + } + + foreach ($this->hiddens as $key) { + if (Arr::has($data, $key)) { + Arr::set($data, $key, '******'); + } + } + + foreach ($data as $key => $var) { + if (!is_string($data[$key])) { + $data[$key] = DataCollector::getDefaultVarDumper()->renderVar($var); + } else { + $data[$key] = e($data[$key]); + } + } + + if (class_exists(Telescope::class)) { + $entry = IncomingEntry::make([ + 'requestId' => $this->currentRequestId, + ])->type('debugbar'); + Telescope::$entriesQueue[] = $entry; + $url = route('debugbar.telescope', [$entry->uuid]); + $htmlData['telescope'] = 'View in Telescope'; + } + + $tooltip = [ + 'status' => $data['status'], + 'full_url' => Str::limit($request->fullUrl(), 100), + ]; + + if ($this->request instanceof Request) { + $tooltip += [ + 'action_name' => optional($this->request->route())->getName(), + 'controller_action' => optional($this->request->route())->getActionName(), + ]; + } + + unset($htmlData['as'], $htmlData['uses']); + + return [ + 'data' => $tooltip + $htmlData + $data, + 'tooltip' => array_filter($tooltip), + 'badge' => $statusCode >= 300 ? $data['status'] : null, + ]; + } + + protected function getRouteInformation($route) + { + if (!is_a($route, 'Illuminate\Routing\Route')) { + return []; + } + $uri = head($route->methods()) . ' ' . $route->uri(); + $action = $route->getAction(); + + $result = [ + 'uri' => $uri ?: '-', + ]; + + $result = array_merge($result, $action); + $uses = $action['uses'] ?? null; + $controller = is_string($action['controller'] ?? null) ? $action['controller'] : ''; + + if (request()->hasHeader('X-Livewire')) { + try { + $component = request('components')[0]; + $name = json_decode($component['snapshot'], true)['memo']['name']; + $method = $component['calls'][0]['method']; + $class = app(\Livewire\Mechanisms\ComponentRegistry::class)->getClass($name); + if (class_exists($class) && method_exists($class, $method)) { + $controller = $class . '@' . $method; + $result['controller'] = ltrim($controller, '\\'); + } + } catch (\Throwable $e) { + // + } + } + + if (str_contains($controller, '@')) { + list($controller, $method) = explode('@', $controller); + if (class_exists($controller) && method_exists($controller, $method)) { + $reflector = new \ReflectionMethod($controller, $method); + } + unset($result['uses']); + } elseif ($uses instanceof \Closure) { + $reflector = new \ReflectionFunction($uses); + $result['uses'] = $this->formatVar($uses); + } elseif (is_string($uses) && str_contains($uses, '@__invoke')) { + if (class_exists($controller) && method_exists($controller, 'render')) { + $reflector = new \ReflectionMethod($controller, 'render'); + $result['controller'] = $controller . '@render'; + } + } + + if (isset($reflector)) { + $filename = $this->normalizeFilePath($reflector->getFileName()); + + if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { + $result['file'] = sprintf( + '%s:%s-%s', + $link['url'], + $link['ajax'] ? 'event.preventDefault();$.ajax(this.href);' : '', + $filename, + $reflector->getStartLine(), + $reflector->getEndLine() + ); + + if (isset($result['controller']) && is_string($result['controller'])) { + $result['controller'] .= ''; + } + } else { + $result['file'] = sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()); + } + } + + if (isset($result['middleware']) && is_array($result['middleware'])) { + $middleware = implode(', ', $result['middleware']); + unset($result['middleware']); + $result['middleware'] = $middleware; + } + + return array_filter($result); + } + + private function getCookieHeader($name, $value, $expires, $path, $domain, $secure, $httponly) + { + $cookie = sprintf('%s=%s', $name, urlencode($value ?? '')); + + if (0 !== $expires) { + if (is_numeric($expires)) { + $expires = (int) $expires; + } elseif ($expires instanceof \DateTime) { + $expires = $expires->getTimestamp(); + } else { + $expires = strtotime($expires); + if (false === $expires || -1 == $expires) { + throw new \InvalidArgumentException( + sprintf('The "expires" cookie parameter is not valid.', $expires) + ); + } + } + + $cookie .= '; expires=' . substr( + \DateTime::createFromFormat('U', $expires, new \DateTimeZone('UTC'))->format('D, d-M-Y H:i:s T'), + 0, + -5 + ); + } + + if ($domain) { + $cookie .= '; domain=' . $domain; + } + + $cookie .= '; path=' . $path; + + if ($secure) { + $cookie .= '; secure'; + } + + if ($httponly) { + $cookie .= '; httponly'; + } + + return $cookie; + } +} diff --git a/src/DataCollector/RouteCollector.php b/src/DataCollector/RouteCollector.php new file mode 100644 index 000000000..f548a795e --- /dev/null +++ b/src/DataCollector/RouteCollector.php @@ -0,0 +1,169 @@ +router = $router; + } + + /** + * {@inheritDoc} + */ + public function collect() + { + $route = $this->router->current(); + return $this->getRouteInformation($route); + } + + /** + * Get the route information for a given route. + * + * @param \Illuminate\Routing\Route $route + * @return array + */ + protected function getRouteInformation($route) + { + if (!is_a($route, 'Illuminate\Routing\Route')) { + return []; + } + $uri = head($route->methods()) . ' ' . $route->uri(); + $action = $route->getAction(); + + $result = [ + 'uri' => $uri ?: '-', + ]; + + $result = array_merge($result, $action); + $uses = $action['uses'] ?? null; + $controller = is_string($action['controller'] ?? null) ? $action['controller'] : ''; + + if (request()->hasHeader('X-Livewire')) { + try { + $component = request('components')[0]; + $name = json_decode($component['snapshot'], true)['memo']['name']; + $method = $component['calls'][0]['method']; + $class = app(\Livewire\Mechanisms\ComponentRegistry::class)->getClass($name); + if (class_exists($class) && method_exists($class, $method)) { + $controller = $class . '@' . $method; + $result['controller'] = ltrim($controller, '\\'); + } + } catch (\Throwable $e) { + // + } + } + + if (str_contains($controller, '@')) { + list($controller, $method) = explode('@', $controller); + if (class_exists($controller) && method_exists($controller, $method)) { + $reflector = new \ReflectionMethod($controller, $method); + } + unset($result['uses']); + } elseif ($uses instanceof \Closure) { + $reflector = new \ReflectionFunction($uses); + $result['uses'] = $this->formatVar($uses); + } elseif (is_string($uses) && str_contains($uses, '@__invoke')) { + if (class_exists($controller) && method_exists($controller, 'render')) { + $reflector = new \ReflectionMethod($controller, 'render'); + $result['controller'] = $controller . '@render'; + } + } + + if (isset($reflector)) { + $filename = $this->normalizeFilePath($reflector->getFileName()); + + if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { + $result['file'] = sprintf( + '%s:%s-%s', + $link['url'], + $link['ajax'] ? 'event.preventDefault();$.ajax(this.href);' : '', + $filename, + $reflector->getStartLine(), + $reflector->getEndLine() + ); + + if (isset($result['controller'])) { + $result['controller'] .= ''; + } + } else { + $result['file'] = sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()); + } + } + + if ($middleware = $this->getMiddleware($route)) { + $result['middleware'] = $middleware; + } + + return array_filter($result); + } + + /** + * Get middleware + * + * @param \Illuminate\Routing\Route $route + * @return string + */ + protected function getMiddleware($route) + { + return implode(', ', array_map(function ($middleware) { + return $middleware instanceof Closure ? 'Closure' : $middleware; + }, $route->gatherMiddleware())); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'route'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + $widgets = [ + "route" => [ + "icon" => "share", + "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", + "map" => "route", + "default" => "{}" + ] + ]; + return $widgets; + } + + /** + * Display the route information on the console. + * + * @param array $routes + * @return void + */ + protected function displayRoutes(array $routes) + { + $this->table->setHeaders($this->headers)->setRows($routes); + + $this->table->render($this->getOutput()); + } +} diff --git a/src/DataCollector/SessionCollector.php b/src/DataCollector/SessionCollector.php index d8892134c..e8855efac 100644 --- a/src/DataCollector/SessionCollector.php +++ b/src/DataCollector/SessionCollector.php @@ -5,20 +5,25 @@ use DebugBar\DataCollector\DataCollector; use DebugBar\DataCollector\DataCollectorInterface; use DebugBar\DataCollector\Renderable; +use Illuminate\Support\Arr; class SessionCollector extends DataCollector implements DataCollectorInterface, Renderable { - /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ + /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface|\Illuminate\Contracts\Session\Session $session */ protected $session; + /** @var array */ + protected $hiddens; /** * Create a new SessionCollector * - * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session + * @param \Symfony\Component\HttpFoundation\Session\SessionInterface|\Illuminate\Contracts\Session\Session $session + * @param array $hiddens */ - public function __construct($session) + public function __construct($session, $hiddens = []) { $this->session = $session; + $this->hiddens = $hiddens; } /** @@ -26,10 +31,18 @@ public function __construct($session) */ public function collect() { - $data = array(); - foreach ($this->session->all() as $key => $value) { + $data = $this->session->all(); + + foreach ($this->hiddens as $key) { + if (Arr::has($data, $key)) { + Arr::set($data, $key, '******'); + } + } + + foreach ($data as $key => $value) { $data[$key] = is_string($value) ? $value : $this->formatVar($value); } + return $data; } @@ -46,13 +59,13 @@ public function getName() */ public function getWidgets() { - return array( - "session" => array( + return [ + "session" => [ "icon" => "archive", "widget" => "PhpDebugBar.Widgets.VariableListWidget", "map" => "session", "default" => "{}" - ) - ); + ] + ]; } } diff --git a/src/DataCollector/SymfonyRequestCollector.php b/src/DataCollector/SymfonyRequestCollector.php deleted file mode 100644 index 757e93c8a..000000000 --- a/src/DataCollector/SymfonyRequestCollector.php +++ /dev/null @@ -1,177 +0,0 @@ - - * - */ -class SymfonyRequestCollector extends DataCollector implements DataCollectorInterface, Renderable -{ - /** @var \Symfony\Component\HttpFoundation\Request $request */ - protected $request; - /** @var \Symfony\Component\HttpFoundation\Request $response */ - protected $response; - /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ - protected $session; - - /** - * Create a new SymfonyRequestCollector - * - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \Symfony\Component\HttpFoundation\Request $response - * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session - */ - public function __construct($request, $response, $session = null) - { - $this->request = $request; - $this->response = $response; - $this->session = $session; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'request'; - } - - /** - * {@inheritDoc} - */ - public function getWidgets() - { - return array( - "request" => array( - "icon" => "tags", - "widget" => "PhpDebugBar.Widgets.VariableListWidget", - "map" => "request", - "default" => "{}" - ) - ); - } - - /** - * {@inheritdoc} - */ - public function collect() - { - $request = $this->request; - $response = $this->response; - - $responseHeaders = $response->headers->all(); - $cookies = array(); - foreach ($response->headers->getCookies() as $cookie) { - $cookies[] = $this->getCookieHeader( - $cookie->getName(), - $cookie->getValue(), - $cookie->getExpiresTime(), - $cookie->getPath(), - $cookie->getDomain(), - $cookie->isSecure(), - $cookie->isHttpOnly() - ); - } - if (count($cookies) > 0) { - $responseHeaders['Set-Cookie'] = $cookies; - } - - $statusCode = $response->getStatusCode(); - - $data = array( - 'format' => $request->getRequestFormat(), - 'content_type' => $response->headers->get('Content-Type') ? $response->headers->get( - 'Content-Type' - ) : 'text/html', - 'status_text' => isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : '', - 'status_code' => $statusCode, - 'request_query' => $request->query->all(), - 'request_request' => $request->request->all(), - 'request_headers' => $request->headers->all(), - 'request_server' => $request->server->all(), - 'request_cookies' => $request->cookies->all(), - 'response_headers' => $responseHeaders, - 'path_info' => $request->getPathInfo(), - ); - - if ($this->session) { - $sessionAttributes = array(); - foreach ($this->session->all() as $key => $value) { - $sessionAttributes[$key] = $value; - } - $data['session_attributes'] = $sessionAttributes; - } - - foreach ($data['request_server'] as $key => $value) { - if (str_is('*_KEY', $key) || str_is('*_PASSWORD', $key) - || str_is('*_SECRET', $key) || str_is('*_PW', $key)) { - $data['request_server'][$key] = '******'; - } - } - - if (isset($data['request_headers']['php-auth-pw'])) { - $data['request_headers']['php-auth-pw'] = '******'; - } - - if (isset($data['request_server']['PHP_AUTH_PW'])) { - $data['request_server']['PHP_AUTH_PW'] = '******'; - } - - foreach ($data as $key => $var) { - if (!is_string($data[$key])) { - $data[$key] = $this->formatVar($var); - } - } - - return $data; - } - - private function getCookieHeader($name, $value, $expires, $path, $domain, $secure, $httponly) - { - $cookie = sprintf('%s=%s', $name, urlencode($value)); - - if (0 !== $expires) { - if (is_numeric($expires)) { - $expires = (int) $expires; - } elseif ($expires instanceof \DateTime) { - $expires = $expires->getTimestamp(); - } else { - $expires = strtotime($expires); - if (false === $expires || -1 == $expires) { - throw new \InvalidArgumentException( - sprintf('The "expires" cookie parameter is not valid.', $expires) - ); - } - } - - $cookie .= '; expires=' . substr( - \DateTime::createFromFormat('U', $expires, new \DateTimeZone('UTC'))->format('D, d-M-Y H:i:s T'), - 0, - -5 - ); - } - - if ($domain) { - $cookie .= '; domain=' . $domain; - } - - $cookie .= '; path=' . $path; - - if ($secure) { - $cookie .= '; secure'; - } - - if ($httponly) { - $cookie .= '; httponly'; - } - - return $cookie; - } -} diff --git a/src/DataCollector/ViewCollector.php b/src/DataCollector/ViewCollector.php index d404745e2..70041f50d 100644 --- a/src/DataCollector/ViewCollector.php +++ b/src/DataCollector/ViewCollector.php @@ -2,26 +2,39 @@ namespace Barryvdh\Debugbar\DataCollector; -use DebugBar\Bridge\Twig\TwigCollector; +use Barryvdh\Debugbar\DataFormatter\SimpleFormatter; +use DebugBar\DataCollector\AssetProvider; +use DebugBar\DataCollector\DataCollector; +use DebugBar\DataCollector\Renderable; +use DebugBar\DataCollector\TimeDataCollector; +use Illuminate\Support\Str; use Illuminate\View\View; -use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; -class ViewCollector extends TwigCollector +class ViewCollector extends DataCollector implements Renderable, AssetProvider { - protected $templates = array(); + protected $name; + protected $templates = []; protected $collect_data; + protected $exclude_paths; + protected $group; + protected $timeCollector; /** * Create a ViewCollector * - * @param bool $collectData Collects view data when tru - */ - public function __construct($collectData = true) + * @param bool|string $collectData Collects view data when true + * @param string[] $excludePaths Paths to exclude from collection + * @param int|bool $group Group the same templates together + * @param TimeDataCollector|null TimeCollector + * */ + public function __construct($collectData = true, $excludePaths = [], $group = true, ?TimeDataCollector $timeCollector = null) { + $this->setDataFormatter(new SimpleFormatter()); $this->collect_data = $collectData; - $this->name = 'views'; - $this->templates = array(); - $this->exporter = new ValueExporter(); + $this->templates = []; + $this->exclude_paths = $excludePaths; + $this->group = $group; + $this->timeCollector = $timeCollector; } public function getName() @@ -31,18 +44,29 @@ public function getName() public function getWidgets() { - return array( - 'views' => array( + return [ + 'views' => [ 'icon' => 'leaf', 'widget' => 'PhpDebugBar.Widgets.TemplatesWidget', 'map' => 'views', 'default' => '[]' - ), - 'views:badge' => array( + ], + 'views:badge' => [ 'map' => 'views.nb_templates', 'default' => 0 - ) - ); + ] + ]; + } + + /** + * @return array + */ + public function getAssets() + { + return [ + 'css' => 'widgets/templates/widget.css', + 'js' => 'widgets/templates/widget.js', + ]; } /** @@ -53,48 +77,134 @@ public function getWidgets() public function addView(View $view) { $name = $view->getName(); + $type = null; + $data = $view->getData(); $path = $view->getPath(); - - if (!is_object($path)) { - if ($path) { - $path = ltrim(str_replace(base_path(), '', realpath($path)), '/'); + + if (class_exists('\Inertia\Inertia')) { + list($name, $type, $data, $path) = $this->getInertiaView($name, $data, $path); + } + + if (is_object($path)) { + $type = get_class($view); + $path = null; + } + + if ($path) { + if (!$type) { + if (substr($path, -10) == '.blade.php') { + $type = 'blade'; + } else { + $type = pathinfo($path, PATHINFO_EXTENSION); + } + } + + $shortPath = $this->normalizeFilePath($path); + foreach ($this->exclude_paths as $excludePath) { + if (str_starts_with($shortPath, $excludePath)) { + return; + } } + } + + $this->addTemplate($name, $data, $type, $path); + + if ($this->timeCollector !== null) { + $time = microtime(true); + $this->timeCollector->addMeasure('View: ' . $name, $time, $time, [], 'views', 'View'); + } + } + + private function getInertiaView(string $name, array $data, ?string $path) + { + if (isset($data['page']) && is_array($data['page'])) { + $data = $data['page']; + } - if (substr($path, -10) == '.blade.php') { - $type = 'blade'; - } else { + if (isset($data['props'], $data['component'])) { + $name = $data['component']; + $data = $data['props']; + + if ($files = glob(resource_path(config('debugbar.options.views.inertia_pages') .'/'. $name . '.*'))) { + $path = $files[0]; $type = pathinfo($path, PATHINFO_EXTENSION); + + if (in_array($type, ['js', 'jsx'])) { + $type = 'react'; + } } - } else { - $type = get_class($view); - $path = ''; } - if (!$this->collect_data) { - $params = array_keys($view->getData()); + return [$name, $type ?? '', $data, $path]; + } + + public function addInertiaAjaxView(array $data) + { + list($name, $type, $data, $path) = $this->getInertiaView('', $data, ''); + + if (! $name) { + return; + } + + $this->addTemplate($name, $data, $type, $path); + } + + private function addTemplate(string $name, array $data, ?string $type, ?string $path) + { + // Prevent duplicates + $hash = $type . $path . $name . ($this->collect_data ? implode(array_keys($data)) : ''); + + if ($this->collect_data === 'keys') { + $params = array_keys($data); + } elseif ($this->collect_data) { + $params = array_map( + fn ($value) => $this->getDataFormatter()->formatVar($value), + $data + ); } else { - $data = array(); - foreach ($view->getData() as $key => $value) { - $data[$key] = $this->exporter->exportValue($value); - } - $params = $data; + $params = []; } - $this->templates[] = array( - 'name' => $path ? sprintf('%s (%s)', $name, $path) : $name, - 'param_count' => count($params), + $template = [ + 'name' => $name, + 'param_count' => $this->collect_data ? count($params) : null, 'params' => $params, + 'start' => microtime(true), 'type' => $type, - ); + 'hash' => $hash, + ]; + + if ($path && $this->getXdebugLinkTemplate()) { + $template['xdebug_link'] = $this->getXdebugLink($path); + } + + $this->templates[] = $template; } public function collect() { - $templates = $this->templates; + if ($this->group === true || count($this->templates) > $this->group) { + $templates = []; + foreach ($this->templates as $template) { + $hash = $template['hash']; + if (!isset($templates[$hash])) { + $template['render_count'] = 0; + $template['name_original'] = $template['name']; + $templates[$hash] = $template; + } + + $templates[$hash]['render_count']++; + $templates[$hash]['name'] = $templates[$hash]['render_count'] . 'x ' . $templates[$hash]['name_original']; + } + $templates = array_values($templates); + } else { + $templates = $this->templates; + } - return array( - 'nb_templates' => count($templates), + return [ + 'count' => count($this->templates), + 'nb_templates' => count($this->templates), 'templates' => $templates, - ); + ]; } } diff --git a/src/DataFormatter/QueryFormatter.php b/src/DataFormatter/QueryFormatter.php new file mode 100644 index 000000000..645b6ff13 --- /dev/null +++ b/src/DataFormatter/QueryFormatter.php @@ -0,0 +1,73 @@ +checkBindings($binding); + $binding = '[' . implode(',', $binding) . ']'; + } + + if (is_object($binding)) { + $binding = json_encode($binding); + } + } + + return $bindings; + } + + /** + * Format a source object. + * + * @param object|null $source If the backtrace is disabled, the $source will be null. + * @return string + */ + public function formatSource($source, $short = false) + { + if (! is_object($source)) { + return ''; + } + + $parts = []; + + if (!$short && $source->namespace) { + $parts['namespace'] = $source->namespace . '::'; + } + + $parts['name'] = $short ? basename($source->name) : $source->name; + $parts['line'] = ':' . $source->line; + + return implode($parts); + } +} diff --git a/src/DataFormatter/SimpleFormatter.php b/src/DataFormatter/SimpleFormatter.php new file mode 100644 index 000000000..674a52813 --- /dev/null +++ b/src/DataFormatter/SimpleFormatter.php @@ -0,0 +1,107 @@ +exportValue($data); + } + + /** + * Converts a PHP value to a string. + * + * @param mixed $value The PHP value + * @param int $depth Only for internal usage + * @param bool $deep Only for internal usage + * + * @return string The string representation of the given value + * @author Bernhard Schussek + */ + private function exportValue($value, $depth = 1, $deep = false) + { + if ($value instanceof \__PHP_Incomplete_Class) { + return sprintf('__PHP_Incomplete_Class(%s)', $this->getClassNameFromIncomplete($value)); + } + + if (is_object($value)) { + if ($value instanceof \DateTimeInterface) { + return sprintf('Object(%s) - %s', get_class($value), $value->format(\DateTime::ATOM)); + } + + return sprintf('Object(%s)', get_class($value)); + } + + if (is_array($value)) { + if (empty($value)) { + return '[]'; + } + + $indent = str_repeat(' ', $depth); + + $a = []; + foreach ($value as $k => $v) { + if (is_array($v)) { + $deep = true; + } + $a[] = sprintf('%s => %s', $k, $this->exportValue($v, $depth + 1, $deep)); + } + + if ($deep) { + $args = [$indent, implode(sprintf(", \n%s", $indent), $a), str_repeat(' ', $depth - 1)]; + return sprintf("[\n%s%s\n%s]", ...$args); + } + + $s = sprintf('[%s]', implode(', ', $a)); + + if (80 > strlen($s)) { + return $s; + } + + return sprintf("[\n%s%s\n]", $indent, implode(sprintf(",\n%s", $indent), $a)); + } + + if (is_resource($value)) { + return sprintf('Resource(%s#%d)', get_resource_type($value), $value); + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } + + /** + * @param \__PHP_Incomplete_Class $value + * @return mixed + * @author Bernhard Schussek + */ + private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value) + { + $array = new \ArrayObject($value); + + return $array['__PHP_Incomplete_Class_Name']; + } +} diff --git a/src/DebugbarViewEngine.php b/src/DebugbarViewEngine.php new file mode 100644 index 000000000..ebae06624 --- /dev/null +++ b/src/DebugbarViewEngine.php @@ -0,0 +1,75 @@ +engine = $engine; + $this->laravelDebugbar = $laravelDebugbar; + $this->exclude_paths = app('config')->get('debugbar.options.views.exclude_paths', []); + } + + /** + * @param string $path + * @param array $data + * @return string + */ + public function get($path, array $data = []) + { + $basePath = base_path(); + $shortPath = @file_exists((string) $path) ? realpath($path) : $path; + + if (str_starts_with($shortPath, $basePath)) { + $shortPath = ltrim( + str_replace('\\', '/', substr($shortPath, strlen($basePath))), + '/' + ); + } + + foreach ($this->exclude_paths as $excludePath) { + if (str_starts_with($shortPath, $excludePath)) { + return $this->engine->get($path, $data); + } + } + + return $this->laravelDebugbar->measure($shortPath, function () use ($path, $data) { + return $this->engine->get($path, $data); + }, 'views'); + } + + /** + * NOTE: This is done to support other Engine swap (example: Livewire). + * @param $name + * @param $arguments + * @return mixed + */ + public function __call($name, $arguments) + { + return $this->engine->$name(...$arguments); + } +} diff --git a/src/Facade.php b/src/Facade.php index 678a2b3f4..933040d63 100644 --- a/src/Facade.php +++ b/src/Facade.php @@ -1,12 +1,30 @@ -cssFiles['laravel'] = __DIR__ . '/Resources/laravel-debugbar.css'; - $this->cssVendors['fontawesome'] = __DIR__ . '/Resources/vendor/font-awesome/style.css'; + $this->jsFiles['laravel-cache'] = __DIR__ . '/Resources/cache/widget.js'; + $this->jsFiles['laravel-queries'] = __DIR__ . '/Resources/queries/widget.js'; + + $this->setTheme(config('debugbar.theme', 'auto')); } /** @@ -29,7 +34,6 @@ public function __construct(DebugBar $debugBar, $baseUrl = null, $basePath = nul */ public function setUrlGenerator($url) { - } /** @@ -37,24 +41,45 @@ public function setUrlGenerator($url) */ public function renderHead() { - $cssRoute = route('debugbar.assets.css', [ - 'v' => $this->getModifiedTime('css') - ]); + $cssRoute = preg_replace('/\Ahttps?:\/\/[^\/]+/', '', route('debugbar.assets.css', [ + 'v' => $this->getModifiedTime('css'), + ])); - $jsRoute = route('debugbar.assets.js', [ + $jsRoute = preg_replace('/\Ahttps?:\/\/[^\/]+/', '', route('debugbar.assets.js', [ 'v' => $this->getModifiedTime('js') - ]); + ])); + + $nonce = $this->getNonceAttribute(); - $html = ""; - $html .= ""; + $html = ""; + $html .= ""; if ($this->isJqueryNoConflictEnabled()) { - $html .= '' . "\n"; + $html .= "jQuery.noConflict(true);" . "\n"; } + $inlineHtml = $this->getInlineHtml(); + if ($nonce != '') { + $inlineHtml = preg_replace("/<(script|style)>/", "<$1{$nonce}>", $inlineHtml); + } + $html .= $inlineHtml; + + return $html; } + protected function getInlineHtml() + { + $html = ''; + + foreach (['head', 'css', 'js'] as $asset) { + foreach ($this->getAssets('inline_' . $asset) as $item) { + $html .= $item . "\n"; + } + } + + return $html; + } /** * Get the last modified time of any assets. * @@ -107,14 +132,14 @@ protected function makeUriRelativeTo($uri, $root) } if (is_array($uri)) { - $uris = array(); + $uris = []; foreach ($uri as $u) { $uris[] = $this->makeUriRelativeTo($u, $root); } return $uris; } - if (substr($uri, 0, 1) === '/' || preg_match('/^([a-zA-Z]+:\/\/|[a-zA-Z]:\/|[a-zA-Z]:\\\)/', $uri)) { + if (substr($uri ?? '', 0, 1) === '/' || preg_match('/^([a-zA-Z]+:\/\/|[a-zA-Z]:\/|[a-zA-Z]:\\\)/', $uri ?? '')) { return $uri; } return rtrim($root, '/') . "/$uri"; diff --git a/src/LaravelDebugbar.php b/src/LaravelDebugbar.php index 335e25d74..6ef7d8eae 100644 --- a/src/LaravelDebugbar.php +++ b/src/LaravelDebugbar.php @@ -1,51 +1,68 @@ -app = $app; $this->version = $app->version(); - $this->is_lumen = str_contains($this->version, 'Lumen'); + $this->is_lumen = Str::contains($this->version, 'Lumen'); + if ($this->is_lumen) { + $this->version = Str::betweenFirst($app->version(), '(', ')'); + } else { + $this->setRequestIdGenerator(new RequestIdGenerator()); + } + } + + /** + * Returns the HTTP driver + * + * If no http driver where defined, a PhpHttpDriver is automatically created + * + * @return HttpDriverInterface + */ + public function getHttpDriver() + { + if ($this->httpDriver === null) { + $this->httpDriver = $this->app->make(SymfonyHttpDriver::class); + } + + return $this->httpDriver; } /** @@ -118,13 +167,24 @@ public function boot() return; } - /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugbar */ - $debugbar = $this; - /** @var Application $app */ $app = $this->app; - $this->selectStorage($debugbar); + /** @var \Illuminate\Config\Repository $config */ + $config = $app['config']; + + /** @var \Illuminate\Events\Dispatcher|null $events */ + $events = isset($app['events']) ? $app['events'] : null; + + $this->editorTemplateLink = $config->get('debugbar.editor') ?: null; + $this->remoteServerReplacements = $this->getRemoteServerReplacements(); + + // Set custom error handler + if ($config->get('debugbar.error_handler', false)) { + $this->prevErrorHandler = set_error_handler([$this, 'handleError']); + } + + $this->selectStorage($this); if ($this->shouldCollect('phpinfo', true)) { $this->addCollector(new PhpInfoCollector()); @@ -132,96 +192,129 @@ public function boot() if ($this->shouldCollect('messages', true)) { $this->addCollector(new MessagesCollector()); + + if ($config->get('debugbar.options.messages.trace', true)) { + $this['messages']->collectFileTrace(true); + } + + if ($config->get('debugbar.options.messages.capture_dumps', false)) { + $originalHandler = \Symfony\Component\VarDumper\VarDumper::setHandler(function ($var) use (&$originalHandler) { + if ($originalHandler) { + $originalHandler($var); + } + + self::addMessage($var); + }); + } } if ($this->shouldCollect('time', true)) { - $this->addCollector(new TimeDataCollector()); - - if ( ! $this->isLumen()) { - $this->app->booted( - function () use ($debugbar) { - $startTime = $this->app['request']->server('REQUEST_TIME_FLOAT'); - if ($startTime) { - $debugbar['time']->addMeasure('Booting', $startTime, microtime(true)); - } + $startTime = $app['request']->server('REQUEST_TIME_FLOAT'); + + if (!$this->hasCollector('time')) { + $this->addCollector(new TimeDataCollector($startTime)); + } + + if ($config->get('debugbar.options.time.memory_usage')) { + $this['time']->showMemoryUsage(); + } + + if ($startTime && !$this->isLumen()) { + $app->booted( + function () use ($startTime) { + $this->addMeasure('Booting', $startTime, microtime(true), [], 'time'); } ); } - $debugbar->startMeasure('application', 'Application'); + $this->startMeasure('application', 'Application', 'time'); + + if ($events) { + $events->listen(\Illuminate\Routing\Events\Routing::class, function() { + $this->startMeasure('Routing'); + }); + $events->listen(\Illuminate\Routing\Events\RouteMatched::class, function() { + $this->stopMeasure('Routing'); + }); + + $events->listen(\Illuminate\Routing\Events\PreparingResponse::class, function() { + $this->startMeasure('Preparing Response'); + }); + $events->listen(\Illuminate\Routing\Events\ResponsePrepared::class, function() { + $this->stopMeasure('Preparing Response'); + }); + } } if ($this->shouldCollect('memory', true)) { $this->addCollector(new MemoryCollector()); + $this['memory']->setPrecision($config->get('debugbar.options.memory.precision', 0)); + + if (function_exists('memory_reset_peak_usage') && $config->get('debugbar.options.memory.reset_peak')) { + memory_reset_peak_usage(); + } + if ($config->get('debugbar.options.memory.with_baseline')) { + $this['memory']->resetMemoryBaseline(); + } } if ($this->shouldCollect('exceptions', true)) { try { - $exceptionCollector = new ExceptionsCollector(); - $exceptionCollector->setChainExceptions( - $this->app['config']->get('debugbar.options.exceptions.chain', true) + $this->addCollector(new ExceptionsCollector()); + $this['exceptions']->setChainExceptions( + $config->get('debugbar.options.exceptions.chain', true) ); - $this->addCollector($exceptionCollector); - } catch (\Exception $e) { + } catch (Exception $e) { } } if ($this->shouldCollect('laravel', false)) { - $this->addCollector(new LaravelCollector($this->app)); + $this->addCollector(new LaravelCollector($app)); } if ($this->shouldCollect('default_request', false)) { $this->addCollector(new RequestDataCollector()); } - if ($this->shouldCollect('events', false) && isset($this->app['events'])) { + if ($this->shouldCollect('events', false) && $events) { try { - $startTime = $this->app['request']->server('REQUEST_TIME_FLOAT'); - $eventCollector = new EventCollector($startTime); - $this->addCollector($eventCollector); - $this->app['events']->subscribe($eventCollector); - - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add EventCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + $startTime = $app['request']->server('REQUEST_TIME_FLOAT'); + $collectData = $config->get('debugbar.options.events.data', false); + $excludedEvents = $config->get('debugbar.options.events.excluded', []); + $this->addCollector(new EventCollector($startTime, $collectData, $excludedEvents)); + $events->subscribe($this['event']); + } catch (Exception $e) { + $this->addCollectorException('Cannot add EventCollector', $e); } } - if ($this->shouldCollect('views', true) && isset($this->app['events'])) { + if ($this->shouldCollect('views', true) && $events) { try { - $collectData = $this->app['config']->get('debugbar.options.views.data', true); - $this->addCollector(new ViewCollector($collectData)); - $this->app['events']->listen( + $collectData = $config->get('debugbar.options.views.data', true); + $excludePaths = $config->get('debugbar.options.views.exclude_paths', []); + $group = $config->get('debugbar.options.views.group', true); + if ($this->hasCollector('time') && $config->get('debugbar.options.views.timeline', false)) { + $timeCollector = $this['time']; + } else { + $timeCollector = null; + } + $this->addCollector(new ViewCollector($collectData, $excludePaths, $group, $timeCollector)); + $events->listen( 'composing:*', - function ($view) use ($debugbar) { - $debugbar['views']->addView($view); + function ($event, $params) { + $this['views']->addView($params[0]); } ); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add ViewCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) - ); + } catch (Exception $e) { + $this->addCollectorException('Cannot add ViewCollector', $e); } } if (!$this->isLumen() && $this->shouldCollect('route')) { try { - $this->addCollector($this->app->make('Barryvdh\Debugbar\DataCollector\IlluminateRouteCollector')); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add RouteCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + $this->addCollector($app->make(RouteCollector::class)); + } catch (Exception $e) { + $this->addCollectorException('Cannot add RouteCollector', $e); } } @@ -230,128 +323,240 @@ function ($view) use ($debugbar) { if ($this->hasCollector('messages')) { $logger = new MessagesCollector('log'); $this['messages']->aggregate($logger); - $this->app['log']->listen( - function ($level, $message, $context) use ($logger) { + $app['log']->listen( + function (\Illuminate\Log\Events\MessageLogged $log) use ($logger) { try { - $logMessage = (string) $message; + $logMessage = (string) $log->message; if (mb_check_encoding($logMessage, 'UTF-8')) { - $logMessage .= (!empty($context) ? ' ' . json_encode($context) : ''); + $context = $log->context; + $logMessage .= (!empty($context) ? ' ' . json_encode($context, JSON_PRETTY_PRINT) : ''); } else { $logMessage = "[INVALID UTF-8 DATA]"; } - } catch (\Exception $e) { + } catch (Exception $e) { $logMessage = "[Exception: " . $e->getMessage() . "]"; } $logger->addMessage( - '[' . date('H:i:s') . '] ' . "LOG.$level: " . $logMessage, - $level, + '[' . date('H:i:s') . '] ' . "LOG.{$log->level}: " . $logMessage, + $log->level, false ); } ); } else { - $this->addCollector(new MonologCollector($this->app['log']->getMonolog())); + $this->addCollector(new MonologCollector($this->getMonologLogger())); } - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add LogsCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) - ); + } catch (Exception $e) { + $this->addCollectorException('Cannot add LogsCollector', $e); } } - if ($this->shouldCollect('db', true) && isset($this->app['db'])) { - $db = $this->app['db']; - if ($debugbar->hasCollector('time') && $this->app['config']->get( - 'debugbar.options.db.timeline', - false - ) - ) { - $timeCollector = $debugbar->getCollector('time'); + if ($this->shouldCollect('db', true) && isset($app['db']) && $events) { + if ($this->hasCollector('time') && $config->get('debugbar.options.db.timeline', false)) { + $timeCollector = $this['time']; } else { $timeCollector = null; } $queryCollector = new QueryCollector($timeCollector); - if ($this->app['config']->get('debugbar.options.db.with_params')) { + $queryCollector->setDataFormatter(new QueryFormatter()); + $queryCollector->setLimits($config->get('debugbar.options.db.soft_limit'), $config->get('debugbar.options.db.hard_limit')); + $queryCollector->setDurationBackground($config->get('debugbar.options.db.duration_background')); + + $threshold = $config->get('debugbar.options.db.slow_threshold', false); + if ($threshold && !$config->get('debugbar.options.db.only_slow_queries', true)) { + $queryCollector->setSlowThreshold($threshold); + } + + if ($config->get('debugbar.options.db.with_params')) { $queryCollector->setRenderSqlWithParams(true); } - if ($this->app['config']->get('debugbar.options.db.backtrace')) { - $queryCollector->setFindSource(true); + if ($dbBacktrace = $config->get('debugbar.options.db.backtrace')) { + $middleware = ! $this->is_lumen ? $app['router']->getMiddleware() : []; + $queryCollector->setFindSource($dbBacktrace, $middleware); + } + + if ($excludePaths = $config->get('debugbar.options.db.exclude_paths')) { + $queryCollector->mergeExcludePaths($excludePaths); + } + + if ($excludeBacktracePaths = $config->get('debugbar.options.db.backtrace_exclude_paths')) { + $queryCollector->mergeBacktraceExcludePaths($excludeBacktracePaths); } - if ($this->app['config']->get('debugbar.options.db.explain.enabled')) { - $types = $this->app['config']->get('debugbar.options.db.explain.types'); + if ($config->get('debugbar.options.db.explain.enabled')) { + $types = $config->get('debugbar.options.db.explain.types'); $queryCollector->setExplainSource(true, $types); } - if ($this->app['config']->get('debugbar.options.db.hints', true)) { + if ($config->get('debugbar.options.db.hints', true)) { $queryCollector->setShowHints(true); } + if ($config->get('debugbar.options.db.show_copy', false)) { + $queryCollector->setShowCopyButton(true); + } + $this->addCollector($queryCollector); try { - $db->listen( - function ($query, $bindings = null, $time = null, $connectionName = null) use ($db, $queryCollector) { - // Laravel 5.2 changed the way some core events worked. We must account for - // the first argument being an "event object", where arguments are passed - // via object properties, instead of individual arguments. - if ( $query instanceof \Illuminate\Database\Events\QueryExecuted ) { - $bindings = $query->bindings; - $time = $query->time; - $connection = $query->connection; - - $query = $query->sql; - } else { - $connection = $db->connection($connectionName); + $events->listen( + function (\Illuminate\Database\Events\QueryExecuted $query) { + if (!app(static::class)->shouldCollect('db', true)) { + return; // Issue 776 : We've turned off collecting after the listener was attached } - $queryCollector->addQuery((string) $query, $bindings, $time, $connection); + $threshold = app('config')->get('debugbar.options.db.slow_threshold', false); + $onlyThreshold = app('config')->get('debugbar.options.db.only_slow_queries', true); + //allow collecting only queries slower than a specified amount of milliseconds + if (!$onlyThreshold || !$threshold || $query->time > $threshold) { + $this['queries']->addQuery($query); + } } ); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add listen to Queries for Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) + } catch (Exception $e) { + $this->addCollectorException('Cannot listen to Queries', $e); + } + + try { + $events->listen( + \Illuminate\Database\Events\TransactionBeginning::class, + function ($transaction) { + $this['queries']->collectTransactionEvent('Begin Transaction', $transaction->connection); + } + ); + + $events->listen( + \Illuminate\Database\Events\TransactionCommitted::class, + function ($transaction) { + $this['queries']->collectTransactionEvent('Commit Transaction', $transaction->connection); + } ); + + $events->listen( + \Illuminate\Database\Events\TransactionRolledBack::class, + function ($transaction) { + $this['queries']->collectTransactionEvent('Rollback Transaction', $transaction->connection); + } + ); + + $events->listen( + 'connection.*.beganTransaction', + function ($event, $params) { + $this['queries']->collectTransactionEvent('Begin Transaction', $params[0]); + } + ); + + $events->listen( + 'connection.*.committed', + function ($event, $params) { + $this['queries']->collectTransactionEvent('Commit Transaction', $params[0]); + } + ); + + $events->listen( + 'connection.*.rollingBack', + function ($event, $params) { + $this['queries']->collectTransactionEvent('Rollback Transaction', $params[0]); + } + ); + + $events->listen( + function (\Illuminate\Database\Events\ConnectionEstablished $event) { + $this['queries']->collectTransactionEvent('Connection Established', $event->connection); + + if (app('config')->get('debugbar.options.db.memory_usage')) { + $event->connection->beforeExecuting(function () { + $this['queries']->startMemoryUsage(); + }); + } + } + ); + } catch (Exception $e) { + $this->addCollectorException('Cannot listen transactions to Queries', $e); } } - if ($this->shouldCollect('mail', true) && class_exists('Illuminate\Mail\MailServiceProvider')) { + if ($this->shouldCollect('models', true) && $events) { try { - $mailer = $this->app['mailer']->getSwiftMailer(); - $this->addCollector(new SwiftMailCollector($mailer)); - if ($this->app['config']->get('debugbar.options.mail.full_log') && $this->hasCollector( - 'messages' - ) - ) { - $this['messages']->aggregate(new SwiftLogCollector($mailer)); + $this->addCollector(new ObjectCountCollector('models')); + $eventList = ['retrieved', 'created', 'updated', 'deleted']; + $this['models']->setKeyMap(array_combine($eventList, array_map('ucfirst', $eventList))); + $this['models']->collectCountSummary(true); + foreach ($eventList as $event) { + $events->listen("eloquent.{$event}: *", function ($event, $models) { + $event = explode(': ', $event); + $count = count(array_filter($models)); + $this['models']->countClass($event[1], $count, explode('.', $event[0])[1]); + }); } - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add MailCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) - ); + } catch (Exception $e) { + $this->addCollectorException('Cannot add Models Collector', $e); + } + } + + if ($this->shouldCollect('livewire', true) && $app->bound('livewire')) { + try { + $this->addCollector($app->make(LivewireCollector::class)); + } catch (Exception $e) { + $this->addCollectorException('Cannot add Livewire Collector', $e); + } + } + + if ($this->shouldCollect('mail', true) && class_exists('Illuminate\Mail\MailServiceProvider') && $events) { + try { + $mailCollector = new SymfonyMailCollector(); + $this->addCollector($mailCollector); + $events->listen(function (MessageSent $event) use ($mailCollector) { + $mailCollector->addSymfonyMessage($event->sent->getSymfonySentMessage()); + }); + + if ($config->get('debugbar.options.mail.show_body') || $config->get('debugbar.options.mail.full_log')) { + $mailCollector->showMessageBody(); + } + + if ($this->hasCollector('time') && $config->get('debugbar.options.mail.timeline')) { + $transport = $app['mailer']->getSymfonyTransport(); + $app['mailer']->setSymfonyTransport(new class ($transport, $this) extends AbstractTransport{ + private $originalTransport; + private $laravelDebugbar; + + public function __construct($transport, $laravelDebugbar) + { + $this->originalTransport = $transport; + $this->laravelDebugbar = $laravelDebugbar; + } + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + return $this->laravelDebugbar['time']->measure( + 'mail: ' . Str::limit($message->getSubject(), 100), + function () use ($message, $envelope) { + return $this->originalTransport->send($message, $envelope); + }, + 'mail' + ); + } + protected function doSend(SentMessage $message): void + { + } + public function __toString(): string + { + return $this->originalTransport->__toString(); + } + }); + } + } catch (Exception $e) { + $this->addCollectorException('Cannot add SymfonyMailCollector', $e); } } if ($this->shouldCollect('logs', false)) { try { - $file = $this->app['config']->get('debugbar.options.logs.file'); + $file = $config->get('debugbar.options.logs.file'); $this->addCollector(new LogsCollector($file)); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add LogsCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) - ); + } catch (Exception $e) { + $this->addCollectorException('Cannot add LogsCollector', $e); } } if ($this->shouldCollect('files', false)) { @@ -360,39 +565,72 @@ function ($query, $bindings = null, $time = null, $connectionName = null) use ($ if ($this->shouldCollect('auth', false)) { try { - if($this->checkVersion('5.2')) { - // fix for compatibility with Laravel 5.2.* - $guards = array_keys($this->app['config']->get('auth.guards')); - $authCollector = new MultiAuthCollector($app['auth'], $guards); - } else { - $authCollector = new AuthCollector($app['auth']); - } + $guards = $config->get('auth.guards', []); + $this->addCollector(new MultiAuthCollector($app['auth'], $guards)); - $authCollector->setShowName( - $this->app['config']->get('debugbar.options.auth.show_name') + $this['auth']->setShowName( + $config->get('debugbar.options.auth.show_name') ); - $this->addCollector($authCollector); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add AuthCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) + $this['auth']->setShowGuardsData( + $config->get('debugbar.options.auth.show_guards', true) ); + } catch (Exception $e) { + $this->addCollectorException('Cannot add AuthCollector', $e); } } if ($this->shouldCollect('gate', false)) { try { - $gateCollector = $this->app->make('Barryvdh\Debugbar\DataCollector\GateCollector'); - $this->addCollector($gateCollector); - } catch (\Exception $e){ - // No Gate collector + $this->addCollector($app->make(GateCollector::class)); + + if ($config->get('debugbar.options.gate.trace', false)) { + $this['gate']->collectFileTrace(true); + $this['gate']->addBacktraceExcludePaths($config->get('debugbar.options.gate.exclude_paths',[])); + } + } catch (Exception $e) { + $this->addCollectorException('Cannot add GateCollector', $e); + } + } + + if ($this->shouldCollect('cache', false) && $events) { + try { + $collectValues = $config->get('debugbar.options.cache.values', true); + $startTime = $app['request']->server('REQUEST_TIME_FLOAT'); + $this->addCollector(new CacheCollector($startTime, $collectValues)); + $events->subscribe($this['cache']); + } catch (Exception $e) { + $this->addCollectorException('Cannot add CacheCollector', $e); + } + } + + if ($this->shouldCollect('jobs', false) && $events) { + try { + $this->addCollector(new ObjectCountCollector('jobs', 'briefcase')); + $events->listen(\Illuminate\Queue\Events\JobQueued::class, function ($event) { + $this['jobs']->countClass($event->job); + }); + } catch (Exception $e) { + $this->addCollectorException('Cannot add Jobs Collector', $e); + } + } + + if ($this->shouldCollect('pennant', false)) { + if (class_exists('Laravel\Pennant\FeatureManager') && $app->bound(\Laravel\Pennant\FeatureManager::class)) { + $featureManager = $app->make(\Laravel\Pennant\FeatureManager::class); + try { + $this->addCollector(new PennantCollector($featureManager)); + } catch (Exception $e) { + $this->addCollectorException('Cannot add PennantCollector', $e); + } } } $renderer = $this->getJavascriptRenderer(); - $renderer->setIncludeVendors($this->app['config']->get('debugbar.include_vendors', true)); - $renderer->setBindAjaxHandlerToXHR($app['config']->get('debugbar.capture_ajax', true)); + $renderer->setHideEmptyTabs($config->get('debugbar.hide_empty_tabs', false)); + $renderer->setIncludeVendors($config->get('debugbar.include_vendors', true)); + $renderer->setBindAjaxHandlerToFetch($config->get('debugbar.capture_ajax', true)); + $renderer->setBindAjaxHandlerToXHR($config->get('debugbar.capture_ajax', true)); + $renderer->setDeferDatasets($config->get('debugbar.defer_datasets', false)); $this->booted = true; } @@ -402,18 +640,73 @@ public function shouldCollect($name, $default = false) return $this->app['config']->get('debugbar.collectors.' . $name, $default); } + /** + * Adds a data collector + * + * @param DataCollectorInterface $collector + * + * @throws DebugBarException + * @return $this + */ + public function addCollector(DataCollectorInterface $collector) + { + parent::addCollector($collector); + + if (method_exists($collector, 'useHtmlVarDumper')) { + $collector->useHtmlVarDumper(); + } + if (method_exists($collector, 'setEditorLinkTemplate') && $this->editorTemplateLink) { + $collector->setEditorLinkTemplate($this->editorTemplateLink); + } + if (method_exists($collector, 'addXdebugReplacements') && $this->remoteServerReplacements) { + $collector->addXdebugReplacements($this->remoteServerReplacements); + } + + return $this; + } + + /** + * Handle silenced errors + * + * @param $level + * @param $message + * @param string $file + * @param int $line + * @param array $context + * @throws \ErrorException + */ + public function handleError($level, $message, $file = '', $line = 0, $context = []) + { + if ($this->hasCollector('exceptions')) { + $this['exceptions']->addWarning($level, $message, $file, $line); + } + + if ($this->hasCollector('messages')) { + $file = $file ? ' on ' . $this['messages']->normalizeFilePath($file) . ":{$line}" : ''; + $this['messages']->addMessage($message . $file, 'deprecation'); + } + + if (! $this->prevErrorHandler) { + return; + } + + return call_user_func($this->prevErrorHandler, $level, $message, $file, $line, $context); + } + /** * Starts a measure * * @param string $name Internal name, used to stop the measure * @param string $label Public name + * @param string|null $collector + * @param string|null $group */ - public function startMeasure($name, $label = null) + public function startMeasure($name, $label = null, $collector = null, $group = null) { if ($this->hasCollector('time')) { - /** @var \DebugBar\DataCollector\TimeDataCollector $collector */ - $collector = $this->getCollector('time'); - $collector->startMeasure($name, $label); + /** @var \DebugBar\DataCollector\TimeDataCollector */ + $time = $this->getCollector('time'); + $time->startMeasure($name, $label, $collector, $group); } } @@ -429,8 +722,8 @@ public function stopMeasure($name) $collector = $this->getCollector('time'); try { $collector->stopMeasure($name); - } catch (\Exception $e) { - // $this->addException($e); + } catch (Exception $e) { + // $this->addThrowable($e); } } } @@ -439,21 +732,49 @@ public function stopMeasure($name) * Adds an exception to be profiled in the debug bar * * @param Exception $e + * @deprecated in favor of addThrowable */ public function addException(Exception $e) + { + return $this->addThrowable($e); + } + + /** + * Adds an exception to be profiled in the debug bar + * + * @param Throwable $e + */ + public function addThrowable($e) { if ($this->hasCollector('exceptions')) { /** @var \DebugBar\DataCollector\ExceptionsCollector $collector */ $collector = $this->getCollector('exceptions'); - $collector->addException($e); + $collector->addThrowable($e); } } + /** + * Register collector exceptions + * + * @param string $message + * @param Exception $exception + */ + protected function addCollectorException(string $message, Exception $exception) + { + $this->addThrowable( + new Exception( + $message . ' on Laravel Debugbar: ' . $exception->getMessage(), + (int) $exception->getCode(), + $exception + ) + ); + } + /** * Returns a JavascriptRenderer for this instance * * @param string $baseUrl - * @param string $basePathng + * @param string $basePath * @return JavascriptRenderer */ public function getJavascriptRenderer($baseUrl = null, $basePath = null) @@ -473,14 +794,24 @@ public function getJavascriptRenderer($baseUrl = null, $basePath = null) */ public function modifyResponse(Request $request, Response $response) { + /** @var Application $app */ $app = $this->app; - if ($app->runningInConsole() || !$this->isEnabled() || $this->isDebugbarRequest()) { + if (!$this->isEnabled() || !$this->booted || $this->isDebugbarRequest() || $this->responseIsModified) { return $response; } + // Prevent duplicate modification + $this->responseIsModified = true; + + // Set the Response if required + $httpDriver = $this->getHttpDriver(); + if ($httpDriver instanceof SymfonyHttpDriver) { + $httpDriver->setResponse($response); + } + // Show the Http Response Exception in the Debugbar, when available if (isset($response->exception)) { - $this->addException($response->exception); + $this->addThrowable($response->exception); } if ($this->shouldCollect('config', false)) { @@ -488,102 +819,97 @@ public function modifyResponse(Request $request, Response $response) $configCollector = new ConfigCollector(); $configCollector->setData($app['config']->all()); $this->addCollector($configCollector); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add ConfigCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + } catch (Exception $e) { + $this->addCollectorException('Cannot add ConfigCollector', $e); } } - if ($this->app->bound(SessionManager::class)){ - + $sessionHiddens = $app['config']->get('debugbar.options.session.hiddens', []); + if ($app->bound(SessionManager::class)) { /** @var \Illuminate\Session\SessionManager $sessionManager */ $sessionManager = $app->make(SessionManager::class); - $httpDriver = new SymfonyHttpDriver($sessionManager, $response); - $this->setHttpDriver($httpDriver); if ($this->shouldCollect('session') && ! $this->hasCollector('session')) { try { - $this->addCollector(new SessionCollector($sessionManager)); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add SessionCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + $this->addCollector(new SessionCollector($sessionManager, $sessionHiddens)); + } catch (Exception $e) { + $this->addCollectorException('Cannot add SessionCollector', $e); } } } else { $sessionManager = null; } + $requestHiddens = array_merge( + $app['config']->get('debugbar.options.symfony_request.hiddens', []), + array_map(fn ($key) => 'session_attributes.' . $key, $sessionHiddens) + ); if ($this->shouldCollect('symfony_request', true) && !$this->hasCollector('request')) { try { - $this->addCollector(new SymfonyRequestCollector($request, $response, $sessionManager)); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add SymfonyRequestCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + $reqId = $this->getCurrentRequestId(); + $this->addCollector(new RequestCollector($request, $response, $sessionManager, $reqId, $requestHiddens)); + } catch (Exception $e) { + $this->addCollectorException('Cannot add SymfonyRequestCollector', $e); } } if ($app['config']->get('debugbar.clockwork') && ! $this->hasCollector('clockwork')) { - try { - $this->addCollector(new ClockworkCollector($request, $response, $sessionManager)); - } catch (\Exception $e) { - $this->addException( - new Exception( - 'Cannot add ClockworkCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + $this->addCollector(new ClockworkCollector($request, $response, $sessionManager, $requestHiddens)); + } catch (Exception $e) { + $this->addCollectorException('Cannot add ClockworkCollector', $e); } $this->addClockworkHeaders($response); } + try { + if ($this->hasCollector('views') && $response->headers->has('X-Inertia')) { + $content = $response->getContent(); + + if (is_string($content)) { + $content = json_decode($content, true); + } + + if (is_array($content)) { + $this['views']->addInertiaAjaxView($content); + } + } + } catch (Exception $e) { + } + + if ($app['config']->get('debugbar.add_ajax_timing', false)) { + $this->addServerTimingHeaders($response); + } + if ($response->isRedirection()) { try { $this->stackData(); - } catch (\Exception $e) { - $app['log']->error('Debugbar exception: ' . $e->getMessage()); - } - } elseif ( - $this->isJsonRequest($request) && - $app['config']->get('debugbar.capture_ajax', true) - ) { - try { - $this->sendDataInHeaders(true); - } catch (\Exception $e) { + } catch (Exception $e) { $app['log']->error('Debugbar exception: ' . $e->getMessage()); } - } elseif ( - ($response->headers->has('Content-Type') && - strpos($response->headers->get('Content-Type'), 'html') === false) - || $request->getRequestFormat() !== 'html' + + return $response; + } + + try { + // Collect + store data, only inject the ID in theheaders + $this->sendDataInHeaders(true); + } catch (Exception $e) { + $app['log']->error('Debugbar exception: ' . $e->getMessage()); + } + + // Check if it's safe to inject the Debugbar + if ( + $app['config']->get('debugbar.inject', true) + && str_contains($response->headers->get('Content-Type', 'text/html'), 'html') + && !$this->isJsonRequest($request, $response) + && $response->getContent() !== false + && in_array($request->getRequestFormat(), [null, 'html'], true) ) { - try { - // Just collect + store data, don't inject it. - $this->collect(); - } catch (\Exception $e) { - $app['log']->error('Debugbar exception: ' . $e->getMessage()); - } - } elseif ($app['config']->get('debugbar.inject', true)) { try { $this->injectDebugbar($response); - } catch (\Exception $e) { + } catch (Exception $e) { $app['log']->error('Debugbar exception: ' . $e->getMessage()); } } @@ -598,7 +924,15 @@ public function modifyResponse(Request $request, Response $response) public function isEnabled() { if ($this->enabled === null) { - $this->enabled = value($this->app['config']->get('debugbar.enabled')); + /** @var \Illuminate\Config\Repository $config */ + $config = $this->app['config']; + $configEnabled = value($config->get('debugbar.enabled')); + + if ($configEnabled === null) { + $configEnabled = $config->get('app.debug'); + } + + $this->enabled = $configEnabled && !$this->app->runningInConsole() && !$this->app->environment('testing'); } return $this->enabled; @@ -611,23 +945,38 @@ public function isEnabled() */ protected function isDebugbarRequest() { - return $this->app['request']->segment(1) == '_debugbar'; + return $this->app['request']->is($this->app['config']->get('debugbar.route_prefix') . '*'); } /** * @param \Symfony\Component\HttpFoundation\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response * @return bool */ - protected function isJsonRequest(Request $request) + protected function isJsonRequest(Request $request, Response $response) { - // If XmlHttpRequest, return true - if ($request->isXmlHttpRequest()) { + // If XmlHttpRequest, Live or HTMX, return true + if ( + $request->isXmlHttpRequest() || + $request->headers->has('X-Livewire') || + ($request->headers->has('Hx-Request') && $request->headers->has('Hx-Target')) + ) { return true; } // Check if the request wants Json $acceptable = $request->getAcceptableContentTypes(); - return (isset($acceptable[0]) && $acceptable[0] == 'application/json'); + if (isset($acceptable[0]) && in_array($acceptable[0], ['application/json', 'application/javascript'], true)) { + return true; + } + + // Check if content looks like JSON without actually validating + $content = $response->getContent(); + if (is_string($content) && strlen($content) > 0 && in_array($content[0], ['{', '['], true)) { + return true; + } + + return false; } /** @@ -640,16 +989,16 @@ public function collect() /** @var Request $request */ $request = $this->app['request']; - $this->data = array( - '__meta' => array( + $this->data = [ + '__meta' => [ 'id' => $this->getCurrentRequestId(), 'datetime' => date('Y-m-d H:i:s'), 'utime' => microtime(true), 'method' => $request->getMethod(), 'uri' => $request->getRequestUri(), 'ip' => $request->getClientIp() - ) - ); + ] + ]; foreach ($this->collectors as $name => $collector) { $this->data[$name] = $collector->collect(); @@ -680,24 +1029,78 @@ function (&$item) { */ public function injectDebugbar(Response $response) { + /** @var \Illuminate\Config\Repository $config */ + $config = $this->app['config']; $content = $response->getContent(); $renderer = $this->getJavascriptRenderer(); + $autoShow = $config->get('debugbar.ajax_handler_auto_show', true); + $renderer->setAjaxHandlerAutoShow($autoShow); + + $enableTab = $config->get('debugbar.ajax_handler_enable_tab', true); + $renderer->setAjaxHandlerEnableTab($enableTab); + if ($this->getStorage()) { $openHandlerUrl = route('debugbar.openhandler'); $renderer->setOpenHandlerUrl($openHandlerUrl); } - $renderedContent = $renderer->renderHead() . $renderer->render(); + $head = $renderer->renderHead(); + $widget = $renderer->render(); + // Try to put the js/css directly before the + $pos = stripos($content, ''); + if (false !== $pos) { + $content = substr($content, 0, $pos) . $head . substr($content, $pos); + } else { + // Append the head before the widget + $widget = $head . $widget; + } + + // Try to put the widget at the end, directly before the $pos = strripos($content, ''); if (false !== $pos) { - $content = substr($content, 0, $pos) . $renderedContent . substr($content, $pos); + $content = substr($content, 0, $pos) . $widget . substr($content, $pos); } else { - $content = $content . $renderedContent; + $content = $content . $widget; } + $original = null; + if ($response instanceof \Illuminate\Http\Response && $response->getOriginalContent()) { + $original = $response->getOriginalContent(); + } + + // Update the new content and reset the content length $response->setContent($content); + $response->headers->remove('Content-Length'); + + // Restore original response (e.g. the View or Ajax data) + if ($original) { + $response->original = $original; + } + } + + /** + * Checks if there is stacked data in the session + * + * @return boolean + */ + public function hasStackedData() + { + return count($this->getStackedData(false)) > 0; + } + + /** + * Returns the data stacked in the session + * + * @param boolean $delete Whether to delete the data in the session + * @return array + */ + public function getStackedData($delete = true): array + { + $this->stackedData = array_merge($this->stackedData, parent::getStackedData($delete)); + + return $this->stackedData; } /** @@ -714,13 +1117,16 @@ public function disable() * @param string $label * @param float $start * @param float $end + * @param array|null $params + * @param string|null $collector + * @param string|null $group */ - public function addMeasure($label, $start, $end) + public function addMeasure($label, $start, $end, $params = [], $collector = null, $group = null) { if ($this->hasCollector('time')) { - /** @var \DebugBar\DataCollector\TimeDataCollector $collector */ - $collector = $this->getCollector('time'); - $collector->addMeasure($label, $start, $end); + /** @var \DebugBar\DataCollector\TimeDataCollector */ + $time = $this->getCollector('time'); + $time->addMeasure($label, $start, $end, $params, $collector, $group); } } @@ -729,16 +1135,20 @@ public function addMeasure($label, $start, $end) * * @param string $label * @param \Closure $closure + * @param string|null $collector + * @param string|null $group + * @return mixed */ - public function measure($label, \Closure $closure) + public function measure($label, \Closure $closure, $collector = null, $group = null) { if ($this->hasCollector('time')) { - /** @var \DebugBar\DataCollector\TimeDataCollector $collector */ - $collector = $this->getCollector('time'); - $collector->measure($label, $closure); + /** @var \DebugBar\DataCollector\TimeDataCollector */ + $time = $this->getCollector('time'); + $result = $time->measure($label, $closure, $collector, $group); } else { - $closure(); + $result = $closure(); } + return $result; } /** @@ -752,16 +1162,16 @@ public function collectConsole() return; } - $this->data = array( - '__meta' => array( + $this->data = [ + '__meta' => [ 'id' => $this->getCurrentRequestId(), 'datetime' => date('Y-m-d H:i:s'), 'utime' => microtime(true), 'method' => 'CLI', 'uri' => isset($_SERVER['argv']) ? implode(' ', $_SERVER['argv']) : null, 'ip' => isset($_SERVER['SSH_CLIENT']) ? $_SERVER['SSH_CLIENT'] : null - ) - ); + ] + ]; foreach ($this->collectors as $name => $collector) { $this->data[$name] = $collector->collect(); @@ -793,9 +1203,9 @@ function (&$item) { */ public function __call($method, $args) { - $messageLevels = array('emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug', 'log'); + $messageLevels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug', 'log']; if (in_array($method, $messageLevels)) { - foreach($args as $arg) { + foreach ($args as $arg) { $this->addMessage($arg, $method); } } @@ -840,6 +1250,7 @@ protected function isLumen() */ protected function selectStorage(DebugBar $debugbar) { + /** @var \Illuminate\Config\Repository $config */ $config = $this->app['config']; if ($config->get('debugbar.storage.enabled')) { $driver = $config->get('debugbar.storage.driver', 'file'); @@ -853,12 +1264,21 @@ protected function selectStorage(DebugBar $debugbar) break; case 'redis': $connection = $config->get('debugbar.storage.connection'); - $storage = new RedisStorage($this->app['redis']->connection($connection)); + $client = $this->app['redis']->connection($connection); + if (is_a($client, 'Illuminate\Redis\Connections\Connection', false)) { + $client = $client->client(); + } + $storage = new RedisStorage($client); break; case 'custom': $class = $config->get('debugbar.storage.provider'); $storage = $this->app->make($class); break; + case 'socket': + $hostname = $config->get('debugbar.storage.hostname', '127.0.0.1'); + $port = $config->get('debugbar.storage.port', 2304); + $storage = new SocketStorage($hostname, $port); + break; case 'file': default: $path = $config->get('debugbar.storage.path'); @@ -874,7 +1294,53 @@ protected function addClockworkHeaders(Response $response) { $prefix = $this->app['config']->get('debugbar.route_prefix'); $response->headers->set('X-Clockwork-Id', $this->getCurrentRequestId(), true); - $response->headers->set('X-Clockwork-Version', 1, true); - $response->headers->set('X-Clockwork-Path', $prefix .'/clockwork/', true); + $response->headers->set('X-Clockwork-Version', 9, true); + $response->headers->set('X-Clockwork-Path', $prefix . '/clockwork/', true); + } + + /** + * Add Server-Timing headers for the TimeData collector + * + * @see https://www.w3.org/TR/server-timing/ + * @param Response $response + */ + protected function addServerTimingHeaders(Response $response) + { + if ($this->hasCollector('time')) { + $collector = $this->getCollector('time'); + + $headers = []; + foreach ($collector->collect()['measures'] as $m) { + $headers[] = sprintf('app;desc="%s";dur=%F', str_replace(array("\n", "\r"), ' ', str_replace('"', "'", $m['label'])), $m['duration'] * 1000); + } + + $response->headers->set('Server-Timing', $headers, false); + } + } + + /** + * @return array + */ + private function getRemoteServerReplacements() + { + $localPath = $this->app['config']->get('debugbar.local_sites_path') ?: base_path(); + $remotePaths = array_filter(explode(',', $this->app['config']->get('debugbar.remote_sites_path') ?: '')) ?: [base_path()]; + + return array_fill_keys($remotePaths, $localPath); + } + + /** + * @return \Monolog\Logger + * @throws Exception + */ + private function getMonologLogger() + { + $logger = $this->app['log']->getLogger(); + + if (get_class($logger) !== 'Monolog\Logger') { + throw new Exception('Logger is not a Monolog\Logger instance'); + } + + return $logger; } } diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php index 9eb2dca3e..0afa1294d 100644 --- a/src/LumenServiceProvider.php +++ b/src/LumenServiceProvider.php @@ -1,12 +1,41 @@ -app->call( + function () { + $debugBar = $this->app->get(LaravelDebugbar::class); + if ($debugBar->shouldCollect('time', true)) { + $startTime = $this->app['request']->server('REQUEST_TIME_FLOAT'); + + if (!$debugBar->hasCollector('time')) { + $debugBar->addCollector(new TimeDataCollector($startTime)); + } + + if ($this->app['config']->get('debugbar.options.time.memory_usage')) { + $debugBar['time']->showMemoryUsage(); + } + + if ($startTime) { + $debugBar->addMeasure('Booting', $startTime, microtime(true), [], 'time'); + } + } + } + ); + } + /** * Get the active router. * @@ -14,7 +43,7 @@ class LumenServiceProvider extends ServiceProvider */ protected function getRouter() { - return $this->app; + return $this->app->router; } /** @@ -37,14 +66,6 @@ protected function registerMiddleware($middleware) $this->app->middleware([$middleware]); } - /** - * Check the App Debug status - */ - protected function checkAppDebug() - { - return env('APP_DEBUG'); - } - /** * Get the services provided by the provider. * @@ -52,6 +73,6 @@ protected function checkAppDebug() */ public function provides() { - return array('debugbar', 'command.debugbar.clear'); + return ['debugbar', 'command.debugbar.clear']; } } diff --git a/src/Middleware/DebugbarEnabled.php b/src/Middleware/DebugbarEnabled.php new file mode 100644 index 000000000..146ce2d18 --- /dev/null +++ b/src/Middleware/DebugbarEnabled.php @@ -0,0 +1,43 @@ +debugbar = $debugbar; + } + + /** + * Handle an incoming request. + * + * @param Request $request + * @param Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if (!$this->debugbar->isEnabled()) { + abort(404); + } + + return $next($request); + } +} diff --git a/src/Middleware/Debugbar.php b/src/Middleware/InjectDebugbar.php similarity index 63% rename from src/Middleware/Debugbar.php rename to src/Middleware/InjectDebugbar.php index 6386068e6..48be02d8a 100644 --- a/src/Middleware/Debugbar.php +++ b/src/Middleware/InjectDebugbar.php @@ -1,4 +1,6 @@ -container = $container; $this->debugbar = $debugbar; + $this->except = config('debugbar.except') ?: []; } /** @@ -44,10 +55,16 @@ public function __construct(Container $container, LaravelDebugbar $debugbar) */ public function handle($request, Closure $next) { + if (!$this->debugbar->isEnabled() || $this->inExceptArray($request)) { + return $next($request); + } + + $this->debugbar->boot(); + try { /** @var \Illuminate\Http\Response $response */ $response = $next($request); - } catch (Exception $e) { + } catch (Throwable $e) { $response = $this->handleException($request, $e); } @@ -55,7 +72,6 @@ public function handle($request, Closure $next) $this->debugbar->modifyResponse($request, $response); return $response; - } /** @@ -64,11 +80,11 @@ public function handle($request, Closure $next) * (Copy from Illuminate\Routing\Pipeline by Taylor Otwell) * * @param $passable - * @param Exception $e + * @param Throwable $e * @return mixed * @throws Exception */ - protected function handleException($passable, Exception $e) + protected function handleException($passable, $e) { if (! $this->container->bound(ExceptionHandler::class) || ! $passable instanceof Request) { throw $e; @@ -80,4 +96,25 @@ protected function handleException($passable, Exception $e) return $handler->render($passable, $e); } + + /** + * Determine if the request has a URI that should be ignored. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function inExceptArray($request) + { + foreach ($this->except as $except) { + if ($except !== '/') { + $except = trim($except, '/'); + } + + if ($request->is($except)) { + return true; + } + } + + return false; + } } diff --git a/src/Resources/cache/widget.js b/src/Resources/cache/widget.js new file mode 100644 index 000000000..5635363e6 --- /dev/null +++ b/src/Resources/cache/widget.js @@ -0,0 +1,62 @@ +(function ($) { + + var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); + + /** + * Widget for the displaying cache events + * + * Options: + * - data + */ + var LaravelCacheWidget = PhpDebugBar.Widgets.LaravelCacheWidget = PhpDebugBar.Widgets.TimelineWidget.extend({ + + tagName: 'ul', + + className: csscls('timeline cache'), + + onForgetClick: function (e, el) { + e.stopPropagation(); + + $.ajax({ + url: $(el).attr("data-url"), + type: 'DELETE', + success: function (result) { + $(el).fadeOut(200); + } + }); + }, + + render: function () { + LaravelCacheWidget.__super__.render.apply(this); + + this.bindAttr('data', function (data) { + + if (data.measures) { + var self = this; + var lines = this.$el.find('.' + csscls('measure')); + + for (var i = 0; i < data.measures.length; i++) { + var measure = data.measures[i]; + var m = lines[i]; + + if (measure.params && !$.isEmptyObject(measure.params)) { + if (measure.params.delete) { + $(m).next().find('td.phpdebugbar-widgets-name:contains(delete)').closest('tr').remove(); + } + if (measure.params.delete && measure.params.key) { + $('') + .addClass(csscls('forget')) + .text('forget') + .attr('data-url', measure.params.delete) + .one('click', function (e) { + self.onForgetClick(e, this); }) + .appendTo(m); + } + } + } + } + }); + } + }); + +})(PhpDebugBar.$); diff --git a/src/Resources/laravel-debugbar.css b/src/Resources/laravel-debugbar.css index f13586f22..4a33ba8a8 100644 --- a/src/Resources/laravel-debugbar.css +++ b/src/Resources/laravel-debugbar.css @@ -1,54 +1,187 @@ +div.phpdebugbar, +div.phpdebugbar-openhandler { + --debugbar-red-vivid: #eb4432; + + /*--debugbar-background: #fff;*/ + --debugbar-background-alt: #f5f5f5; + --debugbar-text: #222; + /*--debugbar-text-muted: #888;*/ + --debugbar-border: #bbb; + + --debugbar-header: #fff; + --debugbar-header-text: #555; + /*--debugbar-header-border: #ddd;*/ + + /*--debugbar-active: #ccc;*/ + /*--debugbar-active-text: #666;*/ + + --debugbar-icons: var(--debugbar-header-text); + --debugbar-badge: #fff; + --debugbar-badge-text: var(--debugbar-red-vivid); + + --debugbar-badge-active: var(--debugbar-red-vivid); + --debugbar-badge-active-text: #fff; + + --debugbar-link: #777; + --debugbar-hover: #666; + + --debugbar-header-hover: #ebebeb; +} + +/* Dark mode */ +div.phpdebugbar[data-theme='dark'], +div.phpdebugbar-openhandler[data-theme='dark'] { + --debugbar-white: #FFFFFF; + --debugbar-gray-100: #F7FAFC; + --debugbar-gray-200: #EDF2F7; + --debugbar-gray-300: #E2E8F0; + --debugbar-gray-400: #CBD5E0; + --debugbar-gray-500: #A0AEC0; + --debugbar-gray-600: #718096; + --debugbar-gray-700: #4A5568; + --debugbar-gray-800: #252a37; + --debugbar-gray-900: #18181b; + --debugbar-red-vivid: #eb4432; + + --debugbar-background: var(--debugbar-gray-800); + --debugbar-background-alt: var(--debugbar-gray-900); + --debugbar-text: var(--debugbar-gray-100); + --debugbar-text-muted: var(--debugbar-gray-600); + --debugbar-border: var(--debugbar-gray-600); + + --debugbar-header:var(--debugbar-gray-900); + --debugbar-header-text: var(--debugbar-gray-200); + --debugbar-header-border: var(--debugbar-gray-800); + --debugbar-header-hover: var(--debugbar-gray-700); + + --debugbar-active: var(--debugbar-gray-800); + --debugbar-active-text: var(--debugbar-gray-100); + + --debugbar-icons: var(--debugbar-header-text); + --debugbar-badge: var(--debugbar-white); + --debugbar-badge-text: var(--debugbar-red-vivid); + + --debugbar-badge-active: var(--debugbar-red-vivid); + --debugbar-badge-active-text: var(--debugbar-white); + + --debugbar-link: var(--debugbar-gray-300); + --debugbar-hover: var(--debugbar-gray-100); + --debugbar-hover-bg: var(--debugbar-gray-700); +} + +div.phpdebugbar[data-theme='dark'] code.phpdebugbar-widgets-sql, +div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-name, +div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-key, +div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-success > pre.sf-dump > .sf-dump-note { + color: #fdfd96; +} + +/* Force Laravel Whoops exception handler to be displayed under the debug bar */ +.Whoops.container { + z-index: 5999999; +} + div.phpdebugbar { font-size: 13px; - font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; + z-index: 6000000; +} + +div.phpdebugbar-openhandler-overlay { + z-index: 6000001; + cursor: pointer; +} + +div.phpdebugbar-openhandler { + border: 1px solid var(--debugbar-border); + border-top: 3px solid var(--debugbar-red-vivid); + border-radius: 5px; + overflow-y: scroll; + z-index: 6000002; + cursor: default; +} + + +div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form { + margin: 15px 0px 5px; + font-size: 13px; + font-weight: bold; +} + +div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions button, +div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions select, +div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions input { + height: 24px; } div.phpdebugbar-resize-handle { - border-bottom-color: #ddd; + display: block!important; + height: 3px; + margin-top: -3px; + width: 100%; + background: none; + cursor: ns-resize; + border-top: none; + border-bottom: 0px; + background-color: var(--debugbar-red-vivid); +} + +.phpdebugbar.phpdebugbar-minimized div.phpdebugbar-resize-handle { + cursor: default!important; } div.phpdebugbar-closed, div.phpdebugbar-minimized { - border-top-color: #ddd; + border-top-color: var(--debugbar-border); } -a.phpdebugbar-restore-btn { - border-right-color: #ddd !important; +div.phpdebugbar .hljs { + padding: 0; +} + +div.phpdebugbar .phpdebugbar-widgets-messages .hljs > code { + padding-bottom: 3px; } div.phpdebugbar code, div.phpdebugbar pre { - background: none; - font-family: monospace; - font-size: 1em; - border: 0; - padding: 0; + color: var(--debugbar-text); } -div.phpdebugbar-body { - border-top: none; +div.phpdebugbar-widgets-exceptions .phpdebugbar-widgets-filename { + margin-top: 4px; } -div.phpdebugbar-header { - min-height: 30px; - line-height: 20px; - padding-left: 39px; +div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item pre.phpdebugbar-widgets-file[style="display: block;"] ~ div { + display: block; } -div.phpdebugbar-header, -a.phpdebugbar-restore-btn, -div.phpdebugbar-openhandler .phpdebugbar-openhandler-header { - background: #f5f5f5 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIcAAACHCAYAAAA850oKAAAMfElEQVR42u2dC5RVZRXH7zwQGRTEqUEDxMdEQSggWAYjomlKGOAyylJBs5AhYVgIKYQ5hrgIfPVQwXgYQ4lppTApqxdiZGqAYy8cHIyKfCGi9lCPqO3N3VOX6d57vn3u9zrn7LvWfy3E4dzvfPs35+xvf3vvL5NhfoLxw4eC5oKaQW2gN0HvirxTANpBdpqHdsuY+MCFu4AaQNtl0mOtp0EzQFW6wJgEek4mNlFCe14MKosKxeGgn8hEJlpo32ouGLWgZ2TyUiG0cy0HjOdl0lL3mumn8iqRJ0Z6nyDVxeBYJ5OUaj2Q10mFv5wok5NKvQ36d85/T8wXx3hWJirRQgC2glaBrgKNJf+yghg4GnQdvV4OyYVjukxeoiE4FlSuuCDpDxqT+xcS+UwZBJy9Eplsf/VP4xAUgWOOGMAbCH4LuhM0Gx/t5AeUZVx94MtXimFSDkEROAaA3hKjGYVhOWgC6BgvIQgBZLEY0YgeBr3XIzt3B50KmgZaBtpM+TiohYUCYN1kW96IBjiCoJxiGOeB5oPuB+1UGO/VhS54gRhTuwZaAAF/sUeA6kG3g35Dr7Ko0dKz8n1JGWijGFSrlmqEoBL0IdD5oAWgtYpPA672gPrmG8BA0D4xqja9AzotAgi9QaNBXwY1gVooJ9TWuHE11TnfwG4So2rV30A9FID4OOgh0F5Pxr1EnFM7WqMAR71v4y400AvFoNp1UQgcnUBb4gAHOqebxKBa9RpGQkMAeX8JKw07cNBAB4lzql2b2vMnisz7RO/hoIF+UwyqXV9R8D+a4gAHhltfEINqFe5jnRQy74dSqam/cEhuqTFhclXXkHkfZjm2EQkOcU4dRU/hZ2Z5DQcNcjDF3cWoejVW4Rdzvddw0EBvFWNq127QESHzXuOqAhG/fLgiHD1AL4pBtevBsAQgCq2/4wKOzYy09c+LMY3ocoW5/7oLOPAPlyrCUUb5AmJQvXo9LDGIwuuPuYADYxndFAEZIs6pET2Rd5v8wLk/jsLwVuFALRbn1LkWKcz9Z13AEYT2aDiwVcNLYkw3yUHwMytsw4FqZjw9vijGdJMchIXOoFbbcKDOZmQ4PybGdJYcNCQw3OYzHxzb0DNWBGSYi/W3JAfZ6Y6QKfA/ZjBeL0vEkM6SgzC00GwbjldUK7XEOTWqXyskB73HVOOdjI6aC/jZyWJIY5qnMP+nm4g9ZUKqnwYznNPNYkhjyUEfVrDBAptw7C8EZjw9PiLOqdHkoEMUKuIesQkH6jMMQL4jhjSmOxTm/2jyF63B8RdQF0U40Dl6WQxpTOMUbDDBJhyoaxhPjyliRKPJQUcq2OAOm3Bg57o+4pzGJjmoCvQnW3Cg7mI8PU4WIxrVNAUbnAB6wxYcqDoGICvEiMb0hkrXIPiZqczrYq3MMqqV7sOFYysjpbDGo3YCSVRLWHIQ2eG+Itd4KgeG3vn+MXdQX2A8Pb4kRjSqxYrbG7tyYFhC3YGOUDEgd0CYUthdEY4KSn8TQ5pLDjpdwQ7HKsGgAQ7UjYzrf1SMaFS7VDoHRW1EFmVAmFL4AXFOncc8fgyamddfcAgH6gHGd/QEvSoGLUkYqV5NQcb+gY1OyCUOeDTje+RMF57QeVxKK4m+rloel3IDrYyUQtw1fFKMXjA9AqPKN4PODRy3xKYVzqcyGm5sJuNL6wSE/waxsCEwHpt1dth2vAUYDgqyPdExJ+Tx9sQhHXCgL1HDGMiqFMLwKrVSmEO/IJ0zjj8whoNB44PsqdT/0hE+j5xrkDLnFGNB94IaqISgwgMYutJ+CwJxBagRdEOx7kEZjcGYIYyBNiQMhj+DvovRY9AHPTk+o5KCX2fSCqexgB4yDcf+lELV5RUN/A8xhgHHfjvVrfb2BIZy6sA0i8oVcF/rr0H2LJ3GEO0yDQc3pfDUmICwjyr78BE8Dj35jCcfrG0Osm2x8RW2p8D48WSFa0PgmE9lmEbhQFKrGDf3PQ9hwMSmDTShZwQhXf8sw4CnKUyiV9guxj1tUHh6fIP6hBiDA9XIuNkjbfabKKC99Bi+kvaBDvIIhmqMN9ArbHuJPuEKBUB+YBoOpO8oxgTMtAwDnghxN7ZaCrItvMs9ggGr5z9Br7AWzaUeL1PrqI5A4C/F57A3HD2ZbjMJB+pupldt0jnF7KaVoEtAtRmPPhR8GgX6WpDt9Wr6lM4nyGH9NBZK4TZ+x0UExT9aTMKBGsmYpFEal9QYov82OseRchjMwlBBnQnwZOmfkX9j+zVarzBOPL3hHxnDlJYzJm5NxNQBbGCHx16eYyyvoTQg8Fy2aZSut9eT0P3xCuM+P2N4IJMZk9gLaQ25Hp5F8nOso8H2SJyVkUUYsOrsUtpef97T5fkflQrVDA/iRdWUQhrL7DxO1P30nsRa3EoPYaihnExM1N0Ro0DeUtdwoG5iTHQn8pYx3Dsw8PBo7yB7zMhY0C2g38c87H+eazj2qSTB+voh7x2DYdeDHg2S1YN1b9Gwg6VBPBojGCopGDYP9MtSq8Zi0j2o0gUcL1HyCLZm6OcpDGUUDJtBkdLXEg5DPs03DQc+bv9OS0uMQC7qEImb4osPgcEw0GU0zt2SmbbfdqN0wvE65QK0p7pdqRC7P8ERDO+jRN2VlMUtqYr561+qo8LxCqWUYXRvRMcNKsoyCoNjuo2sKEqQPZcipdvE8Mpae8DTXdcGFb27JysAcpIBGKro6bWIsrilN5mO9g45f/k0aDno4qgbVBQdDINjVqnb4rRhVUfXezhwfIpiAjPjB7VP9ASdG1S0/RsGyCkRUuBOJLDWF8qWFmnTNiNJTtQ07qshcFyl2oSOrtmjSBqcyIyWmXIIxyg8Pc5kXrNeDGZXJjOa5obAgUvgnszI5e/EaDGHg4w5sgMMC6jabRMFy3BFsYZ5zdPEaMmAowvoW7Q/sZM24PJlbg1lXvdeMVzM4KA4x2BKGG6mxByltHnm9xzTMYVe5CEctEcxhVLaS9mj+CTze68T43kGR84exZ1UwKSzUUklYxxdyW8RI7qCw/IexdQIwTYxoi04aI/irJw9CptZT5hv2o3p4zwiRjQIB+1RXEOdZlzvUVzPfHrI6ZSG4fBpQLgK6cMERNpYpgQOVBMTjp4pTe1LJRz4mhjGBGS2GDMdcKA2Rsjv2C4GTQccqHFMQM4Rg6YHjlZu+SMlAolhUwAH6nImHP0t9LgQODzRbk4hNgFysxg2HXCgFjLhOIyirWLgFMCBgbG+TEAuE+OmAw7U6gjZ6lvFwOmA490IgbERYuDIQqf+wTjBsTFCMtL3xdCsPirrqWXV4Tb7c+jSeCYcvaQAKrS6/heUzVfjqnmLLmGIvBMTkLkCwf/tXf0qyHY47Om67ZO5Ql/1tk3PCBT7W1Y1YKonZ/LidpPYLegwJiDjUwoEdlW6ImC0G487HKhFEe7zpykBYgvVIh+no94kjhPwJtavMO9zQIHCqiQIW15eHTAOgk4yHKh7ItzrLQkCAss5sKG+uWPDYj5BdSnbd8EOyXiq0vEZG5+Yw7E5YHYoDLKH9MUNiIXcmmKBI6uJEfZdtnh+T1hNeCPo5MBle87AzZkfOoVlkVXMex7u4X08S10JhgeO+7VSpeMl+Ie2BDw9GiNMgA8nY6P/gwcBnBI4Pk6MgoXYH+4+Km7bjn+5LgFw4P5Jrwj1Li5OxsZjQvD4jY8Fjk+qplcsNsRZnmcu1mYoYJKEpd2qCJMz3dLYcOKbQKO5e0OGoBhE9dDFjh+dgz84NEEbStycD+wz9qSh8WDzmjXUpaCzB0D0CbItyFX7qg1u/4dPJQSQTREmrU5zSuMPg+zJTVUeANGdlu4bmAXnrbkXaUhQ5HCCZef0LfLbLuJmyhsCojNtNN5D2wyl7XwH2eZuzyUEDtyeP9iwc/o2beT9L2vKLRBltOJZSg6v3tAAtXNKytNjboQJnqqYJJM/a8oNFLiZiMeL7TQaVCT61iUEDlza9o7w29dUIElmBitJxiwQ2JdtpqHs+uawyNiOhAByV4SJryD/60e0xD/KEyAOBU0KsqdYm2rF1Rb6igyy7SNfSAggIzMx/QTZY1TH0HLYdN9VjHfUqg6sNiFPkBbXUcgIrzY8mfLWwN7Zc20B93wdesUkwQepjwEU/UDXOtjnQvtWl0LyhTFf5u7xYbmZZ25rKHz/uKMd4Em6bqQLOWpxjaTe5gkQ2Hn5Aiw1dJTP2kr1Kl1N3eBQ8uTX0WMwDvkg6OEPcgQEroDwcMLVgfphAbpC+W1kJ7TXidyx/wdIVCWo/YcgUwAAAABJRU5ErkJggg==) no-repeat 5px 3px; +div.phpdebugbar pre.sf-dump { + color: var(--debugbar-text); + outline: none; + padding-left: 0px; +} + +div.phpdebugbar-body { + border-top: 1px solid var(--debugbar-header-border); } -a.phpdebugbar-close-btn { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuM4zml1AAAADDSURBVDhPxZCxCsIwFEUzuGdwCvQD7BIIcehUXDqVfGM/wsG/iG4ifkzMlRuSPLo4eeFBue8c6Iv6b4wxW557Hs0KnWa3seqDxTiOyVqbhmF4UND4Rofdruyce3rvE6bIRSo9GOI1McbLPM/vVm4l7MAQr0kpHaQsJTDE+6zrepym6SVFdNgR69M+hBTLzWCI10gJvydvBkO8ZlmWayvhJnkzGOI1+fBTCOHWPkT7YNiBId4HizxnCKy+r81uX/otSn0A7dioI/vYX+8AAAAASUVORK5CYII=) no-repeat 9px 6px; - color : #555; +div.phpdebugbar-header span.phpdebugbar-text, div.phpdebugbar-header > div > span > span, div.phpdebugbar-header > div > span > i{ + display: inline-block; } -a.phpdebugbar-open-btn { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAOCAYAAADJ7fe0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElNRQfdCgYULwwNKp3GAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4wLjOM5pdQAAAA1UlEQVQ4T2OgKpCUlOQH4vdA/B8Jv4dKEwYgDdLS0v8NDQ3/GxsbwzGIj2YoGEO1oQJkjcRgqDZUAJKwsrJ6/v//fwdiMFQbKgAZkpGR0QR0ajy60wlgRJhBXSGhpqb2CNnZhHBkZORcqBEMDFBX2BsYGGBVjAv39vZaQ41gYIC6Ygs2hbiwr6/vdqA+DqgR4CiW19bWxqoYF87Ly4uFaocAZWXlydgU4sJ2dna3ga4QgGqHAC0trY/YFOPCKSkpDVCtCAA01QaIsaYJHFgCqpVagIEBACGlF2c3r4ViAAAAAElFTkSuQmCC) no-repeat 8px 6px; +a.phpdebugbar-restore-btn:after { + background: url(data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2048%2048%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M47.973%2010.859a.74.74%200%200%201%20.027.196v10.303a.735.735%200%200%201-.104.377.763.763%200%200%201-.285.275l-8.902%204.979v9.868a.75.75%200%200%201-.387.652L19.74%2047.9c-.043.023-.09.038-.135.054-.018.006-.034.016-.053.021a.801.801%200%200%201-.396%200c-.021-.006-.04-.017-.061-.024-.043-.015-.087-.029-.128-.051L.39%2037.509a.763.763%200%200%201-.285-.276.736.736%200%200%201-.104-.376V5.947c0-.067.01-.133.027-.196.006-.022.02-.042.027-.063.015-.04.028-.08.05-.117.014-.024.035-.044.053-.067.022-.03.042-.06.068-.087.022-.021.051-.037.077-.056.028-.023.053-.047.085-.065L9.677.1a.793.793%200%200%201%20.774%200l9.29%205.196h.002c.03.019.057.042.085.064.025.019.053.036.075.056.027.027.047.058.07.088.016.023.038.043.052.067.022.038.035.077.05.117.008.021.021.04.027.063a.74.74%200%200%201%20.027.197v19.305l7.742-4.33v-9.869c0-.066.01-.132.027-.195.006-.023.019-.042.027-.064.015-.04.029-.08.05-.116.014-.025.036-.045.052-.067.023-.03.043-.061.07-.087.022-.022.05-.038.075-.057.03-.022.054-.047.085-.065l9.292-5.195a.792.792%200%200%201%20.773%200l9.29%205.195c.033.02.058.043.087.064.025.02.053.036.075.057.027.027.046.058.07.088.017.022.038.042.052.067.022.036.034.077.05.116.009.022.021.041.027.064ZM46.45%2020.923v-8.567l-3.25%201.818-4.492%202.512v8.567l7.743-4.33Zm-9.29%2015.5v-8.574l-4.417%202.45-12.616%206.995v8.654l17.033-9.526ZM1.55%207.247v29.174l17.03%209.525v-8.653l-8.897-4.89-.003-.003-.003-.002c-.03-.017-.056-.041-.084-.062-.024-.018-.052-.033-.073-.054l-.002-.003c-.025-.023-.042-.053-.064-.079-.02-.025-.042-.047-.058-.073v-.003c-.018-.028-.029-.062-.041-.094-.013-.028-.03-.054-.037-.084-.01-.036-.012-.075-.016-.111-.003-.028-.011-.056-.011-.085V11.58L4.8%209.064%201.549%207.248Zm8.516-5.628-7.74%204.328%207.738%204.328%207.74-4.33-7.74-4.326h.002Zm4.026%2027.01%204.49-2.51V7.247L15.33%209.066l-4.492%202.512V30.45l3.253-1.819ZM37.935%206.727l-7.74%204.328%207.74%204.328%207.738-4.329-7.738-4.327Zm-.775%209.959-4.49-2.512-3.252-1.818v8.567l4.49%202.511%203.252%201.82v-8.568ZM19.353%2035.993l11.352-6.295%205.674-3.146-7.733-4.325-8.904%204.98-8.116%204.538%207.727%204.248Z%22%20fill%3D%22%23FF2D20%22/%3E%3C/svg%3E) no-repeat 11px center / 20px 20px !important; } +div.phpdebugbar-openhandler .phpdebugbar-openhandler-header { + background: url(data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2048%2048%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M47.973%2010.859a.74.74%200%200%201%20.027.196v10.303a.735.735%200%200%201-.104.377.763.763%200%200%201-.285.275l-8.902%204.979v9.868a.75.75%200%200%201-.387.652L19.74%2047.9c-.043.023-.09.038-.135.054-.018.006-.034.016-.053.021a.801.801%200%200%201-.396%200c-.021-.006-.04-.017-.061-.024-.043-.015-.087-.029-.128-.051L.39%2037.509a.763.763%200%200%201-.285-.276.736.736%200%200%201-.104-.376V5.947c0-.067.01-.133.027-.196.006-.022.02-.042.027-.063.015-.04.028-.08.05-.117.014-.024.035-.044.053-.067.022-.03.042-.06.068-.087.022-.021.051-.037.077-.056.028-.023.053-.047.085-.065L9.677.1a.793.793%200%200%201%20.774%200l9.29%205.196h.002c.03.019.057.042.085.064.025.019.053.036.075.056.027.027.047.058.07.088.016.023.038.043.052.067.022.038.035.077.05.117.008.021.021.04.027.063a.74.74%200%200%201%20.027.197v19.305l7.742-4.33v-9.869c0-.066.01-.132.027-.195.006-.023.019-.042.027-.064.015-.04.029-.08.05-.116.014-.025.036-.045.052-.067.023-.03.043-.061.07-.087.022-.022.05-.038.075-.057.03-.022.054-.047.085-.065l9.292-5.195a.792.792%200%200%201%20.773%200l9.29%205.195c.033.02.058.043.087.064.025.02.053.036.075.057.027.027.046.058.07.088.017.022.038.042.052.067.022.036.034.077.05.116.009.022.021.041.027.064ZM46.45%2020.923v-8.567l-3.25%201.818-4.492%202.512v8.567l7.743-4.33Zm-9.29%2015.5v-8.574l-4.417%202.45-12.616%206.995v8.654l17.033-9.526ZM1.55%207.247v29.174l17.03%209.525v-8.653l-8.897-4.89-.003-.003-.003-.002c-.03-.017-.056-.041-.084-.062-.024-.018-.052-.033-.073-.054l-.002-.003c-.025-.023-.042-.053-.064-.079-.02-.025-.042-.047-.058-.073v-.003c-.018-.028-.029-.062-.041-.094-.013-.028-.03-.054-.037-.084-.01-.036-.012-.075-.016-.111-.003-.028-.011-.056-.011-.085V11.58L4.8%209.064%201.549%207.248Zm8.516-5.628-7.74%204.328%207.738%204.328%207.74-4.33-7.74-4.326h.002Zm4.026%2027.01%204.49-2.51V7.247L15.33%209.066l-4.492%202.512V30.45l3.253-1.819ZM37.935%206.727l-7.74%204.328%207.74%204.328%207.738-4.329-7.738-4.327Zm-.775%209.959-4.49-2.512-3.252-1.818v8.567l4.49%202.511%203.252%201.82v-8.568ZM19.353%2035.993l11.352-6.295%205.674-3.146-7.733-4.325-8.904%204.98-8.116%204.538%207.727%204.248Z%22%20fill%3D%22%23FF2D20%22/%3E%3C/svg%3E) no-repeat 11px center / 20px 20px !important; + padding: 4px 4px 6px 38px; + margin: 0px !important; +} + +div.phpdebugbar-openhandler .phpdebugbar-openhandler-header a { + display: flex; + cursor: pointer; +} div.phpdebugbar-header, div.phpdebugbar-openhandler-header { @@ -57,192 +190,576 @@ div.phpdebugbar-openhandler-header { } a.phpdebugbar-restore-btn { - background-size: 20px; - width: 16px; - border-right-color: #ccc; + border-right-color: var(--debugbar-border) !important; + height: 20px; + width: 24px; + background-position: center; + background-size: 21px; } -div.phpdebugbar-header > div > * { - font-size: 13px; +.phpdebugbar:not(.phpdebugbar-closed) a.phpdebugbar-restore-btn { + border-right: none; } + div.phpdebugbar-header .phpdebugbar-tab { - padding: 5px 6px; + border-left: 1px solid var(--debugbar-header-border); + display: flex; + align-items: center; + justify-content: center; + min-width: 16px; +} + +a.phpdebugbar-tab.phpdebugbar-tab-history { + display: flex; + justify-content: center; + align-items: center; +} + +div.phpdebugbar-header .phpdebugbar-header-left { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } div.phpdebugbar .phpdebugbar-header select { - padding: 1px 0; + margin: 0 4px; + padding: 2px 3px 3px 3px; + border-radius: 3px; + width: auto; + cursor: pointer; } -dl.phpdebugbar-widgets-kvlist dt { - width: 200px; +dl.phpdebugbar-widgets-kvlist dt, +dl.phpdebugbar-widgets-kvlist dd, +table.phpdebugbar-widgets-tablevar td { min-height: 20px; - padding: 7px 5px; line-height: 20px; + padding: 4px 5px 5px; + border-top: 0px; } -dl.phpdebugbar-widgets-kvlist dd { - min-height: 20px; - margin-left: 210px; - padding: 7px 5px; - line-height: 20px; +dl.phpdebugbar-widgets-kvlist dd.phpdebugbar-widgets-value.phpdebugbar-widgets-pretty .phpdebugbar-widgets-code-block { + padding: 0px 0px; + background: transparent; } -ul.phpdebugbar-widgets-timeline .phpdebugbar-widgets-measure { - height: 25px; - line-height: 25px; - border: none; +dl.phpdebugbar-widgets-kvlist dt, +table.phpdebugbar-widgets-tablevar td:first-child { + width: calc(25% - 10px); } -ul.phpdebugbar-widgets-timeline li:nth-child(even) { - background-color: #f9f9f9; +dl.phpdebugbar-widgets-kvlist dd { + margin-left: 25%; } -ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-value { - height: 15px; - background-color: #f4645f; + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-label, +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-collector { + + text-transform: uppercase; + font-style: normal; } -ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label, -ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-collector { - top: 0px; +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-collector { + text-transform: none; } -div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter { - background-color: #f4645f; +.phpdebugbar-widgets-toolbar i.phpdebugbar-fa.phpdebugbar-fa-search { + position: relative; + top: -1px; + padding: 0px 10px; } -a.phpdebugbar-tab:hover, -span.phpdebugbar-indicator:hover, -a.phpdebugbar-indicator:hover, -a.phpdebugbar-close-btn:hover, -a.phpdebugbar-open-btn:hover { - background-color: #ebebeb; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter, +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded { + + display: inline-block; + background-color: #6d6d6d; + margin-left: 3px; + border-radius: 3px; + text-transform: uppercase; + font-size: 10px; transition: background-color .25s linear 0s, color .25s linear 0s; + color: #FFF; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } -a.phpdebugbar-tab.phpdebugbar-active { - background: #f4645f; - color: #fff; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="alert"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="alert"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="info"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="info"] { + background-color: #5896e2; } -a.phpdebugbar-tab.phpdebugbar-active span.phpdebugbar-badge { - background-color: white; - color: #f4645f; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="debug"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="debug"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="success"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="success"] { + background-color: #45ab45; } -a.phpdebugbar-tab span.phpdebugbar-badge { - vertical-align: 0px; - padding: 2px 6px; - background: #f4645f; - font-size: 12px; - color: #fff; - border-radius: 10px; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="critical"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="critical"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="error"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="error"] { + background-color: var(--debugbar-red-vivid); } -div.phpdebugbar-openhandler .phpdebugbar-openhandler-header { - background-size: 20px; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="notice"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="notice"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="warning"], +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="warning"] { + background-color: #f99400; } -div.phpdebugbar-openhandler a { - color: #555; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter:hover { + color: #FFF; + opacity: 0.85; } -div.phpdebugbar-openhandler table { - table-layout: fixed; +div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded { + opacity: 0.45; } -div.phpdebugbar-openhandler table td, -div.phpdebugbar-openhandler table th { - text-align: left; + +a.phpdebugbar-tab.phpdebugbar-active { + background: var(--debugbar-red-vivid); + background-image: none; + color: #fff !important; } -div.phpdebugbar-openhandler table td a { - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +a.phpdebugbar-tab.phpdebugbar-active span.phpdebugbar-badge { + background-color: var(--debugbar-badge); + color: var(--debugbar-badge-text); } -.phpdebugbar-indicator span.phpdebugbar-tooltip { - top: -36px; - border: none; - border-radius: 5px; - background: #f5f5f5; - font-size: 12px; +a.phpdebugbar-tab span.phpdebugbar-badge { + vertical-align: 0px; + padding: 2px 8px; + text-align: center; + background: var(--debugbar-red-vivid); + font-size: 11px; + font-family: var(--font-mono); + color: var(--debugbar-badge-active-text); + border-radius: 10px; + position: relative; } -div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter { - margin: 0; - padding: 5px 8px; - border-radius: 0; - font-size: 12px; - transition: background-color .25s linear 0s, color .25s linear 0s; +.phpdebugbar-indicator { + cursor: text; } -div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter:hover { - background-color: #ad4844; - color: #fff; +div.phpdebugbar-mini-design a.phpdebugbar-tab:hover span.phpdebugbar-text { + left: 0px; + right: auto; } .phpdebugbar-widgets-toolbar > .fa { width: 25px; font-size: 15px; - color: #555; text-align: center; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item { - padding: 5px 10px; + padding: 7px 10px; border: none; font-family: inherit; overflow: visible; - display: flex; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-sql { - flex: 1 1 auto; + line-height: 20px; +} + + +.phpdebugbar-widgets-templates ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item { + display: block; +} + +.phpdebugbar-widgets-templates ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item, +.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item { + line-height: 15px; } -ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-duration { - flex: 0 0 auto; +.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item { + cursor: pointer; + display: block; +} + +.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-subject { + display: inline-block; + margin-right: 15px; +} + +.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-headers { + margin: 10px 0px; + padding: 7px 10px; + border-left: 2px solid var(--debugbar-header); + line-height: 17px; +} + +.phpdebugbar-widgets-sql.phpdebugbar-widgets-name { + font-weight: bold; +} + +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-sql { + flex: 1; + margin-right: 5px; + max-width: 100%; +} + +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-copy-clipboard, +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-duration, +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-stmt-id, +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-memory { + margin-left: auto; + margin-right: 5px; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-database { - flex: 0 0 auto; + margin-left: auto; +} + +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-stmt-id a { + color: #888; +} + +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-stmt-id a:hover { + color: #aaa; } -ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-params { - flex: 1 1 auto; - order: -1; +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item table.phpdebugbar-widgets-params { + margin: 10px 0px; + font-size: 12px; + border-left: 2px solid var(--debugbar-border); +} + +div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params th, +div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params td { + padding: 1px 10px!important; +} + +div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params th { + padding: 2px 10px!important; + background-color: var(--debugbar-background); +} + +div.phpdebugbar-widgets-templates ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(odd) table.phpdebugbar-widgets-params th { + background-color: var(--debugbar-background-alt); +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name { width: auto; - max-width: 200px; } -ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(even) { - background-color: #f9f9f9; +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name .phpdebugbar-fa { + position: relative; + top: 1px; + margin-left: 3px; +} + +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(even), +table.phpdebugbar-widgets-tablevar tr:nth-child(even) { + background-color: var(--debugbar-background-alt); +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value { + display: inline-flex; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value:before { + font-family: PhpDebugbarFontAwesome; + content: "\f005"; + color: #333; + font-size: 15px !important; + margin-right: 8px; + float: left; +} + +table.phpdebugbar-widgets-tablevar td { + border: 0; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-info { + color: #1299DA; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-info:before { + content: "\f05a"; + color: #5896e2; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-success:before { + content: "\f058"; + color: #45ab45; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-error { + color: #e74c3c; } div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-error:before { + color: var(--debugbar-red-vivid); +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-warning:before, +div.phpdebugbar-widgets-messages .phpdebugbar-widgets-value.phpdebugbar-widgets-warning { + color: #FF9800; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-deprecation:before { + content: "\f1f6"; + color: #FF9800; +} + +div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item pre.sf-dump { + display: inline-block !important; + position: relative; + top: -1px; +} + +div.phpdebugbar-panel div.phpdebugbar-widgets-status { + padding: 9px 10px !important; + width: calc(100% - 20px); + margin-top: 0px !important; + line-height: 11px!important; + font-weight: bold!important; + background: var(--debugbar-background-alt) !important; + border-bottom: 1px solid var(--debugbar-border) !important; +} + +div.phpdebugbar-panel div.phpdebugbar-widgets-status > * { + color: var(--debugbar-header-text)!important; +} + +div.phpdebugbar-panel div.phpdebugbar-widgets-status > span:first-child:before { + font-family: PhpDebugbarFontAwesome; + content: "\f05a"; + color: var(--debugbar-icons); + font-size: 14px; + position: relative; + top: 1px; + margin-right: 8px; +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params th, +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td { + padding: 4px 10px; +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params th { + background-color: var(--debugbar-background-alt); +} + +div.phpdebugbar-widgets-sqlqueries ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(even) table.phpdebugbar-widgets-params th { + background-color: var(--debugbar-background); +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name { + text-align: right; + vertical-align: top; + white-space: nowrap; +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-value { + text-align: left; +} + +div.phpdebugbar-widgets-templates .phpdebugbar-widgets-list-item table.phpdebugbar-widgets-params { + width: auto!important; +} + +ul.phpdebugbar-widgets-list ul.phpdebugbar-widgets-table-list { + text-align: left; + line-height: 150%; +} + + +ul.phpdebugbar-widgets-cache a.phpdebugbar-widgets-forget { + float: right; font-size: 12px; - color: #e74c3c; + padding: 0 4px; + background: var(--debugbar-red-vivid); + margin: 0 2px; + border-radius: 4px; + color: #fff; + text-decoration: none; + line-height: 1.25rem; +} + +div.phpdebugbar-mini-design div.phpdebugbar-header-left a.phpdebugbar-tab { + border-right: none; +} + +div.phpdebugbar-header-right { + display:flex; + flex-direction: row-reverse; + align-items: center; + flex-wrap: wrap; +} + +div.phpdebugbar-header-right > * { + border-right: 1px solid var(--debugbar-header); +} + +div.phpdebugbar-header-right > *:first-child { + border-right: 0; +} + +div.phpdebugbar-header-right a.phpdebugbar-tab.phpdebugbar-tab-settings { + border-left: 0; +} + +div.phpdebugbar-panel[data-collector="__datasets"] { + padding: 0 10px; +} + +div.phpdebugbar-panel table { + margin: 10px 0px!important; + width: 100%!important; +} + +div.phpdebugbar-panel table .phpdebugbar-widgets-name { + font-size: 13px; } -div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-warning:before { +dl.phpdebugbar-widgets-kvlist > :nth-child(4n-1), +dl.phpdebugbar-widgets-kvlist > :nth-child(4n) { + background-color: var(--debugbar-background-alt); +} + +.phpdebugbar pre.sf-dump:after { + clear: none!important; +} + +div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item > div { + display: none; +} + + +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-database:before, +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-duration:before, +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-memory:before, +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-row-count:before, +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-copy-clipboard:before, +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-stmt-id:before, +div.phpdebugbar-widgets-templates span.phpdebugbar-widgets-param-count:before { + margin-right: 6px!important; +} + +div.phpdebugbar dl.phpdebugbar-widgets-kvlist > :nth-child(4n)::before { +background-color: var(--background-color-alt); +} + +dt.phpdebugbar-widgets-key { + padding-left: 10px !important; +} + +dt.phpdebugbar-widgets-key { + position: relative; + /*background: white;*/ + z-index: 1; +} + +dd.phpdebugbar-widgets-value { + position: relative; +} + +dd.phpdebugbar-widgets-value::before { + content: " "; + position: absolute; + height: 100%; + left: 0; + top: 0; + width: 33.33%; + margin-left: -33.33%; +} + +dd.phpdebugbar-widgets-value pre.sf-dump { + padding-top: 0; + padding-bottom: 0; +} + +ul.phpdebugbar-widgets-table-list { + padding: 4px 0; +} + +ul.phpdebugbar-widgets-table-list li { + margin-bottom: 4px; +} + +ul.phpdebugbar-widgets-table-list li:last-child { + margin-bottom: 0; +} + +div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-copy-clipboard { + margin-left: 8px !important; +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name { + width: 150px; +} + +div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-connection { font-size: 12px; - color: #f1c40f; + padding: 2px 4px; + background: #737373; + margin-left: 6px; + border-radius: 4px; + color: #fff !important; } -div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-error { - color: #e74c3c; +div.phpdebugbar-widgets-sqlqueries button.phpdebugbar-widgets-explain-btn { + cursor: pointer; + background: #383838; + color: #fff; + font-size: 13px; + padding: 0 8px; + border-radius: 4px; + line-height: 1.25rem; +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain { + margin: 0 !important; +} + +div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain th { + border: 1px solid var(--debugbar-border); + text-align: center; +} + +div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-visual-explain { + display: inline-block; + font-weight: bold; + text-decoration: underline; + margin-top: 6px; +} + +div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-visual-link { + margin-left: 6px; +} + +div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-visual-explain:after { + content: "\f08e"; + font-family: PhpDebugbarFontAwesome; + margin-left: 4px; + font-size: 12px; } -.phpdebugbar-widgets-value.phpdebugbar-widgets-warning { - color: #f1c40f; +div.phpdebugbar-widgets-sqlqueries li.phpdebugbar-widgets-list-item.phpdebugbar-widgets-expandable { + cursor: pointer; } -div.phpdebugbar-widgets-sqlqueries .phpdebugbar-widgets-status { - background: none !important; - font-family: inherit !important; - font-weight: 400 !important; +div.phpdebugbar-widgets-sqlqueries li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-params { + cursor: default; } diff --git a/src/Resources/queries/widget.js b/src/Resources/queries/widget.js new file mode 100644 index 000000000..b9de555a2 --- /dev/null +++ b/src/Resources/queries/widget.js @@ -0,0 +1,413 @@ +(function($) { + + let css = PhpDebugBar.utils.makecsscls('phpdebugbar-'); + let csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); + + /** + * Widget for displaying sql queries. + * + * Options: + * - data + */ + const QueriesWidget = PhpDebugBar.Widgets.LaravelQueriesWidget = PhpDebugBar.Widget.extend({ + + className: csscls('sqlqueries'), + + duplicateQueries: new Set(), + + hiddenConnections: new Set(), + + copyToClipboard: function (code) { + if (document.selection) { + const range = document.body.createTextRange(); + range.moveToElementText(code); + range.select(); + } else if (window.getSelection) { + const range = document.createRange(); + range.selectNodeContents(code); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + } + + var isCopied = false; + try { + isCopied = document.execCommand('copy'); + console.log('Query copied to the clipboard'); + } catch (err) { + alert('Oops, unable to copy'); + } + + window.getSelection().removeAllRanges(); + + return isCopied; + }, + + explainMysql: function ($element, statement, rows, visual) { + const headings = []; + for (const key in rows[0]) { + headings.push($('').text(key)); + } + + const values = []; + for (const row of rows) { + const $tr = $(''); + for (const key in row) { + $tr.append($('').text(row[key])); + } + values.push($tr); + } + + const $table = $('
').addClass(csscls('explain')); + $table.find('thead').append($('').append(headings)); + $table.find('tbody').append(values); + + $element.append($table); + if (visual) { + $element.append(this.explainVisual(statement, visual.confirm)); + } + }, + + explainPgsql: function ($element, statement, rows, visual) { + const $ul = $('