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 index 798753326..41f9ae41c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,5 @@ # These are supported funding model platforms github: barryvdh -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +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/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 f5eada032..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,13 +17,18 @@ } ], "require": { - "php": ">=7.0", - "maximebf/debugbar": "~1.15.0", - "illuminate/routing": "^5.5|^6", - "illuminate/session": "^5.5|^6", - "illuminate/support": "^5.5|^6", - "symfony/debug": "^3|^4|^5", - "symfony/finder": "^3|^4|^5" + "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": { @@ -26,22 +38,29 @@ "src/helpers.php" ] }, + "autoload-dev": { + "psr-4": { + "Barryvdh\\Debugbar\\Tests\\": "tests" + } + }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.16-dev" }, "laravel": { "providers": [ "Barryvdh\\Debugbar\\ServiceProvider" ], "aliases": { - "Debugbar": "Barryvdh\\Debugbar\\Facade" + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" } } }, - "require-dev": { - "laravel/framework": "5.5.x" + "scripts": { + "check-style": "phpcs", + "fix-style": "phpcbf", + "test": "phpunit" } } diff --git a/config/debugbar.php b/config/debugbar.php index 3275f105a..2d86208c3 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -15,8 +15,10 @@ */ 'enabled' => env('DEBUGBAR_ENABLED', null), + 'hide_empty_tabs' => env('DEBUGBAR_HIDE_EMPTY_TABS', true), // Hide tabs until they have content 'except' => [ - 'telescope*' + 'telescope*', + 'horizon*', ], /* @@ -24,21 +26,70 @@ | 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' => [ - '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 + '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')), + /* |-------------------------------------------------------------------------- | Vendors @@ -47,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), /* |-------------------------------------------------------------------------- @@ -64,11 +115,21 @@ | 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, - 'add_ajax_timing' => false, - + '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 @@ -78,7 +139,7 @@ | in the Messages tab. | */ - 'error_handler' => false, + 'error_handler' => env('DEBUGBAR_ERROR_HANDLER', false), /* |-------------------------------------------------------------------------- @@ -89,7 +150,7 @@ | Extension, without the server-side code. It uses Debugbar collectors instead. | */ - 'clockwork' => false, + 'clockwork' => env('DEBUGBAR_CLOCKWORK', false), /* |-------------------------------------------------------------------------- @@ -101,28 +162,31 @@ */ 'collectors' => [ - '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 - 'auth' => false, // Display Laravel authentication status - 'gate' => true, // Display Laravel Gate checks - 'session' => true, // Display session data - 'symfony_request' => true, // Only one can be enabled.. - 'mail' => true, // Catch mail messages - 'laravel' => false, // Laravel version and environment - 'events' => false, // All events fired - 'default_request' => false, // Regular or special Symfony request logger - 'logs' => false, // Add the latest log messages - 'files' => false, // Show the included files - 'config' => false, // Display config settings - 'cache' => false, // Display cache events - 'models' => false, // Display models + '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 ], /* @@ -135,33 +199,77 @@ */ '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' => true, // Also show the users name/email in the debugbar + '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' => true, // Render SQL with the parameters substituted - 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. - 'timeline' => false, // Add the queries to the timeline + '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' => false, - 'types' => ['SELECT'], // // workaround ['SELECT'] only. https://github.com/barryvdh/laravel-debugbar/issues/888 ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ + 'enabled' => env('DEBUGBAR_OPTIONS_DB_EXPLAIN_ENABLED', false), ], - 'hints' => true, // Show hints for common mistakes + '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' => [ - 'full_log' => false + 'timeline' => env('DEBUGBAR_OPTIONS_MAIL_TIMELINE', true), // Add mails to the timeline + 'show_body' => env('DEBUGBAR_OPTIONS_MAIL_SHOW_BODY', true), ], 'views' => [ - 'data' => false, //Note: Can slow down the application, because the data can be quite large.. + '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' => true // show complete route on bar + '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' => null + 'file' => env('DEBUGBAR_OPTIONS_LOGS_FILE', null), ], 'cache' => [ - 'values' => true // collect cache values + 'values' => env('DEBUGBAR_OPTIONS_CACHE_VALUES', true), // Collect cache values ], ], @@ -176,27 +284,56 @@ | */ - '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 domain + | Debugbar route middleware |-------------------------------------------------------------------------- | - | By default DebugBar route served from the same domain that request served. + | 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' => null, + '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 f3a3d25a6..edb7d523f 100644 --- a/readme.md +++ b/readme.md @@ -1,21 +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) - -### Note for v3: Debugbar is now enabled by requiring the package, but still needs APP_DEBUG=true by default! - -### For Laravel < 5.5, please use the [2.4 branch](https://github.com/barryvdh/laravel-debugbar/tree/2.4)! - -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) + +> [!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). -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. +> [!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 @@ -31,7 +33,7 @@ This package includes some custom collectors: Bootstraps the following collectors for Laravel: - LogCollector: Show all Log messages - - SwiftMailCollector and SwiftLogCollector for Mail + - SymfonyMailCollector for Mail And the default collectors: - PhpInfoCollector @@ -40,7 +42,7 @@ 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 @@ -50,24 +52,28 @@ Require this package with composer. It is recommended to only require the packag composer require barryvdh/laravel-debugbar --dev ``` -Laravel 5.5 uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider. +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.5+: +### Laravel without auto-discovery: -If you don't use auto-discovery, add the ServiceProvider to the providers array in config/app.php +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: ```php -'Debugbar' => Barryvdh\Debugbar\Facade::class, +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`) or by setting `DEBUGBAR_ENABLED` in your `.env`. See more options in `config/debugbar.php` @@ -80,6 +86,16 @@ You can also only display the js or css vendors, by setting it to 'js' or 'css'. 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`: @@ -134,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)); @@ -171,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 @@ -199,3 +224,7 @@ The Stopwatch extension adds a [stopwatch tag](http://symfony.com/blog/new-in-sy …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 64c7dd28d..abafae915 100644 --- a/src/Console/ClearCommand.php +++ b/src/Console/ClearCommand.php @@ -1,4 +1,6 @@ -debugbar->boot(); if ($storage = $this->debugbar->getStorage()) { - try - { + try { $storage->clear(); - } catch(\InvalidArgumentException $e) { + } catch (\InvalidArgumentException $e) { // hide InvalidArgumentException if storage location does not exist - if(strpos($e->getMessage(), 'does not exist') === false) { + if (strpos($e->getMessage(), 'does not exist') === false) { throw $e; } } diff --git a/src/Controllers/AssetController.php b/src/Controllers/AssetController.php index 00c58300f..646ab44ab 100644 --- a/src/Controllers/AssetController.php +++ b/src/Controllers/AssetController.php @@ -1,4 +1,6 @@ -dumpAssetsToString('js'); $response = new Response( - $content, 200, [ + $content, + 200, + [ 'Content-Type' => 'text/javascript', ] ); @@ -36,7 +40,9 @@ public function css() $content = $renderer->dumpAssetsToString('css'); $response = new Response( - $content, 200, [ + $content, + 200, + [ 'Content-Type' => 'text/css', ] ); diff --git a/src/Controllers/BaseController.php b/src/Controllers/BaseController.php index 36dfe5b4d..3d2f15f7d 100644 --- a/src/Controllers/BaseController.php +++ b/src/Controllers/BaseController.php @@ -1,10 +1,13 @@ -debugbar = $debugbar; - if ($request->hasSession()){ + if ($request->hasSession()) { $request->session()->reflash(); } @@ -38,7 +41,7 @@ public function __construct(Request $request, LaravelDebugbar $debugbar) { $this->debugbar = $debugbar; - if ($request->hasSession()){ + if ($request->hasSession()) { $request->session()->reflash(); } } diff --git a/src/Controllers/CacheController.php b/src/Controllers/CacheController.php index 3d04305ab..c347700b6 100644 --- a/src/Controllers/CacheController.php +++ b/src/Controllers/CacheController.php @@ -1,4 +1,6 @@ -json(compact('success')); } - } diff --git a/src/Controllers/OpenHandlerController.php b/src/Controllers/OpenHandlerController.php index 77056f403..4b7449e0c 100644 --- a/src/Controllers/OpenHandlerController.php +++ b/src/Controllers/OpenHandlerController.php @@ -1,19 +1,67 @@ -ip(), ['127.0.0.1', '::1'], true)) { + return true; + } + + return false; + } + + public function handle(Request $request) { - $openHandler = new OpenHandler($this->debugbar); - $data = $openHandler->handle(null, false, false); + 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, [ + $data, + 200, + [ 'Content-Type' => 'application/json' ] ); @@ -26,7 +74,7 @@ public function handle() * @return mixed * @throws \DebugBar\DebugBarException */ - public function clockwork($id) + public function clockwork(Request $request, $id) { $request = [ 'op' => 'get', 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 index e98118eda..5fb9943bb 100644 --- a/src/Controllers/TelescopeController.php +++ b/src/Controllers/TelescopeController.php @@ -1,4 +1,6 @@ -find($uuid); $result = $storage->get('request', (new EntryQueryOptions())->batchId($entry->batchId))->first(); - return redirect(config('telescope.path') . '/requests/' . $result->id); + return redirect(config('telescope.domain') . '/' . config('telescope.path') . '/requests/' . $result->id); } } diff --git a/src/DataCollector/CacheCollector.php b/src/DataCollector/CacheCollector.php index 61b7ad4fa..059514725 100644 --- a/src/DataCollector/CacheCollector.php +++ b/src/DataCollector/CacheCollector.php @@ -1,51 +1,73 @@ 'hit', - CacheMissed::class => 'missed', - KeyWritten::class => 'written', - KeyForgotten::class => 'forgotten', + CacheHit::class => ['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 = null, $collectValues) + public function __construct($requestStartTime, $collectValues) { - parent::__construct(); + parent::__construct($requestStartTime); $this->collectValues = $collectValues; } - public function onCacheEvent(CacheEvent $event) + public function onCacheEvent($event) { $class = get_class($event); $params = get_object_vars($event); - - $label = $this->classMap[$class]; + $label = $this->classMap[$class][0]; if (isset($params['value'])) { if ($this->collectValues) { - $params['value'] = htmlspecialchars($this->getDataFormatter()->formatVar($event->value)); + 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']) && in_array($label, ['hit', 'written'])) { + 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']) : '', @@ -53,21 +75,44 @@ public function onCacheEvent(CacheEvent $event) } $time = microtime(true); - $this->addMeasure($label . "\t" . $event->key, $time, $time, $params); + $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 ($this->classMap as $eventClass => $type) { + 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'] = count($data['measures']); + $data['nb_measures'] = $data['count'] = count($data['measures']); return $data; } diff --git a/src/DataCollector/EventCollector.php b/src/DataCollector/EventCollector.php index 572cab538..5dfff86ba 100644 --- a/src/DataCollector/EventCollector.php +++ b/src/DataCollector/EventCollector.php @@ -1,9 +1,10 @@ collectValues = $collectValues; + $this->excludedEvents = $excludedEvents; $this->setDataFormatter(new SimpleFormatter()); } public function onWildcardEvent($name = null, $data = []) { + $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); + + return; + } + $params = $this->prepareParams($data); - $time = microtime(true); // 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; @@ -49,7 +74,8 @@ public function onWildcardEvent($name = null, $data = []) // 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->getDataFormatter()->formatVar($listener); @@ -57,7 +83,7 @@ public function onWildcardEvent($name = null, $data = []) $params['listeners.' . $i] = $listener; } - $this->addMeasure($name, $time, $time, $params); + $this->addMeasure($name, $currentTime, $currentTime, $params, null, $eventClass); } public function subscribe(Dispatcher $events) @@ -82,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; } diff --git a/src/DataCollector/FilesCollector.php b/src/DataCollector/FilesCollector.php index 4c56837c4..f6180f2bf 100644 --- a/src/DataCollector/FilesCollector.php +++ b/src/DataCollector/FilesCollector.php @@ -15,7 +15,7 @@ class FilesCollector extends DataCollector implements Renderable /** * @param \Illuminate\Container\Container $app */ - public function __construct(Container $app = null) + public function __construct(?Container $app = null) { $this->app = $app; $this->basePath = base_path(); @@ -34,7 +34,8 @@ public function collect() 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 @@ -49,7 +50,7 @@ public function collect() } else { $alreadyCompiled[] = [ 'message' => "* '" . $this->stripBasePath($file) . "',", - // Mark with *, so know they are compiled anyways. + // Mark with *, so know they are compiled anyway. 'is_string' => true, ]; } diff --git a/src/DataCollector/GateCollector.php b/src/DataCollector/GateCollector.php index 921df7439..ebb5a781e 100644 --- a/src/DataCollector/GateCollector.php +++ b/src/DataCollector/GateCollector.php @@ -4,45 +4,179 @@ 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 = null, $ability, $result, $arguments = []) { + $gate->after(function ($user, $ability, $result, $arguments = []) { $this->addCheck($user, $ability, $result, $arguments); }); } - public function addCheck($user = null, $ability, $result, $arguments = []) + /** + * {@inheritDoc} + */ + protected function customizeMessageHtml($messageHtml, $message) + { + $pos = strpos((string) $messageHtml, 'array:5'); + if ($pos !== false) { + + $name = $message['ability'] .' ' . $message['target'] ?? ''; + + $messageHtml = substr_replace($messageHtml, $name, $pos, 7); + } + + return parent::customizeMessageHtml($messageHtml, $message); + } + + 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->id; + $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, $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/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 ec327deb6..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 [ - "version" => $app::VERSION, - "environment" => $app->environment(), - "locale" => $app->getLocale(), + "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} */ @@ -49,22 +52,13 @@ public function getWidgets() { return [ "version" => [ - "icon" => "github", - "tooltip" => "Version", + "icon" => "laravel phpdebugbar-fab", "map" => "laravel.version", "default" => "" ], - "environment" => [ - "icon" => "desktop", - "tooltip" => "Environment", - "map" => "laravel.environment", - "default" => "" - ], - "locale" => [ - "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 2950e9506..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, + ]; } } @@ -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 = []; 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[] = ['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 index 3ce356c38..913877548 100644 --- a/src/DataCollector/ModelsCollector.php +++ b/src/DataCollector/ModelsCollector.php @@ -2,53 +2,65 @@ namespace Barryvdh\Debugbar\DataCollector; -use Barryvdh\Debugbar\DataFormatter\SimpleFormatter; -use DebugBar\DataCollector\MessagesCollector; +use DebugBar\DataCollector\DataCollector; +use DebugBar\DataCollector\DataCollectorInterface; +use DebugBar\DataCollector\Renderable; use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Support\Str; /** * Collector for Models. + * @deprecated in favor of \DebugBar\DataCollector\ObjectCountCollector */ -class ModelsCollector extends MessagesCollector +class ModelsCollector extends DataCollector implements DataCollectorInterface, Renderable { public $models = []; + public $count = 0; /** * @param Dispatcher $events */ public function __construct(Dispatcher $events) { - parent::__construct('models'); - $this->setDataFormatter(new SimpleFormatter()); - - $events->listen('eloquent.*', function ($event, $models) { - if (Str::contains($event, 'eloquent.retrieved')) { - foreach (array_filter($models) as $model) { - $class = get_class($model); - $this->models[$class] = ($this->models[$class] ?? 0) + 1; - } + $events->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() { - foreach ($this->models as $type => $count) { - $this->addMessage($count, $type); - } + ksort($this->models, SORT_NUMERIC); - return [ - 'count' => array_sum($this->models), - 'messages' => $this->getMessages(), - ]; + return ['data' => array_reverse($this->models), 'count' => $this->count]; } - public function getWidgets() + /** + * {@inheritDoc} + */ + public function getName() { - $widgets = parent::getWidgets(); - $widgets['models']['icon'] = 'cubes'; + return 'models'; + } - return $widgets; + /** + * {@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 f040e2b9a..32f4b416e 100644 --- a/src/DataCollector/MultiAuthCollector.php +++ b/src/DataCollector/MultiAuthCollector.php @@ -11,7 +11,6 @@ use Illuminate\Support\Str; use Illuminate\Contracts\Support\Arrayable; - /** * Collector for Laravel's Auth provider */ @@ -26,6 +25,9 @@ class MultiAuthCollector extends DataCollector implements Renderable /** @var bool */ protected $showName = false; + /** @var bool */ + protected $showGuardsData = true; + /** * @param \Illuminate\Auth\AuthManager $auth * @param array $guards @@ -45,6 +47,15 @@ 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} */ @@ -55,13 +66,13 @@ public function collect() ]; $names = ''; - foreach($this->guards as $guardName => $config) { + foreach ($this->guards as $guardName => $config) { try { $guard = $this->auth->guard($guardName); if ($this->hasUser($guard)) { $user = $guard->user(); - if(!is_null($user)) { + if (!is_null($user)) { $data['guards'][$guardName] = $this->getUserInformation($user); $names .= $guardName . ": " . $data['guards'][$guardName]['name'] . ', '; } @@ -80,6 +91,9 @@ public function collect() } $data['names'] = rtrim($names, ', '); + if (!$this->showGuardsData) { + unset($data['guards']); + } return $data; } @@ -90,11 +104,6 @@ private function hasUser(Guard $guard) return $guard->hasUser(); } - // For Laravel 5.5 - if (method_exists($guard, 'alreadyAuthenticated')) { - return $guard->alreadyAuthenticated(); - } - return false; } @@ -114,14 +123,16 @@ protected function getUserInformation($user = null) } // The default auth identifer is the ID number, which isn't all that - // useful. Try username and email. - $identifier = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user->id; - if (is_numeric($identifier)) { + // 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) { } @@ -146,14 +157,16 @@ public function getName() */ public function getWidgets() { - $widgets = [ - "auth" => [ + $widgets = []; + + if ($this->showGuardsData) { + $widgets["auth"] = [ "icon" => "lock", "widget" => "PhpDebugBar.Widgets.VariableListWidget", "map" => "auth.guards", - "default" => "{}" - ] - ]; + "default" => "{}", + ]; + } if ($this->showName) { $widgets['auth.name'] = [ @@ -166,5 +179,4 @@ public function getWidgets() 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 1686744f6..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 @@ -12,22 +15,50 @@ class QueryCollector extends PDOCollector { protected $timeCollector; 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 = ['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 * @@ -49,18 +80,53 @@ 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, array $middleware) { - $this->findSource = (bool) $value; + $this->findSource = $value; $this->middleware = $middleware; } + public function mergeExcludePaths(array $excludePaths) + { + $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; + } + /** * Enable/disable the EXPLAIN queries * @@ -70,81 +136,91 @@ public function setFindSource($value, array $middleware) public function setExplainSource($enabled, $types) { $this->explainQuery = $enabled; - // workaround ['SELECT'] only. https://github.com/barryvdh/laravel-debugbar/issues/888 -// 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 = []; - $time = $time / 1000; - $endTime = microtime(true); - $startTime = $endTime - $time; - $hints = $this->performQueryAnalysis($query); - - $pdo = $connection->getPdo(); - $bindings = $connection->prepareBindings($bindings); + $this->queryCount++; - // Run EXPLAIN on this query (if needed) - if ($this->explainQuery && preg_match('/^\s*('.implode('|', $this->explainTypes).') /i', $query)) { - $statement = $pdo->prepare('EXPLAIN ' . $query); - $statement->execute($bindings); - $explainResults = $statement->fetchAll(\PDO::FETCH_CLASS); + if ($this->hardLimit && $this->queryCount > $this->hardLimit) { + return; } - $bindings = $this->getDataFormatter()->checkBindings($bindings); - if (!empty($bindings) && $this->renderSqlWithParams) { - 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) - ? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/" - : "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"; - - // Mimic bindValue and only quote non-integer and non-float data types - if (!is_int($binding) && !is_float($binding)) { - $binding = $pdo->quote($binding); - } + $limited = $this->softLimit && $this->queryCount > $this->softLimit; + + $sql = (string) $query->sql; + $time = $query->time / 1000; + $endTime = microtime(true); + $startTime = $endTime - $time; + $hints = $this->performQueryAnalysis($sql); + + $pdo = null; + try { + $pdo = $query->connection->getPdo(); - $query = preg_replace($regex, $binding, $query, 1); + if(! ($pdo instanceof \PDO)) { + $pdo = null; } + } catch (\Throwable $e) { + // ignore error for non-pdo laravel drivers } $source = []; - if ($this->findSource) { + if (!$limited && $this->findSource) { try { $source = $this->findSource(); } catch (\Exception $e) { } } + $bindings = match (true) { + $limited && filled($query->bindings) => [], + default => $query->connection->prepareBindings($query->bindings), + }; + $this->queries[] = [ - 'query' => $query, + 'query' => $sql, 'type' => 'query', - 'bindings' => $this->getDataFormatter()->escapeBindings($bindings), + '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'); } } + /** + * Mimic mysql_real_escape_string + * + * @param string $value + * @return string + */ + protected function emulateQuote($value) + { + $search = ["\\", "\x00", "\n", "\r", "'", '"', "\x1a"]; + $replace = ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"]; + + return "'" . str_replace($search, $replace, (string) $value) . "'"; + } + /** * Explainer::performQueryAnalysis() * @@ -156,18 +232,19 @@ public function addQuery($query, $bindings, $time, $connection) * @version $Id$ * @access public * @param string $query - * @return string + * @return string[] */ protected function performQueryAnalysis($query) { + // @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.'; @@ -179,10 +256,12 @@ 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 $hints; + + // @codingStandardsIgnoreEnd } /** @@ -192,7 +271,7 @@ protected function performQueryAnalysis($query) */ protected function findSource() { - $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, 50); + $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, app('config')->get('debugbar.debug_backtrace_limit', 50)); $sources = []; @@ -200,7 +279,7 @@ protected function findSource() $sources[] = $this->parseTrace($index, $trace); } - return array_filter($sources); + return array_slice(array_filter($sources), 0, is_int($this->findSource) ? $this->findSource : 5); } /** @@ -216,7 +295,8 @@ protected function parseTrace($index, array $trace) 'index' => $index, 'namespace' => null, 'name' => null, - 'line' => isset($trace['line']) ? $trace['line'] : '?', + 'file' => null, + 'line' => $trace['line'] ?? '1', ]; if (isset($trace['function']) && $trace['function'] == 'substituteBindings') { @@ -225,37 +305,41 @@ protected function parseTrace($index, array $trace) return $frame; } - if (isset($trace['class']) && + if ( + isset($trace['class']) && isset($trace['file']) && !$this->fileIsInExcludedPath($trace['file']) ) { - $file = $trace['file']; + $frame->file = $trace['file']; - if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) { - list($file, $frame->line) = $this->getTwigInfo($trace); - } elseif (strpos($file, storage_path()) !== false) { - $hash = pathinfo($file, PATHINFO_FILENAME); + 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)) { + 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($file, 'Middleware') !== false) { - $frame->name = $this->findMiddlewareFromFile($file); + } elseif (strpos($frame->file, 'Middleware') !== false) { + $frame->name = $this->findMiddlewareFromFile($frame->file); if ($frame->name) { $frame->namespace = 'middleware'; } else { - $frame->name = $this->normalizeFilename($file); + $frame->name = $this->normalizeFilePath($frame->file); } return $frame; } - $frame->name = $this->normalizeFilename($file); + $frame->name = $this->normalizeFilePath($frame->file); return $frame; } @@ -272,15 +356,9 @@ protected function parseTrace($index, array $trace) */ protected function fileIsInExcludedPath($file) { - $excludedPaths = [ - '/vendor/laravel/framework/src/Illuminate/Database', - '/vendor/laravel/framework/src/Illuminate/Events', - '/vendor/barryvdh/laravel-debugbar', - ]; - $normalizedPath = str_replace('\\', '/', $file); - foreach ($excludedPaths as $excludedPath) { + foreach ($this->backtraceExcludePaths as $excludedPath) { if (strpos($normalizedPath, $excludedPath) !== false) { return true; } @@ -300,7 +378,7 @@ protected function findMiddlewareFromFile($file) $filename = pathinfo($file, PATHINFO_FILENAME); foreach ($this->middleware as $alias => $class) { - if (strpos($class, $filename) !== false) { + if (!is_null($class) && !is_null($filename) && strpos($class, $filename) !== false) { return $alias; } } @@ -310,7 +388,7 @@ protected function findMiddlewareFromFile($file) * Find the template name from the hash. * * @param string $hash - * @return null|string + * @return null|array */ protected function findViewFromHash($hash) { @@ -325,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]; } } } @@ -353,20 +433,6 @@ protected function getTwigInfo($trace) return [$file, -1]; } - /** - * Shorten the path by removing the relative links and base dir - * - * @param string $path - * @return string - */ - protected function normalizeFilename($path) - { - if (file_exists($path)) { - $path = realpath($path); - } - return str_replace(base_path(), '', $path); - } - /** * Collect a database transaction event. * @param string $event @@ -375,6 +441,7 @@ protected function normalizeFilename($path) */ public function collectTransactionEvent($event, $connection) { + $this->transactionEventsCount++; $source = []; if ($this->findSource) { @@ -388,11 +455,14 @@ public function collectTransactionEvent($event, $connection) 'query' => $event, 'type' => 'transaction', 'bindings' => [], + 'start' => microtime(true), 'time' => 0, + 'memory' => 0, 'source' => $source, - 'explain' => [], - 'connection' => $connection->getDatabaseName(), + 'connection' => $connection, + 'driver' => $connection->getConfig('driver'), 'hints' => null, + 'show_copy' => false, ]; } @@ -402,6 +472,8 @@ public function collectTransactionEvent($event, $connection) public function reset() { $this->queries = []; + $this->queryCount = 0; + $this->infoStatements = 0 ; } /** @@ -410,46 +482,120 @@ public function reset() public function collect() { $totalTime = 0; + $totalMemory = 0; $queries = $this->queries; $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']; + + $connectionName = $query['connection']->getDatabaseName(); + if (str_ends_with($connectionName, '.sqlite')) { + $connectionName = $this->normalizeFilePath($connectionName); + } + + $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->getDataFormatter()->formatSql($query['query']), + 'sql' => $this->getSqlQueryToDisplay($query), 'type' => $query['type'], 'params' => [], - 'bindings' => $query['bindings'], + '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' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']), - 'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])), - 'connection' => $query['connection'], + '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; - //Add the results from the explain as new rows - foreach($query['explain'] as $explain){ - $statements[] = [ - 'sql' => ' - EXPLAIN #' . $explain->id . ': `' . $explain->table . '` (' . $explain->select_type . ')', - 'type' => 'explain', - 'params' => $explain, - 'row_count' => $explain->rows, - 'stmt_id' => $explain->id, - ]; + 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; + } } } - $nb_statements = array_filter($queries, function ($query) { - return $query['type'] == 'query'; - }); + 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 = [ - 'nb_statements' => count($nb_statements), + '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; @@ -471,7 +617,7 @@ public function getWidgets() return [ "queries" => [ "icon" => "database", - "widget" => "PhpDebugBar.Widgets.LaravelSQLQueriesWidget", + "widget" => "PhpDebugBar.Widgets.LaravelQueriesWidget", "map" => "queries", "default" => "[]" ], @@ -481,4 +627,55 @@ public function getWidgets() ] ]; } + + 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 index 3a243781a..de372d18f 100644 --- a/src/DataCollector/RequestCollector.php +++ b/src/DataCollector/RequestCollector.php @@ -5,9 +5,13 @@ use DebugBar\DataCollector\DataCollector; use DebugBar\DataCollector\DataCollectorInterface; use DebugBar\DataCollector\Renderable; +use Illuminate\Http\Request; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; use Laravel\Telescope\IncomingEntry; use Laravel\Telescope\Telescope; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; /** @@ -19,26 +23,34 @@ class RequestCollector extends DataCollector implements DataCollectorInterface, { /** @var \Symfony\Component\HttpFoundation\Request $request */ protected $request; - /** @var \Symfony\Component\HttpFoundation\Request $response */ + /** @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\Request $response + * @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) + 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', + ]); } /** @@ -54,14 +66,34 @@ public function getName() */ public function getWidgets() { - return [ + $widgets = [ "request" => [ "icon" => "tags", "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", - "map" => "request", + "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; } /** @@ -90,46 +122,57 @@ public function collect() } $statusCode = $response->getStatusCode(); + $startTime = defined('LARAVEL_START') ? LARAVEL_START : $request->server->get('REQUEST_TIME_FLOAT'); + $query = $request->getQueryString(); + $htmlData = []; $data = [ - 'path_info' => $request->getPathInfo(), - 'status_code' => $statusCode, - 'status_text' => isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : '', - 'format' => $request->getRequestFormat(), - 'content_type' => $response->headers->get('Content-Type') ? $response->headers->get( + '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_server' => $request->server->all(), 'request_cookies' => $request->cookies->all(), 'response_headers' => $responseHeaders, ]; if ($this->session) { - $sessionAttributes = []; - foreach ($this->session->all() as $key => $value) { - $sessionAttributes[$key] = $value; - } - $data['session_attributes'] = $sessionAttributes; + $data['session_attributes'] = $this->session->all(); } - 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']['authorization'][0])) { + $data['request_headers']['authorization'][0] = substr($data['request_headers']['authorization'][0], 0, 12) . '******'; } - 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 ($this->hiddens as $key) { + if (Arr::has($data, $key)) { + Arr::set($data, $key, '******'); + } } - ; foreach ($data as $key => $var) { if (!is_string($data[$key])) { @@ -137,25 +180,118 @@ public function collect() } else { $data[$key] = e($data[$key]); } - } - $htmlData = []; 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'; + $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 $htmlData + $data; + return array_filter($result); } private function getCookieHeader($name, $value, $expires, $path, $domain, $secure, $httponly) { - $cookie = sprintf('%s=%s', $name, urlencode($value)); + $cookie = sprintf('%s=%s', $name, urlencode($value ?? '')); if (0 !== $expires) { if (is_numeric($expires)) { diff --git a/src/DataCollector/RouteCollector.php b/src/DataCollector/RouteCollector.php index 6128e331d..f548a795e 100644 --- a/src/DataCollector/RouteCollector.php +++ b/src/DataCollector/RouteCollector.php @@ -2,10 +2,9 @@ namespace Barryvdh\Debugbar\DataCollector; +use Closure; use DebugBar\DataCollector\DataCollector; use DebugBar\DataCollector\Renderable; -use Illuminate\Http\Request; -use Illuminate\Routing\Route; use Illuminate\Routing\Router; use Illuminate\Support\Facades\Config; @@ -49,38 +48,73 @@ protected function getRouteInformation($route) return []; } $uri = head($route->methods()) . ' ' . $route->uri(); - $action = $route->getAction(); + $action = $route->getAction(); $result = [ - 'uri' => $uri ?: '-', + '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 (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); - } + 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 (isset($action['uses']) && $action['uses'] instanceof \Closure) { - $reflector = new \ReflectionFunction($action['uses']); - $result['uses'] = $this->formatVar($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 = ltrim(str_replace(base_path(), '', $reflector->getFileName()), '/'); - $result['file'] = $filename . ':' . $reflector->getStartLine() . '-' . $reflector->getEndLine(); + $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; - } - - + if ($middleware = $this->getMiddleware($route)) { + $result['middleware'] = $middleware; + } - return $result; + return array_filter($result); } /** @@ -91,7 +125,9 @@ protected function getRouteInformation($route) */ protected function getMiddleware($route) { - return implode(', ', $route->middleware()); + return implode(', ', array_map(function ($middleware) { + return $middleware instanceof Closure ? 'Closure' : $middleware; + }, $route->gatherMiddleware())); } /** @@ -110,19 +146,11 @@ public function getWidgets() $widgets = [ "route" => [ "icon" => "share", - "widget" => "PhpDebugBar.Widgets.VariableListWidget", + "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", "map" => "route", "default" => "{}" ] ]; - if (Config::get('debugbar.options.route.label', true)) { - $widgets['currentroute'] = [ - "icon" => "share", - "tooltip" => "Route", - "map" => "route.uri", - "default" => "" - ]; - } return $widgets; } diff --git a/src/DataCollector/SessionCollector.php b/src/DataCollector/SessionCollector.php index 8501646d6..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|\Illuminate\Contracts\Session\Session $session */ protected $session; + /** @var array */ + protected $hiddens; /** * Create a new SessionCollector * * @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 = []; - 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; } diff --git a/src/DataCollector/ViewCollector.php b/src/DataCollector/ViewCollector.php index b7c657e16..70041f50d 100644 --- a/src/DataCollector/ViewCollector.php +++ b/src/DataCollector/ViewCollector.php @@ -3,26 +3,38 @@ namespace Barryvdh\Debugbar\DataCollector; use Barryvdh\Debugbar\DataFormatter\SimpleFormatter; -use DebugBar\Bridge\Twig\TwigCollector; +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\VarDumper\Cloner\VarCloner; -class ViewCollector extends TwigCollector +class ViewCollector extends DataCollector implements Renderable, AssetProvider { + 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 = []; + $this->exclude_paths = $excludePaths; + $this->group = $group; + $this->timeCollector = $timeCollector; } public function getName() @@ -46,6 +58,17 @@ public function getWidgets() ]; } + /** + * @return array + */ + public function getAssets() + { + return [ + 'css' => 'widgets/templates/widget.css', + 'js' => 'widgets/templates/widget.js', + ]; + } + /** * Add a View instance to the Collector * @@ -54,41 +77,104 @@ 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); + } } - if (substr($path, -10) == '.blade.php') { - $type = 'blade'; - } else { + $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 (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 = []; - foreach ($view->getData() as $key => $value) { - $data[$key] = $this->getDataFormatter()->formatVar($value); - } - $params = $data; + $params = []; } $template = [ - 'name' => $path ? sprintf('%s (%s)', $name, $path) : $name, - 'param_count' => count($params), + 'name' => $name, + 'param_count' => $this->collect_data ? count($params) : null, 'params' => $params, + 'start' => microtime(true), 'type' => $type, + 'hash' => $hash, ]; - if ( $this->getXdebugLink($path)) { + if ($path && $this->getXdebugLinkTemplate()) { $template['xdebug_link'] = $this->getXdebugLink($path); } @@ -97,10 +183,27 @@ public function addView(View $view) 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 [ - 'nb_templates' => count($templates), + 'count' => count($this->templates), + 'nb_templates' => count($this->templates), 'templates' => $templates, ]; } diff --git a/src/DataFormatter/QueryFormatter.php b/src/DataFormatter/QueryFormatter.php index aa0db8c7b..645b6ff13 100644 --- a/src/DataFormatter/QueryFormatter.php +++ b/src/DataFormatter/QueryFormatter.php @@ -4,9 +4,9 @@ use DebugBar\DataFormatter\DataFormatter; +#[\AllowDynamicProperties] class QueryFormatter extends DataFormatter { - /** * Removes extra spaces at the beginning and end of the SQL query and its lines. * @@ -15,7 +15,10 @@ class QueryFormatter extends DataFormatter */ public function formatSql($sql) { - return trim(preg_replace("/\s*\n\s*/", "\n", $sql)); + $sql = preg_replace("/\?(?=(?:[^'\\\']*'[^'\\']*')*[^'\\\']*$)(?:\?)/", '?', $sql); + $sql = trim(preg_replace("/\s*\n\s*/", "\n", $sql)); + + return $sql; } /** @@ -30,21 +33,15 @@ public function checkBindings($bindings) if (is_string($binding) && !mb_check_encoding($binding, 'UTF-8')) { $binding = '[BINARY DATA]'; } - } - return $bindings; - } + if (is_array($binding)) { + $binding = $this->checkBindings($binding); + $binding = '[' . implode(',', $binding) . ']'; + } - /** - * Make the bindings safe for outputting. - * - * @param array $bindings - * @return array - */ - public function escapeBindings($bindings) - { - foreach ($bindings as &$binding) { - $binding = htmlentities($binding, ENT_QUOTES, 'UTF-8', false); + if (is_object($binding)) { + $binding = json_encode($binding); + } } return $bindings; @@ -56,7 +53,7 @@ public function escapeBindings($bindings) * @param object|null $source If the backtrace is disabled, the $source will be null. * @return string */ - public function formatSource($source) + public function formatSource($source, $short = false) { if (! is_object($source)) { return ''; @@ -64,11 +61,11 @@ public function formatSource($source) $parts = []; - if ($source->namespace) { + if (!$short && $source->namespace) { $parts['namespace'] = $source->namespace . '::'; } - $parts['name'] = $source->name; + $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 index 0a1fc9380..674a52813 100644 --- a/src/DataFormatter/SimpleFormatter.php +++ b/src/DataFormatter/SimpleFormatter.php @@ -9,6 +9,7 @@ * * @see https://github.com/symfony/symfony/blob/v3.4.4/src/Symfony/Component/HttpKernel/DataCollector/Util/ValueExporter.php */ +#[\AllowDynamicProperties] class SimpleFormatter extends DataFormatter { /** @@ -51,7 +52,7 @@ private function exportValue($value, $depth = 1, $deep = false) $indent = str_repeat(' ', $depth); - $a = array(); + $a = []; foreach ($value as $k => $v) { if (is_array($v)) { $deep = true; @@ -60,7 +61,8 @@ private function exportValue($value, $depth = 1, $deep = false) } if ($deep) { - return sprintf("[\n%s%s\n%s]", $indent, implode(sprintf(", \n%s", $indent), $a), str_repeat(' ', $depth - 1)); + $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)); 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 9c1f75926..933040d63 100644 --- a/src/Facade.php +++ b/src/Facade.php @@ -1,29 +1,30 @@ -cssFiles['laravel'] = __DIR__ . '/Resources/laravel-debugbar.css'; - $this->cssVendors['fontawesome'] = __DIR__ . '/Resources/vendor/font-awesome/style.css'; - $this->jsFiles['laravel-sql'] = __DIR__ . '/Resources/sqlqueries/widget.js'; $this->jsFiles['laravel-cache'] = __DIR__ . '/Resources/cache/widget.js'; + $this->jsFiles['laravel-queries'] = __DIR__ . '/Resources/queries/widget.js'; + + $this->setTheme(config('debugbar.theme', 'auto')); } /** @@ -31,7 +34,6 @@ public function __construct(DebugBar $debugBar, $baseUrl = null, $basePath = nul */ public function setUrlGenerator($url) { - } /** @@ -39,25 +41,28 @@ 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') - ]); + ])); - $cssRoute = preg_replace('/\Ahttps?:/', '', $cssRoute); - $jsRoute = preg_replace('/\Ahttps?:/', '', $jsRoute); + $nonce = $this->getNonceAttribute(); - $html = ""; - $html .= ""; + $html = ""; + $html .= ""; if ($this->isJqueryNoConflictEnabled()) { - $html .= '' . "\n"; + $html .= "jQuery.noConflict(true);" . "\n"; } - $html .= $this->getInlineHtml(); + $inlineHtml = $this->getInlineHtml(); + if ($nonce != '') { + $inlineHtml = preg_replace("/<(script|style)>/", "<$1{$nonce}>", $inlineHtml); + } + $html .= $inlineHtml; return $html; @@ -134,7 +139,7 @@ protected function makeUriRelativeTo($uri, $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 c1397eafd..6ef7d8eae 100644 --- a/src/LaravelDebugbar.php +++ b/src/LaravelDebugbar.php @@ -1,42 +1,54 @@ -app = $app; $this->version = $app->version(); $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; } /** @@ -123,18 +167,24 @@ public function boot() return; } - /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugbar */ - $debugbar = $this; - /** @var Application $app */ $app = $this->app; + /** @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 ($app['config']->get('debugbar.error_handler' , false)) { - set_error_handler([$this, 'handleError']); + if ($config->get('debugbar.error_handler', false)) { + $this->prevErrorHandler = set_error_handler([$this, 'handleError']); } - $this->selectStorage($debugbar); + $this->selectStorage($this); if ($this->shouldCollect('phpinfo', true)) { $this->addCollector(new PhpInfoCollector()); @@ -142,99 +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->addThrowable( - 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, $data = []) use ($debugbar) { - if ($data) { - $view = $data[0]; // For Laravel >= 5.4 - } - $debugbar['views']->addView($view); + function ($event, $params) { + $this['views']->addView($params[0]); } ); - } catch (\Exception $e) { - $this->addThrowable( - 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\RouteCollector')); - } catch (\Exception $e) { - $this->addThrowable( - 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); } } @@ -243,262 +323,314 @@ function ($view, $data = []) use ($debugbar) { if ($this->hasCollector('messages')) { $logger = new MessagesCollector('log'); $this['messages']->aggregate($logger); - $this->app['log']->listen( - function ($level, $message = null, $context = null) use ($logger) { - // Laravel 5.4 changed how the global log listeners are called. We must account for - // the first argument being an "event object", where arguments are passed - // via object properties, instead of individual arguments. - if ($level instanceof \Illuminate\Log\Events\MessageLogged) { - $message = $level->message; - $context = $level->context; - $level = $level->level; - } - + $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->addThrowable( - 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); $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')); - if ($this->app['config']->get('debugbar.options.db.with_params')) { + $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')) { - $middleware = ! $this->is_lumen ? $this->app['router']->getMiddleware() : []; - $queryCollector->setFindSource(true, $middleware); + 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 ($this->app['config']->get('debugbar.options.db.explain.enabled')) { - $types = $this->app['config']->get('debugbar.options.db.explain.types'); + if ($excludeBacktracePaths = $config->get('debugbar.options.db.backtrace_exclude_paths')) { + $queryCollector->mergeBacktraceExcludePaths($excludeBacktracePaths); + } + + 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) { - if (!$this->shouldCollect('db', true)) { + $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 } - // 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); - } - $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->addThrowable( - 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 { - $db->getEventDispatcher()->listen([ + $events->listen( \Illuminate\Database\Events\TransactionBeginning::class, - 'connection.*.beganTransaction', - ], function ($transaction) use ($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($transaction instanceof \Illuminate\Database\Events\TransactionBeginning) { - $connection = $transaction->connection; - } else { - $connection = $transaction; + function ($transaction) { + $this['queries']->collectTransactionEvent('Begin Transaction', $transaction->connection); } + ); - $queryCollector->collectTransactionEvent('Begin Transaction', $connection); - }); - - $db->getEventDispatcher()->listen([ + $events->listen( \Illuminate\Database\Events\TransactionCommitted::class, - 'connection.*.committed', - ], function ($transaction) use ($queryCollector) { + 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); + } + ); - if($transaction instanceof \Illuminate\Database\Events\TransactionCommitted) { - $connection = $transaction->connection; - } else { - $connection = $transaction; + $events->listen( + 'connection.*.beganTransaction', + function ($event, $params) { + $this['queries']->collectTransactionEvent('Begin Transaction', $params[0]); } + ); - $queryCollector->collectTransactionEvent('Commit Transaction', $connection); - }); + $events->listen( + 'connection.*.committed', + function ($event, $params) { + $this['queries']->collectTransactionEvent('Commit Transaction', $params[0]); + } + ); - $db->getEventDispatcher()->listen([ - \Illuminate\Database\Events\TransactionRolledBack::class, + $events->listen( 'connection.*.rollingBack', - ], function ($transaction) use ($queryCollector) { - - if($transaction instanceof \Illuminate\Database\Events\TransactionRolledBack) { - $connection = $transaction->connection; - } else { - $connection = $transaction; + function ($event, $params) { + $this['queries']->collectTransactionEvent('Rollback Transaction', $params[0]); } + ); - $queryCollector->collectTransactionEvent('Rollback Transaction', $connection); - }); - } catch (\Exception $e) { - $this->addThrowable( - new Exception( - 'Cannot add listen transactions to Queries for Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) + $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('models', true) && $events) { + try { + $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->addCollectorException('Cannot add Models Collector', $e); } } - if ($this->shouldCollect('models', false)) { + if ($this->shouldCollect('livewire', true) && $app->bound('livewire')) { try { - $modelsCollector = $this->app->make('Barryvdh\Debugbar\DataCollector\ModelsCollector'); - $this->addCollector($modelsCollector); - } catch (\Exception $e){ - // No Models collector + $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')) { + if ($this->shouldCollect('mail', true) && class_exists('Illuminate\Mail\MailServiceProvider') && $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)); + $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(); } - } catch (\Exception $e) { - $this->addThrowable( - new Exception( - 'Cannot add MailCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) - ); + + 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->addThrowable( - 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)) { $this->addCollector(new FilesCollector($app)); } - if ($this->shouldCollect('auth', false)) { - try { - $guards = $this->app['config']->get('auth.guards', []); - $authCollector = new MultiAuthCollector($app['auth'], $guards); - - $authCollector->setShowName( - $this->app['config']->get('debugbar.options.auth.show_name') - ); - $this->addCollector($authCollector); - } catch (\Exception $e) { - $this->addThrowable( - new Exception( - 'Cannot add AuthCollector to Laravel Debugbar: ' . $e->getMessage(), $e->getCode(), $e - ) - ); - } - } + if ($this->shouldCollect('auth', false)) { + try { + $guards = $config->get('auth.guards', []); + $this->addCollector(new MultiAuthCollector($app['auth'], $guards)); + + $this['auth']->setShowName( + $config->get('debugbar.options.auth.show_name') + ); + $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) && isset($this->app['events'])) { + if ($this->shouldCollect('cache', false) && $events) { try { - $collectValues = $this->app['config']->get('debugbar.options.cache.values', true); - $startTime = $this->app['request']->server('REQUEST_TIME_FLOAT'); - $cacheCollector = new CacheCollector($startTime, $collectValues); - $this->addCollector($cacheCollector); - $this->app['events']->subscribe($cacheCollector); - - } catch (\Exception $e) { - $this->addThrowable( - new Exception( - 'Cannot add CacheCollector to Laravel Debugbar: ' . $e->getMessage(), - $e->getCode(), - $e - ) - ); + $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; } @@ -523,6 +655,12 @@ public function addCollector(DataCollectorInterface $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; } @@ -539,11 +677,20 @@ public function addCollector(DataCollectorInterface $collector) */ public function handleError($level, $message, $file = '', $line = 0, $context = []) { - if (error_reporting() & $level) { - throw new \ErrorException($message, 0, $level, $file, $line); - } else { - $this->addMessage($message, 'deprecation'); + 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); } /** @@ -551,13 +698,15 @@ public function handleError($level, $message, $file = '', $line = 0, $context = * * @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); } } @@ -573,7 +722,7 @@ public function stopMeasure($name) $collector = $this->getCollector('time'); try { $collector->stopMeasure($name); - } catch (\Exception $e) { + } catch (Exception $e) { // $this->addThrowable($e); } } @@ -593,7 +742,7 @@ public function addException(Exception $e) /** * Adds an exception to be profiled in the debug bar * - * @param Exception $e + * @param Throwable $e */ public function addThrowable($e) { @@ -604,11 +753,28 @@ public function 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) @@ -628,11 +794,21 @@ public function getJavascriptRenderer($baseUrl = null, $basePath = null) */ public function modifyResponse(Request $request, Response $response) { + /** @var Application $app */ $app = $this->app; - if (!$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->addThrowable($response->exception); @@ -643,115 +819,101 @@ public function modifyResponse(Request $request, Response $response) $configCollector = new ConfigCollector(); $configCollector->setData($app['config']->all()); $this->addCollector($configCollector); - } catch (\Exception $e) { - $this->addThrowable( - 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->addThrowable( - 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 RequestCollector($request, $response, $sessionManager, $this->getCurrentRequestId())); - } catch (\Exception $e) { - $this->addThrowable( - 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->addThrowable( - 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) { + } 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); - if ($app['config']->get('debugbar.add_ajax_timing', false)) { - $this->addServerTimingHeaders($response); - } + return $response; + } - } 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' - || $response->getContent() === false - || $this->isJsonRequest($request) + 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()); } } - - return $response; } @@ -762,6 +924,7 @@ public function modifyResponse(Request $request, Response $response) public function isEnabled() { if ($this->enabled === null) { + /** @var \Illuminate\Config\Repository $config */ $config = $this->app['config']; $configEnabled = value($config->get('debugbar.enabled')); @@ -782,23 +945,38 @@ public function isEnabled() */ protected function isDebugbarRequest() { - return $this->app['request']->segment(1) == $this->app['config']->get('debugbar.route_prefix'); + 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; } /** @@ -851,26 +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; } /** @@ -887,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); } } @@ -902,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; } /** @@ -968,7 +1205,7 @@ public function __call($method, $args) { $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); } } @@ -1013,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'); @@ -1036,6 +1274,11 @@ protected function selectStorage(DebugBar $debugbar) $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'); @@ -1051,8 +1294,8 @@ 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); } /** @@ -1067,11 +1310,37 @@ protected function addServerTimingHeaders(Response $response) $collector = $this->getCollector('time'); $headers = []; - foreach ($collector->collect()['measures'] as $k => $m) { - $headers[] = sprintf('%d=%F; "%s"', $k, $m['duration'] * 1000, str_replace('"', "'", $m['label'])); + 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 a3542753d..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. * diff --git a/src/Middleware/DebugbarEnabled.php b/src/Middleware/DebugbarEnabled.php index 9e10a7664..146ce2d18 100644 --- a/src/Middleware/DebugbarEnabled.php +++ b/src/Middleware/DebugbarEnabled.php @@ -1,4 +1,6 @@ -handleException($request, $e); - } catch (Error $error) { - $e = new FatalThrowableError($error); + } catch (Throwable $e) { $response = $this->handleException($request, $e); } @@ -74,7 +72,6 @@ public function handle($request, Closure $next) $this->debugbar->modifyResponse($request, $response); return $response; - } /** @@ -83,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; diff --git a/src/Resources/cache/widget.js b/src/Resources/cache/widget.js index ad7d967fe..5635363e6 100644 --- a/src/Resources/cache/widget.js +++ b/src/Resources/cache/widget.js @@ -1,4 +1,4 @@ -(function($) { +(function ($) { var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); @@ -14,39 +14,42 @@ className: csscls('timeline cache'), - onForgetClick: function(e, el) { + onForgetClick: function (e, el) { e.stopPropagation(); $.ajax({ url: $(el).attr("data-url"), type: 'DELETE', - success: function(result) { + success: function (result) { $(el).fadeOut(200); } }); }, - render: function() { + render: function () { LaravelCacheWidget.__super__.render.apply(this); - this.bindAttr('data', function(data) { + this.bindAttr('data', function (data) { if (data.measures) { var self = this; - var lines = this.$el.find('.'+csscls('measure')); + 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); }) + .one('click', function (e) { + self.onForgetClick(e, this); }) .appendTo(m); } } diff --git a/src/Resources/laravel-debugbar.css b/src/Resources/laravel-debugbar.css index 3c073a701..4a33ba8a8 100644 --- a/src/Resources/laravel-debugbar.css +++ b/src/Resources/laravel-debugbar.css @@ -1,67 +1,188 @@ +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; - direction: ltr; - text-align: left; - z-index: 9999999999; + 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 code, div.phpdebugbar pre, div.phpdebugbar samp { - background: none; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 1em; - border: 0; - padding: 0; +div.phpdebugbar .phpdebugbar-widgets-messages .hljs > code { + padding-bottom: 3px; } div.phpdebugbar code, div.phpdebugbar pre { - color: #000; + color: var(--debugbar-text); +} + +div.phpdebugbar-widgets-exceptions .phpdebugbar-widgets-filename { + margin-top: 4px; +} + +div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item pre.phpdebugbar-widgets-file[style="display: block;"] ~ div { + display: block; } div.phpdebugbar pre.sf-dump { - color: #a0a000; - outline: 0; + color: var(--debugbar-text); + outline: none; + padding-left: 0px; } div.phpdebugbar-body { - border-top: none; + border-top: 1px solid var(--debugbar-header-border); } -div.phpdebugbar-header { - min-height: 30px; - line-height: 20px; - padding-left: 39px; +div.phpdebugbar-header span.phpdebugbar-text, div.phpdebugbar-header > div > span > span, div.phpdebugbar-header > div > span > i{ + display: inline-block; } -div.phpdebugbar-header, -a.phpdebugbar-restore-btn, -div.phpdebugbar-openhandler .phpdebugbar-openhandler-header { - background: #f5f5f5 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIcAAACHCAYAAAA850oKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAmVAAAJlQBMwDy4QAAABJ0RVh0U29mdHdhcmUAZXpnaWYuY29toMOzWAAAGE5JREFUeJztnXt8VOWZx7/PyQURrK3YukUkZwJycXIGMF6KWqVbrNsLdq3A9qLW3W2hVatowUQEQUFNBNvS7dpCrb34sVtFe9N22zWraNeiSETmZOSazJmIvWxFViEqw+Q8+8dMQiDnTCbJzGQS5vtn5jlnnsz85pznvO9zEfKAVleXcfCNryB6DXAiog+hepfYrfvy8f5F+obk+g3UqpwJ7reA4FEv7QPq2c8acZx3c+1Hkd6TM3FosPJsDHc1cGEPpq2ILCUcfVBAc+VPkd6TdXFosHIsJe0rUbmil+d/EWSh2NFns+1Tkb6RNXFocMxJGGW3gl4HlPuYbUH0NVQ+lcajn2MYtfJyy65s+Vakbxj9PYFWV5epFZiHUbod9Ca8hfEayHwmOWdLODYLYTrwR+8T8hna3Yha5loNjftAf/0r0nf6deXQUGAWqt8ExvmYtCGsZkSiXjbueeeIY0GwArNB64BKn3c4gMi9vEVdMWjNP30Sh4bMc3F1NSIX+FgkQB7AZZlEnL+kPVcwWE5J21dRlgPv9bHaA8YK7Oj9Am5ffC7Se3olDp1aeTrt7p3AbP9jtQEpvVHCzU29OndwzEkYpTcDC4BhPu42orpQmpwNvTl3kb6RkTgy++J0MyqL+vvF6eRABWXuivRPO9oAJTeJ3WL3572KpCetODK75PMqyMpsX/I1ZJ6Lsgr4sI9FxreuIn3DUxxdgsV6IOB9aH6CxYyCXvgO7oiVEokcyJUfxyLdxKFWxXSQ1cB5PsccAn6IlCyVcPP/5tS7Dp+qq8uIv/HPoCuB9/uYvQZyB5OiP5D1tOfDr6FOpzg0FJiI6gpgjr+1PkF76Y0Sad6dD+eORq2x7wOjBrgBOM7H7BVUb5am2G/y6NqQRLS68kQOuXWofgmk1MduY/IpIea9cJVnNFQRALkbZS7+cdN/4pZcP1BCzgY6hxK2VVwNXI1IHOH7hJ2H87UHJVpl/g7hEp/Xm0FuwY4+WoibYj2vt/BnyuPjpfFPb+fVsSygVsXHgXtAqo58gU0YslDC0T/k2gdRy0wAJR7u7UaNC6Qp+tdcO9EfNBgsx2j7BfAJTwPXOEciLS/m16u+o0FzKoauApnZg+kvMIxa2dqyM1e+GHgKA0DGI7pNLbNGTdPv/j6gaCgwC+NABD9hABjtfrfKgkKnjDlVLXMtBpszEAbAZbhuk1rmWq0KnJILn0QtM5PbRQsqt9AUXV8It5cMnqi6Wp8ndmxjzp3qIzpx4gmUxWsRXQAc72P2CjAc32UF3gTqGJlYc/QeVn/IdFe2EtGHscwX1Ar0lLyTMzRYOVZDFT8BeY6MhFG46AxK1QrMo/zgTkQX4yUM1dcRFjDKmUL5qIkg84G/eZzuROBuDpTsVCswT+f43Q16R/crh+itqJwLXOp7lMpjlMot+cq5SC3fLwWuIV2uiMo3EH3wqKML7sqhocClqNYDk3xM3gb9Bu7I+qMX9lKfxRLgWvw/i5dwdaFEYk/3x0+vK8cOsZ1PI0xH5DnPo0Qvz0fOxZG5Iiygh1wRyhJP5cqXbKDW2Gq1zKdR/RXewnCB9STkDLFjS71WfCWy5w2xnZtwjdNTPwSv2/yZGPKUWhVPalUg1Fd/fW8rEnaeJxz9MMhcoMXDpAyYhyaaNWQuz2bQqiBqBeYQ37sNdC2eq6J6AOF2RiZOFzu6rpBXRTU0foxa5lowNgEzfKwacKkW25kr26Kxns4pkZZWCceuwjXOBXxSK2Umols0VPETDZp/11u/08YcAip2dD3uiMkg81F93cNqJMoyTtBdagXmaT+zyzRknkso8AfQR/DcT9EEsA41xkvYWZ7NACzbaDA4UkPmcjSxC5iH92ezDWSu2LGLJeK83Nv3kEjLi2I7FyFyKajXgp+BypUY7FbLrNOJE0/I9NwZfZESicTFjq5D2ycC9cBBD6sxoGuxApu0ypyRqQMd6JTKCWqZj6BsRPV8H6sGpHSa2M78Ql5/OXw7bGtGWYb3Uv+fUrdDS+zo+v6+p4Sjj1N+8hmpoNVrz2sEUEP5we2ZBq1eAelsCcceS3eQVp02DjHqQC7Hf/n610CN2M72tOeqnnAy8YPLUv9UmY9ZRpnpOu200SRKXjvqr3kNSDVUcTmu3I1wuo/FAZBVyDv3SvivbTnxYar5XhK6GJGv4b8HFU7m30T/y+88fRJHpxNTAufg6mr6kHORi1yRgRRHBnU6LqIPcYga2Rb7c679AdCqcach7UuAL+F7l9AGXFnkdUvrV3wgW6ObxHYu9L/fSSkwr/N+FwyO7Aw2jbbtKN/CUxipYHM/E8SOrivkvNHOtRfDfQFfYWgDKtMkHLsqX8IAkKbmV8V25qPGucAGH6uZGDSqZT6ikwMVR7zSnytHV1JXgmtQlgIn+VjtAfkbMM3nNHGU75I4tEK2v7a31z7k8cqh1ZUnEneXkX69YQviLpJw639n+/37glrmp0nGjBN9TNqAexmZqJONe97pd91KBxKJxCXsfAvc8Yjci2/Q6ikMBX0UtyQoTc6CvggjnygI8fYG4Ea8hfEqIl/Eds4qFGEAiO38ilFOFaLX4h+03saB0scgC0VN3R1o3Sfh6EJEJyM8TM97MRtRvUDs2JxBk3thjQ2AnOXz6rOUxydJOPqTQrwdygYSEo7dB+4kIOxj9nENnTIi6+LodCIci0rY+WxqkeYZD5PW1K/r/EJJIsocw+82AnAh8bJn1DIvyps7vSTZ+cDYAPivnuqw8pyJowOJtLzIO6Xd92lUvpz6dQ34Lm/2kbOADWpVPKmhcVU9mucJDY49Qy3zN+A+STphpMi5OI5tZCba/lIucy4y4XCuiBEmXe7LURTFkT2WAVGPvyf3oER3qGXW6vQxw/PlkI4f/x6tCtyJW9qxfO+1KvoKUOd1fFEc2cJwHxuInAsvOnNFhid2pHJFuguya66I6JNe5ymKI4tIY+MhsaPrwO3Yg/Io9krtQW03N2mw4iPZ9kGtypnsNV9K7WZ77cS+DdTzbtk4CTtrZAMJv3MVxZEDxG7dJ7ZTi2tMzHXORQeduSLJYNPyMOmSK+LUyu7db/V0zqI4ckg+ci5ykSvSQVEceeBwzoXOTp9zobvUCizR6tF+icad6FTzvWqZ9T3kioRRuaSvuSJFceQRCcceS59zISNBVxAv3+UXtHbmirSzA7iZ9LkiZ6bbku+JojjyTGfQ6o4Yh3A7nkEro1NBq61VlZ3N9TQUmMXBvZFUsOmVu9sG1BMfNikbqZODouBnKJJKHl6uU80f0c6dwOfonjg1GXEf1yrz9xgyEtXzfZpm5KRXSfHKMcDIy44jtvMF1DgHv5wL4ZK0qZNqVIvtzM92E5uiOAoEaWrZLLbzETAuBs2kn1oE9JNixy6Wpqjf7mq/KIqjwBC7pYFRsWmpoNUribqjTmeK2LHf5tKXYsxRgCRXLaPr1KqwQY5KZ3AvFLu1hTy0yiteOQoZt6T70rZb6rvcnW2K4ijiS1EcRXwpiqOIL0VxFPGlKI4ivhTFUcSXojiK+FIURxFfiuIo4ktRHH3C6N5HJOHxt8GCiKfvRXH0AgVDQ4GrwO2eXWXQoJZZo8FgulLJgkKDwXINmTfg8jOv14viyBANBWZhmU2o/hjvlP9RQB1G29ZUq4OCJdkjpeILlLTt9OmR0kb5+w8Ud2V7QK2x1YixCtVMa0wmAb9Uy3wG11hUaH3XtcqcgegqkLN8qpTjoPOlsfFQ8crhgwYrx6pV8SAYm1D8hPEkyu99XrsIw31BLfMhnWqaOXIzY9QyJ2mV+WuEp33aRxzukWLHHoLibaUbne0hDXc7yBV4f0bbk+0hnY9Jk/MPYFyM4JX6L8DnaWeHhsw1OtX0632WM3TSqaPUMuuArQizvI3YhMhFR/dIKYojRZf2kLtT7SHT1Zce0R5S7JYGwk41yFwEr6KhcpTraadZLbNGx4/3mbCZPXT6mOFqmTWUlTUDNXh3IEr2SGlyPuQ1v6UoDkCrKi4jvrcplfLv1SqhDWU5xrumX32pgCt2dD1l8TNAbku2lOzGSUAdww81aSgwW/s5EdzzfwFDLfNqDpTsJFk9f6KH2RsIN+KOOD1dj5RjOiDVqsqzEHc14NeFp9ftIVNToVZo9YTvcii+FNVruo9Hk/GorqfK3KRZnLqkobEfRY1VwDQf3SWHN5aWLpEtu706ARzBMSmOw/053R76cxoLJeJs7ct7SOPO14EbNBS4z3ewonAOqs9qqKJfgxXVMicBd6C+wxsVeBRtv0WaXm3O9LzHlDgOT5dsz9t0SQlHdwBzNVTx96isAs7s7ph8CqP9ErXMjH/VkOr+7Ht16uR5DGORbG35n976fkyIo8tc2hV4lxFCjufSSjj2lMJZaYY5JzsAJRJz1DLr2c8av3Np9ejjiZd/jXh8MfAen1vITpAl/RneeAwEpHJpl9EcXsLYj+gSyuMTcj2ao3MKxchEEGUx4NUj431AHSfwCiXuZd1eNfSLxMt3kww23+Nx/N9Ar6N8VJXY/Ru7dixcOWp9/p4MNtvlZonEslpG2BOpMSB366RT11FWtgjvZrcBlFu6H613+Jw2jvA9yozbpLHlTTyfqHvHsSAOD7QBSm6ScEseSoP8SXVqrtUplQ/guiuB2fT+8TYZbIrWSDjm1bCuzwwZcWj1hJM5GF+S/qOVxtRk7Q15cisjUrNh5yaHK+pq4OzMDuRp0IUSjr2UC78GvTjUNI/jBK4nHr8F8R3N0Qq6GNv5aSE3xRU7+qxONT9Guz6Zpn02wCuoUSN2yxO59GfQikNBsCo+h3AXSoWP2ZvA3exnjTgxryYpBYMGg+VI21dI6FJETk5j+XVGxb6drgtgthiU4kj2FdfVabadD4F+j/Jhd6QWowqWpMgDs6HtbmBcjyGHW/JoPoQBg0wcGgpMPLza6PchagOGLpCtrZF8+tYXNGR+CFdXgV4w0L54MSjEoZNOHUVZ2SJU/eabALwILBI75jWhoaDoFLkyGxEflWsDys8QuT+/3h2moMWh08cM50Dp9cAteO8uQnLwzRLC0QcLOdiErsvdbprlbt2My80SiT2twcqzkYEb2VKQ4lAwCJlXckBXAmN8zPaBruSdsn+X3bs9pkIVDho6ZQQ6/OvEDy5KtpP0vFg4CIsJx35WKCIvOHF0bjtrdradB5KkyANXoFoHfNDn/9kH1CefqJyCeqIqGHFksO0Mok/gugt6s+08UCSnIbn3ourX1zwO/KiQRT7g4tDJFR+klNuBf8F7HgiIPJdaCXw+r871gdR82XvAneFnAvwHJdwqLztO/jzrPQMoDneEWuZtoKn7sAfKLtBasZ2f59m5XqOhigCu3IW4/4Tfc7bwNOouEru1Mb/e9Y2BE4fIT4HjPD9H1ddBVjBs1HelsfFQ3n3rBRoccxJG2a2oXovglzgcAb1ZwrltDZltBvK24pWJldx2LpFl8rLzf+Dk26eM0WCwHOPtq0FXgr7fx+xPILczKvpAvlY1s8mAxxwpktvOuLUSbm0ZaGfS0WW5uw6o9DFrA75DfNidsmPH/jy6l1XyI46SEsX/h7MDNa6QppbNefGlH6Qes+8B7Z4HmuQQKt/HMG6XcLPXxOdBRV7SBGXHjv0+xT4AE0BrdUrlhHz40hc0NK5KLfO3qNGAV4Jwkl8gYklT9NqhIAzIZw6p6GXg2ZRZEL0c123SKvO+gZy/ejQ6ZcypGgrcj7a/DHzcx2wjrnuB2M5nUpnmQ4a8iUO2xrZgO1NB5uI3f1X4KqLNapl1On68V/JsXtDQKSPUMmtwS7eh+q94rb8Isc5x65HW5/LvZe7Ja/Z5Z8mgO2ISwgLg/zzMRgA1HHeoOd/NUDrrZXV4R3b3CR5me4Fa3i6dOHTHrScZkNIEiUTiEnbW4CbG4Td/NZkNVYfRZqsVmJOLutKuqFU5k/jr6eaxvgPUU26ME9upL/TNvmwwoHUrEtnzhthOLVoyAVgHnjUjE0Afocp8PpkBll10SuActcxnkvNYpcrDJDmPtYTkPNbGljez7UOhUhBFTdLU/KrYznwMdwqi3kmzwjnAhqwN6Z1aebpa5iO4+jxwoY/V4XmsBb4PkgsKQhwdyNbWiIRjs5KjrNjiY3V4SO+000b39j06m5m0uzbJ4mav21UE1U/1dR7rUKGgxNGB2C0N2KlmKOC1Ypoc0pso2aWWWafVlX5ZYp1o9ejjj2pm4rEPons6R2RlqZB6MFOQ4oAudaXuiMmpeWdeOQ/HAzXEXd+OOclmJoE5xMsj+DYz0QMItzOyPef1soOJghVHBxKJxMWOrgN3Isknm3c8zJJtHo9P7NBQ4KqOJxu1KmcSMhtBHwFMj+MOAeuQ0nESdpanaliLpCiUjbceEbt1H1Crwcr7EHcFQvdmbkoF6I+pMq9V0bfAnemzCqEIj6DuYrELe6NvIOl+5XDFb/u5IJBIS6s0OV9EZRrwO28jzgGZ6XOKZzHkQxJ2PlvwwjDaB/S7ELXMo39bivIwhi7OdtV2LkjulJbUg1b3YLoNkVoJR3+dF8f6gYbGfQC3fTmiX+5WwuAaFRJpac2HH6KW2QQEPV47iOi/oXpX6pJesCgIIfOzKHfSvWPOX4DljHJ+UOgJN1o9+ngOld2ISg3eS/f72c8H8pWlLmqNrQb5JYhffcgbCCtoH3GfRCLxfDjVVzRYORbDPTI1QPV8aYr90eeQgkDBwDKvSralSvM9GHxetjp+HZOzjiF2ayMj2yeQ7IDjtTR8Eso3Mdp2dX0SKEhK490fQY0BLBnLAA2N/SiWuRn4oY8wkk9UpaWT8ikMOOqL7qxJ9W5DlDJiE1nsnZlNdMqYU3FL9xzxR2G6hJ2CK2norNPxakGZMqEP7SGzyRFPK7L9tb1iO7WIhID1nkcc7p35uAbHjc+Hk0MJrZ5wsobMNaAdy/devIBhXCi2M3cgC7jS3iLUqpgOsho4z8ckWZooJUsLITWukK8cne0hIdUe0suIXYjc2p/2kNkk7Qqp2LGN2M4FabO3YB6aaNaQuVxN06/x6zFL53Sn9O0h3wBqebfU6m97yGzS4/J5lz2ONNlbMhJlGSewU63APB0Ey/L5QK3KmVjmS6npTh/0MIkjfJsSCjKBqNdPHskKr9KbgQV47mwC6GaQhWI7eW2kUii3FQ2OPQOj5B7QT/qZkKP2kNmk17/wzuytEsMiGbR6XALlLDoSc0LjvLKrhiQ6ZcypaplrMYxwGmFsBD1fbGduIQsD+nH5l5dbdontzEWYjqpP03WZiSa2qGWuLaSSg2xzRLY6zMO7W8CO1HSn88SObcyzi32i37GBhJ0XpCn2YUQuBTweu6QUmIe4uzVkLtfpY7pPQBqk6AxKe8xWPzzdqarrdKfBQNYCRwlHH6d8VJrEnFTQeqB0l1qBeTrHpxfHIEGtypnsNdNlq78N1PNu2Ti/6U6FTk6Wwg/PNSHdXJNkWwI7e20J8hGQHh4l6jsx0gUeIyGLZFu0/93pB5CcPHKK3bpPbKcW15iI6IN4P7cHQX6TrWzyXKOh8WPUMtemHyXaJVt9kAsDcrweIZGWVgnHrsI1zgWe9bE6nE0+ucJrLWBA6RwlqomdJINNr89sWzLYHFrZ6vmpso+0vCi2cxEilyZbOXn4oXIlpZLMJp840SuXIa9kNEo02ZxlPpOOHCU6VMhvrWw4+jjDRgVTQavXXkyyTrb84PaBDFqTpZF7t5BulCjUEx82aShnqw9YboYGgyMpaVuIUoN/0LoNZFmmv8r+BqSpToCr8a2A6/0o0cHMgCfuaGj8GDSxFOhhjKcs6ul+3ldxaLByLCXtK1G5At/PRBtQ4+vSFA2nO9dQYsDF0UEyXdFYDczwMenxEbG34hioR+7BQsHsnord2ii285FUnaxXByADmEOpvtLf5i6dwSbGDvC9rb3WWRp5DAoDCkgcHYjd0sAo58xU0Oo1tTFZAnncoWYNmTfojN4VZmkoMKvLKFGvupC2ZGlk4vShHGxmQsHcVrxITRu4DnSJb5dj2A5ym9jR9eluKxoyz8XV1Yj4DL7RBMgDuCyTiJPXUaKFSkGLo4NU0LoSuBL/q91TqPFNxH38yIPlKgz9R5TPpHmLXyFSM9QavvWXQSGODpJJNMYq4BNZOuNmVBZJk7MhO+cbWgwqcXTQOa4C+ron8yrISuzo/ZJ8CiriwaAUB3gNusnoqAOI3Mtb1BXa4JtCZNCKo4PDQSu34l1fCgVWQjFYGPTi6ECnnTaaRMky4MimsqJP0F56o0Sadw+Yc4OUISOODtSqtMC9DihH9fuFXkRdpEiRIkOL/wek5t5E7P42/AAAADV0RVh0Q29tbWVudABDb252ZXJ0ZWQgd2l0aCBlemdpZi5jb20gU1ZHIHRvIFBORyBjb252ZXJ0ZXIsKeMjAAAAAElFTkSuQmCC) no-repeat 5px 3px; +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; } -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-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; } -a.phpdebugbar-open-btn { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAOCAYAAADJ7fe0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElNRQfdCgYULwwNKp3GAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4wLjOM5pdQAAAA1UlEQVQ4T2OgKpCUlOQH4vdA/B8Jv4dKEwYgDdLS0v8NDQ3/GxsbwzGIj2YoGEO1oQJkjcRgqDZUAJKwsrJ6/v//fwdiMFQbKgAZkpGR0QR0ajy60wlgRJhBXSGhpqb2CNnZhHBkZORcqBEMDFBX2BsYGGBVjAv39vZaQ41gYIC6Ygs2hbiwr6/vdqA+DqgR4CiW19bWxqoYF87Ly4uFaocAZWXlydgU4sJ2dna3ga4QgGqHAC0trY/YFOPCKSkpDVCtCAA01QaIsaYJHFgCqpVagIEBACGlF2c3r4ViAAAAAElFTkSuQmCC) no-repeat 8px 6px; +div.phpdebugbar-openhandler .phpdebugbar-openhandler-header a { + display: flex; + cursor: pointer; } - div.phpdebugbar-header, div.phpdebugbar-openhandler-header { background-size: 21px auto; @@ -69,148 +190,222 @@ 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: 15px 10px; + padding: 7px 10px; border: none; font-family: inherit; overflow: visible; - display: flex; - flex-wrap: wrap; +} + +ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-sql { + 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; +} + +.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 { @@ -220,68 +415,154 @@ ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item { ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-sql { flex: 1; margin-right: 5px; - cursor: text; + max-width: 100%; } -ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-duration { - /*flex: 0 0 auto;*/ +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 { - /*flex: 0 0 auto;*/ - margin-left: auto; - margin-right: 5px; +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-params { - background-color: rgba(255, 255, 255, .5); - flex: 1 1 auto; - margin: 10px 100% 10px 0; - max-width: 100%; +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:nth-child(even) { - background-color: #f9f9f9; +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-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-error:before { - font-size: 12px; - color: #e74c3c; +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-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-warning:before { - font-size: 12px; - color: #f1c40f; +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; +} + +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; } -.phpdebugbar-widgets-value.phpdebugbar-widgets-warning { - color: #f1c40f; +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-sqlqueries { - line-height: 20px; +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-widgets-sqlqueries .phpdebugbar-widgets-status { - background: none !important; - font-family: inherit !important; - font-weight: 400 !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: 5px 10px; + 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 { @@ -294,26 +575,191 @@ div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugb 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-list li.phpdebugbar-widgets-table-list-item { - /*padding: 5px 10px;*/ -} - -.phpdebugbar-text-muted { - color: #888; -} ul.phpdebugbar-widgets-cache a.phpdebugbar-widgets-forget { float: right; font-size: 12px; padding: 0 4px; - background: #f4645f; + background: var(--debugbar-red-vivid); margin: 0 2px; border-radius: 4px; color: #fff; text-decoration: none; - line-height: 1.5rem; + 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; +} + +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; + padding: 2px 4px; + background: #737373; + margin-left: 6px; + border-radius: 4px; + color: #fff !important; +} + +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; +} + +div.phpdebugbar-widgets-sqlqueries li.phpdebugbar-widgets-list-item.phpdebugbar-widgets-expandable { + cursor: pointer; +} + +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 = $('