Repository: fruitcake/laravel-debugbar Branch: master Commit: dc1ef5dc7a54 Files: 152 Total size: 562.9 KB Directory structure: gitextract_327ueo68/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── release-drafter.yml │ ├── stale.yml │ └── workflows/ │ ├── build-docs.yml │ ├── fix-code-style.yml │ ├── integration-tests.yml │ ├── livewire-tests.yml │ ├── release-drafter.yml │ ├── static-analysis.yml │ ├── unit-tests.yml │ └── update-changelog.yaml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── SECURITY.md ├── UPGRADE.md ├── build/ │ ├── build-docs.php │ └── build-icons.js ├── composer.json ├── config/ │ └── debugbar.php ├── database/ │ └── migrations/ │ └── 2014_12_01_120000_create_phpdebugbar_storage_table.php ├── docs/ │ ├── CNAME │ ├── assets/ │ │ └── extra.css │ ├── collectors.md │ ├── features.md │ ├── index.md │ ├── installation.md │ ├── overrides/ │ │ ├── main.html │ │ └── shortcodes.py │ └── usage.md ├── eslint.config.js ├── mkdocs.yml ├── package.json ├── phpstan.neon ├── phpunit.xml.dist ├── pint.json ├── readme.md ├── resources/ │ ├── cache/ │ │ └── widget.js │ ├── laravel-debugbar.css │ ├── laravel-icons.css │ └── queries/ │ └── widget.js ├── src/ │ ├── CollectorProviders/ │ │ ├── AbstractCollectorProvider.php │ │ ├── AuthCollectorProvider.php │ │ ├── CacheCollectorProvider.php │ │ ├── ConfigCollectorProvider.php │ │ ├── DatabaseCollectorProvider.php │ │ ├── EventsCollectorCollectorProvider.php │ │ ├── ExceptionsCollectorProvider.php │ │ ├── GateCollectorProvider.php │ │ ├── HttpClientCollectorProvider.php │ │ ├── InertiaCollectorProvider.php │ │ ├── JobsCollectorProvider.php │ │ ├── LaravelCollectorProvider.php │ │ ├── LivewireCollectorProvider.php │ │ ├── LogCollectorProvider.php │ │ ├── LogsCollectorProvider.php │ │ ├── MailCollectorProvider.php │ │ ├── MemoryCollectorProvider.php │ │ ├── MessagesCollectorProvider.php │ │ ├── ModelsCollectorProvider.php │ │ ├── PennantCollectorProvider.php │ │ ├── PhpInfoCollectorProvider.php │ │ ├── RequestCollectorProvider.php │ │ ├── RouteCollectorProvider.php │ │ ├── SessionCollectorProvider.php │ │ ├── TimeCollectorProvider.php │ │ └── ViewsCollectorProvider.php │ ├── Console/ │ │ └── ClearCommand.php │ ├── Controllers/ │ │ ├── AssetController.php │ │ ├── CacheController.php │ │ ├── OpenHandlerController.php │ │ ├── QueriesController.php │ │ └── TelescopeController.php │ ├── DataCollector/ │ │ ├── CacheCollector.php │ │ ├── ConfigCollector.php │ │ ├── EventCollector.php │ │ ├── GateCollector.php │ │ ├── HttpClientCollector.php │ │ ├── InertiaCollector.php │ │ ├── LaravelCollector.php │ │ ├── LivewireCollector.php │ │ ├── LogsCollector.php │ │ ├── MultiAuthCollector.php │ │ ├── PennantCollector.php │ │ ├── QueryCollector.php │ │ ├── RequestCollector.php │ │ ├── RouteCollector.php │ │ ├── SessionCollector.php │ │ └── ViewCollector.php │ ├── Facades/ │ │ └── Debugbar.php │ ├── LaravelDebugbar.php │ ├── LaravelHttpDriver.php │ ├── Middleware/ │ │ ├── DebugbarEnabled.php │ │ └── StopRecordingTelescope.php │ ├── Requests/ │ │ ├── AssetRequest.php │ │ ├── CacheDeleteRequest.php │ │ ├── OpenHandlerRequest.php │ │ └── QueriesExplainRequest.php │ ├── ServiceProvider.php │ ├── Support/ │ │ ├── Clockwork/ │ │ │ ├── ClockworkCollector.php │ │ │ └── Converter.php │ │ ├── Explain.php │ │ └── Octane/ │ │ └── ResetDebugbar.php │ ├── Twig/ │ │ └── Extension/ │ │ ├── Debug.php │ │ ├── Dump.php │ │ └── Stopwatch.php │ ├── debugbar-routes.php │ └── helpers.php └── tests/ ├── BrowserTestCase.php ├── Controllers/ │ ├── AssetControllerTest.php │ ├── CacheControllerTest.php │ ├── DebugbarEnabledMiddlewareTest.php │ ├── OpenHandlerControllerTest.php │ └── QueriesControllerTest.php ├── DataCollector/ │ ├── CacheCollectorTest.php │ ├── GateCollectorTest.php │ ├── HttpClientCollectorTest.php │ ├── JobsCollectorTest.php │ ├── Livewire/ │ │ └── DummyComponent.php │ ├── LivewireCollectorTest.php │ ├── MailCollectorTest.php │ ├── ModelsCollectorTest.php │ ├── PennantCollectorTest.php │ ├── QueryCollectorRuntimeDatabaseTest.php │ ├── QueryCollectorTest.php │ ├── RouteCollectorTest.php │ ├── SessionCollectorTest.php │ └── ViewCollectorTest.php ├── DataFormatter/ │ └── QueryFormatterTest.php ├── DebugbarBrowserTest.php ├── DebugbarDocsTest.php ├── DebugbarTest.php ├── ErrorHandlerTest.php ├── Jobs/ │ ├── OrderShipped.php │ └── SendNotification.php ├── LivewireBrowserTest.php ├── Mocks/ │ ├── MockController.php │ ├── MockMiddleware.php │ └── MockViewComponent.php ├── Models/ │ ├── Person.php │ └── User.php ├── Support/ │ └── ExplainTest.php ├── TestCase.php └── resources/ └── views/ ├── ajax.blade.php ├── custom-prototype.blade.php ├── dashboard.blade.php ├── layouts/ │ └── app.blade.php ├── livewire-component.blade.php └── query.blade.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ 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 ================================================ FILE: .gitattributes ================================================ * text=auto /.github export-ignore /build export-ignore /docs export-ignore /tests export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore /CHANGELOG.md export-ignore phpunit.xml.dist export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /.nvmrc export-ignore /eslint.config.js export-ignore /mkdocs.yml export-ignore /package.json export-ignore /package-lock.json export-ignore /pint.json export-ignore /UPGRADE.md export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: barryvdh custom: ['https://fruitcake.nl'] ================================================ FILE: .github/release-drafter.yml ================================================ template: | ## What’s Changed $CHANGES ================================================ FILE: .github/stale.yml ================================================ # 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 ================================================ FILE: .github/workflows/build-docs.yml ================================================ name: Build docs on: workflow_dispatch: push: branches: - master paths: - 'resources/**' - 'docs/**' - 'tests/DebugbarDocsTest.php' permissions: contents: write jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Configure Git Credentials run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.2 coverage: none tools: composer:v2 - name: Install dependencies run: composer update --prefer-dist --no-progress - name: Run docs test run: php vendor/bin/phpunit --filter=testItInjectsOnDocs - name: Run build script run: php build/build-docs.php - uses: actions/setup-python@v6 with: python-version: 3.x - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - uses: actions/cache@v5 with: key: mkdocs-material-${{ env.cache_id }} path: .cache restore-keys: | mkdocs-material- - run: pip install mkdocs-material - run: mkdocs gh-deploy --force ================================================ FILE: .github/workflows/fix-code-style.yml ================================================ name: Fix Code Style on: push: branches: - master paths: - '**.php' pull_request: branches: - "*" paths: - '**.php' permissions: contents: write jobs: cs-fix: runs-on: ubuntu-24.04 timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.4 coverage: none tools: composer:v2 - name: Install dependencies run: composer update --prefer-dist --no-progress - name: Fix Code Style run: vendor/bin/pint - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Fix CS ================================================ FILE: .github/workflows/integration-tests.yml ================================================ name: Integration Tests on: push: branches: - master pull_request: branches: - "*" jobs: php-laravel-integration-tests: runs-on: ubuntu-24.04 timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 strategy: fail-fast: false matrix: php: [8.5, 8.4, 8.3, 8.2] laravel: ['11.*', '12.*', '13.*'] exclude: - laravel: 13.* php: 8.2 name: P${{ matrix.php }} - Laravel${{ matrix.laravel }} steps: - name: Checkout code uses: actions/checkout@v6 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 "fruitcake/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: | echo "Files in sample/storage/debugbar/:" ls -la "sample/storage/debugbar/" FILE_COUNT=$(ls -1q "sample/storage/debugbar/" | wc -l) echo "File count: $FILE_COUNT" if [ "$FILE_COUNT" -gt 0 ]; then echo "ERROR: Expected 0 files but found $FILE_COUNT" echo "File contents:" for f in sample/storage/debugbar/*; do echo "--- $f ---" head -c 500 "$f" echo "" done exit 1 fi ================================================ FILE: .github/workflows/livewire-tests.yml ================================================ name: Livewire Tests on: push: branches: - master pull_request: branches: - * jobs: unit-tests: runs-on: ubuntu-24.04 timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 strategy: fail-fast: false matrix: php: [8.2, '8.3', '8.4', '8.5'] livewire: [^3, ^4] name: Livewire${{ matrix.livewire }} steps: - name: Checkout code uses: actions/checkout@v6 - 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 remove --dev --no-update larastan/larastan phpstan/phpstan-phpunit laravel/pint phpstan/phpstan-strict-rules shipmonk/phpstan-rules composer require "livewire/livewire:${{ matrix.livewire }}" --no-interaction --no-update composer update --prefer-stable --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@v6 if: failure() with: name: screenshots path: tests/Browser/screenshots/* ================================================ FILE: .github/workflows/release-drafter.yml ================================================ 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 }} ================================================ FILE: .github/workflows/static-analysis.yml ================================================ name: Code Analysis on: push: branches: - master pull_request: branches: - "*" jobs: static-analysis: runs-on: ubuntu-24.04 timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 steps: - name: Checkout code uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.4 tools: composer:v2 coverage: none - name: Install dependencies run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Analyse with PHPStan run: vendor/bin/phpstan --no-progress --error-format=github ================================================ FILE: .github/workflows/unit-tests.yml ================================================ name: Unit Tests on: push: branches: - master pull_request: branches: - '*' schedule: - cron: '0 0 * * *' jobs: unit-tests: runs-on: ubuntu-24.04 timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 strategy: fail-fast: false matrix: php: [8.5, 8.4, 8.3, 8.2] laravel: [^11, ^12, ^13] dependency-version: [prefer-stable] exclude: - laravel: ^13 php: 8.2 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} steps: - name: Checkout code uses: actions/checkout@v6 - 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: Remove incompatible dependencies if: matrix.laravel == '^13' run: | composer remove --dev --no-update laravel/octane laravel/pennant - name: Install dependencies run: | composer remove --dev --no-update larastan/larastan phpstan/phpstan-phpunit laravel/pint phpstan/phpstan-strict-rules shipmonk/phpstan-rules 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@v6 if: failure() with: name: screenshots path: tests/Browser/screenshots/* ================================================ FILE: .github/workflows/update-changelog.yaml ================================================ 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@v6 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@v7 with: branch: ${{ github.event.release.target_commitish }} commit_message: Update CHANGELOG file_pattern: CHANGELOG.md ================================================ FILE: .gitignore ================================================ /.idea /vendor composer.phar composer.lock .DS_Store .phpunit* /tests/Browser /node_modules /build/phpstan /build/docs /site /docs/overrides/__pycache__/ /docs/assets/dist ================================================ FILE: .nvmrc ================================================ 24 ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## v4.1.3 - 2026-03-09 ### What's Changed * Optin query result by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1997 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.1.2...v4.1.3 ## v4.1.2 - 2026-03-09 ### What's Changed * Fix sqlite and tweak results by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1996 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.1.1...v4.1.2 ## v4.1.1 - 2026-03-08 ### What's Changed * Catch serialization errors when populating cache stats by @miken32 in https://github.com/fruitcake/laravel-debugbar/pull/1993 * Fix booting on console by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1995 * Add some tests for closures in cache by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1994 ### New Contributors * @miken32 made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1993 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.1.0...v4.1.1 ## v4.1.0 - 2026-03-07 ### Biggest changes - Use JsonVardumper for smaller + more detailed depths - Add option to re-query and show results for SELECT queries - Popup query/explain results - Stricter checks for production env / non-debug mode, early exit ### What's Changed * Fix CSS typo: `border-botton` → `border-bottom` by @Copilot in https://github.com/fruitcake/laravel-debugbar/pull/1975 * Bump minimatch by @dependabot[bot] in https://github.com/fruitcake/laravel-debugbar/pull/1973 * Cast database name to string in QueryCollector for null values by @b8ne in https://github.com/fruitcake/laravel-debugbar/pull/1979 * Check privateIp instead of localhost range by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1977 * Fix typo on comment by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1978 * Add button to show query results by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1976 * Check for json formatter by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1980 * Add masked keys to ConfigCollector by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1981 * Include cached items info on laravel tooltip by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1982 * Only allow explain etc on local ip by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1983 * use json formatter by default by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1984 * Tweak open storage / query explain checks by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1985 * Easy collector getter from `debugbar()` helper by @ssw1cblarrion in https://github.com/fruitcake/laravel-debugbar/pull/1989 * [QueryCollector] Support file info on addMessage method by @ssw1cblarrion in https://github.com/fruitcake/laravel-debugbar/pull/1988 * Use highlight on popup title for query result/explain by @parallels999 in https://github.com/fruitcake/laravel-debugbar/pull/1986 * Quick fixes by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1990 * Tweak boot check by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1991 * Tweak controllers by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1992 ### New Contributors * @Copilot made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1975 * @b8ne made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1979 * @ssw1cblarrion made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1989 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.10...v4.1.0 ## v4.0.10 - 2026-02-26 ### What's Changed * Cast LARAVEL_START const to float by @daniser in https://github.com/fruitcake/laravel-debugbar/pull/1968 * Laravel 13.x Compatibility by @laravel-shift in https://github.com/fruitcake/laravel-debugbar/pull/1969 * GateCollector backtrace need more steps by @angeljqv in https://github.com/fruitcake/laravel-debugbar/pull/1972 * Support custom messages on QueryCollector by @angeljqv in https://github.com/fruitcake/laravel-debugbar/pull/1970 * Use upstream collect by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1960 ### New Contributors * @daniser made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1968 * @laravel-shift made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1969 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.9...v4.0.10 ## v4.0.9 - 2026-02-17 ### What's Changed * Check mail is started by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1967 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.8...v4.0.9 ## v4.0.8 - 2026-02-14 ### What's Changed * Use message id instead of subject for mail collector by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1965 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.7...v4.0.8 ## v4.0.7 - 2026-02-06 ### What's Changed * chore: replace deprecated Request::get() with Request::input() by @calebdw in https://github.com/fruitcake/laravel-debugbar/pull/1957 * Fix utf-8 encoding by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1959 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.6...v4.0.7 ## v4.0.6 - 2026-02-04 ### What's Changed * Use Dispatcher contract by @bytestream in https://github.com/fruitcake/laravel-debugbar/pull/1954 * fix: urlencode cache key by @calebdw in https://github.com/fruitcake/laravel-debugbar/pull/1955 * Handle missing bindings in SQL formatting by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1956 ### New Contributors * @bytestream made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1954 * @calebdw made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1955 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.5...v4.0.6 ## v4.0.5 - 2026-01-29 ### What's Changed * Show params table for explain button by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1949 * Change bindings return value on limited by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1948 * use GateEvaluated event by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1951 * Fix livewire deprecations errors by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1952 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.4...v4.0.5 ## v4.0.4 - 2026-01-29 ### What's Changed * Bump workflows actions by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1946 * Allow ability parameter to accept integer type by @jeffersongoncalves in https://github.com/fruitcake/laravel-debugbar/pull/1947 ### New Contributors * @jeffersongoncalves made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1947 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.3...v4.0.4 ## v4.0.3 - 2026-01-26 ### What's Changed * Remove find cache in favor of upstream optimization by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1939 * Update onCacheEvent to accept multiple event types by @Yahav in https://github.com/fruitcake/laravel-debugbar/pull/1943 * Fix cache collector route by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1944 ### New Contributors * @Yahav made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1943 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.2...v4.0.3 ## v4.0.2 - 2026-01-24 ### What's Changed * Fix Auth Collector by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1937 * Fix session collector by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1938 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.1...v4.0.2 ## v4.0.1 - 2026-01-24 ### What's Changed * Fix explain table css on queries widget by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1929 * Check if Telescope is recording by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1931 * Update namespaces in readme by @sajjadhossainshohag in https://github.com/fruitcake/laravel-debugbar/pull/1932 * Add backtrace path by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1933 * Update vendor name, fix release notes by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1934 * Add link class by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1935 * Collected jobs from queue by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1936 ### New Contributors * @sajjadhossainshohag made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1932 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v4.0.0...v4.0.1 ## v4.0.0 - 2026-01-23 ### Laravel Debugbar 4.0 ### Release notes See https://fruitcake.nl/blog/laravel-debugbar-v4-release for the biggest changes. This brings the updates from php-debugbar 3.x to Laravel Debugbar. See https://github.com/php-debugbar/php-debugbar/releases/tag/v3.0.0 for the upstream changes to php-debugbar. ### Updating The name has changed, so remove the old package first: `composer remove barryvdh/laravel-debugbar --dev --no-scripts` Then install the new package `composer require fruitcake/laravel-debugbar --with-dependencies` Check the https://github.com/fruitcake/laravel-debugbar/blob/master/UPGRADE.md for any changes. ### All Changes * Prepare for Debugbar 3.x by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1828 * Fix 4.x queries by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1832 * Remove deprecations, tweak default config by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1833 * Always render widget in footer by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1834 * Fix null handling quoting in emulateQuote[QueryCollector] by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1835 * Update workflows / tools, add static analyses, fix some errors by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1836 * Revert event config by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1837 * Remove socket storage by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1839 * Remove Lumen support by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1838 * Remove icon by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1840 * Fix phpstan by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1841 * Remove PDO extension by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1842 * Extend base sql widget by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1843 * Fix shell quotes in README by @szepeviktor in https://github.com/fruitcake/laravel-debugbar/pull/1264 * refactor: improve routes formats by @jbidad in https://github.com/fruitcake/laravel-debugbar/pull/1392 * remove copy and hints by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1844 * Check response for avoid inject debugbar on json ajax by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1558 * Show estimate of cache byte usage by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1764 * Check string by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1845 * Use original background by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1847 * Add DataProviders for easier maintenance by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1846 * Feat custom collectors by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1848 * Tweak config by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1849 * Feat phpdebugbar symfony by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1850 * Improve Livewire collection and view detection for components by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1853 * Builds docs from source by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1854 * Fix default for excluded events by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1856 * Remove icon overrides by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1857 * Restore Mail collector timeline by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1858 * Add HTTP client collector by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1859 * Add http client to docs by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1860 * Update JavascriptRenderer for upstream changes by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1861 * Simplify Asset Renderer by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1862 * Bring back logs collector by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1863 * Use message context for gate and logs by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1866 * Updates tests for new beta by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1867 * Reduce styling overrides by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1864 * Use symfony-bridge by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1868 * Set livewire sentence by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1869 * Fix timeline by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1871 * Fix storage by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1872 * Seperate listeners from data in events by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1873 * Add casters for heavy objects by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1874 * Fix tests by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1876 * TWeak livewire properties by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1877 * Move namespace to Fruitcake\LaravelDebugbar by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1875 * Replace old package name by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1878 * Fix explain option access in DatabaseCollectorProvider by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1879 * Update .gitattributes by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1881 * Stricter types by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1884 * Add docs directory to export-ignore in .gitattributes by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1883 * Cleanup by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1885 * Fix docs tests by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1886 * Fix cache widget by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1887 * Fix checkVersion accessibility by @angeljqv in https://github.com/fruitcake/laravel-debugbar/pull/1889 * Check signature by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1888 * Add Inertia collector by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1890 * Improve storage scan by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1891 * Use upstream file storage and request generator by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1892 * Optimize livewire by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1893 * Test Livewire 2/3/4 by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1894 * Reset interfaces on Octane request, use current config by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1895 * Separate the debugbar from the application load(TimeCollector) by @erikn69 in https://github.com/fruitcake/laravel-debugbar/pull/1896 * Optimize serviceprovider by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1897 * Octane singleton by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1898 * Tweak constructors and config by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1899 * Tweak pennant by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1900 * Time octane reset by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1901 * Tweak booting time by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1902 * Tweak twig by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1903 * Always ensure time/exceptions/messages are available, to log before b… by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1904 * Tweak config values by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1906 * Tweak subscribers by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1905 * Remove request instances by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1907 * Update console collecting by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1908 * Fix cache events by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1909 * Tweak handle by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1910 * Add octane request start by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1911 * Small reset tweaks by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1912 * Add some timeline options by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1913 * Ensure latest request is used by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1915 * Check if octane needs to enable/disbale by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1917 * Use cookies instead of session, events instead of middleware by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1914 * Update tests for Livewire 3 and 4 by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1918 * collect on terminate by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1919 * Bump lodash from 4.17.21 to 4.17.23 by @dependabot[bot] in https://github.com/fruitcake/laravel-debugbar/pull/1920 * Restore ulid requestids by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1921 * Use openhandler http driver, set etag by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1922 * Check if params table is set by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1923 * Fix event data by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1924 * Update RequestCollector for CLI usage by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1925 * Tweak ClearCommand for uninstall by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1927 * Catch resolve errors by @barryvdh in https://github.com/fruitcake/laravel-debugbar/pull/1928 ### New Contributors * @szepeviktor made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1264 * @jbidad made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1392 * @dependabot[bot] made their first contribution in https://github.com/fruitcake/laravel-debugbar/pull/1920 **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v3.16.3...v4.0.0 ## v3.16.4 - 2026-01-23 - Add new fruitcake namespace to exclude from query backtrace. **Full Changelog**: https://github.com/fruitcake/laravel-debugbar/compare/v3.16.3...v3.16.4 ## v4.0-beta.11 - 2026-01-06 ### What's Changed * Simplify Asset Renderer by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1862 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v4.0-beta.10...v4.0-beta.11 ## v4.0-beta.9 - 2026-01-05 ### What's Changed * Remove icon overrides by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1857 * Restore Mail collector timeline by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1858 * Add HTTP client collector by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1859 * Add http client to docs by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1860 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v4.0-beta.8...v4.0-beta.9 ## v4.0-beta.8 - 2026-01-05 ### What's Changed * Builds docs from source by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1854 * Fix default for excluded events by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1856 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v4.0-beta.7...v4.0-beta.8 ## v4.0-beta.7 - 2026-01-05 ### What's Changed * Improve Livewire collection and view detection for components by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1853 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v4.0-beta.6...v4.0-beta.7 ## v3.16.3 - 2025-12-26 ### What's Changed * Update symfony/finder version constraint to include 8 by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1830 * Allow Symfony v8 by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1827 * Add error_level config option to filter error handler reporting (#1373) by @elliota43 in https://github.com/barryvdh/laravel-debugbar/pull/1825 * Add support for Cursor, Windsurf, and additional editor configurations by @nguyentranchung in https://github.com/barryvdh/laravel-debugbar/pull/1823 * Don't create tags with the onclick attribute by @PeterMead in https://github.com/barryvdh/laravel-debugbar/pull/1820 * docs: Add conditional check for Debugbar alias registration by @erhanurgun in https://github.com/barryvdh/laravel-debugbar/pull/1829 ### New Contributors * @elliota43 made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1825 * @nguyentranchung made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1823 * @PeterMead made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1820 * @erhanurgun made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1829 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.16.2...v3.16.3 ## v3.16.2 - 2025-12-16 ### What's Changed * Remove default null value env by @Erulezz in https://github.com/barryvdh/laravel-debugbar/pull/1815 * Remove --ignore-platform-req=php+ on integration test setup by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1814 * Remove calls to PHP 8.5-deprecated `setAccessible` by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1822 ### New Contributors * @Erulezz made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1815 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.16.1...v3.16.2 ## v3.16.1 - 2025-11-19 ### What's Changed * Slow threshold highlight on queries by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1805 * (fix) trim last line breaks on logs by @angeljqv in https://github.com/barryvdh/laravel-debugbar/pull/1806 * fix: Typo by @aurac in https://github.com/barryvdh/laravel-debugbar/pull/1810 * Test on PHP 8.5 by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1811 * Add '_boost*' to debugbar exceptions by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1818 * Dropped Laravel 9 support ### New Contributors * @aurac made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1810 **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.16.0...v3.16.1 ## 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 ================================================ FILE: LICENSE ================================================ Copyright (C) 2013-present Barry vd. Heuvel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability Please report security issues to `barryvdh@gmail.com` ================================================ FILE: UPGRADE.md ================================================ # Upgrade Guide ## 3.x to 4.x ### php-debugbar 3.x The php-debugbar dependency has been updated to 3.x. This removes jQuery and font-awesome. This should not impact your application, unless you are using custom collectors. ### Updated namespace The new namespace is `Fruitcake\LaravelDebugbar` instead of `Barryvdh\Debugbar`. You usually do not need to change this, unless you are manually registering the service provider / facade. The packge install is now `fruitcake/laravel-debugbar`. ### Removed - SocketStorage (no longer maintained) - Lumen support (no longer maintained) - FileCollector (no longer useful) - `start_measure()`, `add_measure()`, `stop_measure()`, `measure()` helper methods (use `debugbar()->startMeasure()` etc. instead) ### Other changes - The Query Collector now extends the php-debugbar widget. The bindings parameter has been removed in favor of 'params'. - Instead of 'hiddens', we now have an option 'masked' which uses the keys, not array paths. - Ineratia has its own collector / config. Update your config accordingly. ### For packages extending Laravel Debugbar - modifyResponse has changed to handleResponse, and is now done with a listener instead of middleware - the HttpDriver is now session-less, and uses cookies. - Octane keeps the LaravelDebugbar state, so has to reset collectors. You can remove Laravel Debugbar from the flush config. ================================================ FILE: build/build-docs.php ================================================ ', $generatedScripts, 2)[1]; // Read the main.html template $templatePath = __DIR__ . '/../docs/overrides/main.html'; $template = file_get_contents($templatePath); // Replace the scripts block content between specific markers $startMarker = ""; $endMarker = ""; // Find the positions $startPos = strpos($template, $startMarker); $endPos = strpos($template, $endMarker); if ($startPos !== false && $endPos !== false) { $startPos += strlen($startMarker); // Replace the content between markers $newTemplate = substr($template, 0, $startPos) . "\n" . $generatedScripts . "\n" . substr($template, $endPos); // Write back to the file file_put_contents($templatePath, $newTemplate); echo "✓ Updated docs/overrides/main.html with generated debugbar scripts\n"; } else { echo "✗ Could not find script markers in main.html\n"; exit(1); } // Copy dist folder to docs/assets/dist $distSource = __DIR__ . '/docs/assets'; $distDest = __DIR__ . '/../docs/assets/dist'; if (!is_dir($distSource)) { echo "✗ dist folder not found at $distSource\n"; exit(1); } // Create docs/assets directory if it doesn't exist if (!is_dir(__DIR__ . '/../docs/assets')) { mkdir(__DIR__ . '/../docs/assets', 0755, true); } // Remove existing dist folder if it exists if (is_dir($distDest)) { deleteDirectory($distDest); } // Copy dist folder copyDirectory($distSource, $distDest); echo "✓ Copied dist folder to docs/assets/dist\n"; // Update mkdocs.yml with current timestamp $mkdocsPath = __DIR__ . '/../mkdocs.yml'; $mkdocsContent = file_get_contents($mkdocsPath); $timestamp = time(); $mkdocsContent = preg_replace( '/debugbar\.css\?v=\d+/', 'debugbar.css?v=' . $timestamp, $mkdocsContent ); $mkdocsContent = preg_replace( '/debugbar\.js\?v=\d+/', 'debugbar.js?v=' . $timestamp, $mkdocsContent ); file_put_contents($mkdocsPath, $mkdocsContent); echo "✓ Updated mkdocs.yml with timestamp: $timestamp\n"; function copyDirectory($source, $dest) { mkdir($dest, 0755, true); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { $destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathname(); if ($item->isDir()) { mkdir($destPath, 0755, true); } else { copy($item, $destPath); } } } function deleteDirectory($dir) { if (!is_dir($dir)) { return; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $item) { if ($item->isDir()) { rmdir($item); } else { unlink($item); } } rmdir($dir); } ================================================ FILE: build/build-icons.js ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Tabler icons to include // Format: 'icon-name' for outline icons, or { name: 'icon-name', filled: true } for filled icons const icons = [ // Data collector icons 'archive', 'clipboard-text', 'files', 'lock', 'user', 'share-3', 'subtask', // UI control icons 'brand-laravel', 'brand-livewire', 'brand-inertia', // Query widget icons 'pin', 'help-circle', 'list', 'gauge', // Message icons { name: 'star', filled: true }, 'info-circle', // Link icon 'external-link', ]; const svgDirOutline = path.join(__dirname, '../node_modules/@tabler/icons/icons/outline'); const svgDirFilled = path.join(__dirname, '../node_modules/@tabler/icons/icons/filled'); const outputFile = path.join(__dirname, '../resources/laravel-icons.css'); const defaultStrokeWidth = 2; // Tabler default stroke width const brandStrokeWidth = 1; // For brands, use 1 function svgToDataUri(svgContent, strokeWidth) { // Remove XML comments svgContent = svgContent.replace(//g, ''); // Ensure consistent stroke-width svgContent = svgContent.replace(/stroke-width="[^"]*"/g, `stroke-width="${strokeWidth}"`); // Remove unnecessary attributes for mask usage (but not stroke-width!) svgContent = svgContent.replace(/\s+class="[^"]*"/g, ''); svgContent = svgContent.replace(/\s+width="[^"]*"/g, ''); svgContent = svgContent.replace(/\s+height="[^"]*"/g, ''); // Minify: remove newlines and extra spaces svgContent = svgContent.replace(/\s+/g, ' ').trim(); // URL encode for data URI const encoded = encodeURIComponent(svgContent) .replace(/'/g, '%27') .replace(/"/g, '%22'); return `data:image/svg+xml,${encoded}`; } function generateIconsCSS() { let css = `/* Generated file - do not edit manually */\n/* Generated from Tabler Icons */\n\n`; // First, define all CSS variables with the SVG data URIs css += `:root {\n`; for (const iconEntry of icons) { const iconName = typeof iconEntry === 'string' ? iconEntry : iconEntry.name; const isFilled = typeof iconEntry === 'object' && iconEntry.filled; const svgDir = isFilled ? svgDirFilled : svgDirOutline; const svgPath = path.join(svgDir, `${iconName}.svg`); if (!fs.existsSync(svgPath)) { console.warn(`Warning: SVG file not found for icon "${iconName}" at ${svgPath}`); continue; } const svgContent = fs.readFileSync(svgPath, 'utf8'); let strokeWidth = iconName.indexOf('brand-') === 0 ? brandStrokeWidth : defaultStrokeWidth; const dataUri = svgToDataUri(svgContent, strokeWidth); css += ` --debugbar-icon-${iconName}: url('${dataUri}');\n`; } css += `}\n\n`; // Then, apply the variables to the icon classes for (const iconEntry of icons) { const iconName = typeof iconEntry === 'string' ? iconEntry : iconEntry.name; const isFilled = typeof iconEntry === 'object' && iconEntry.filled; const svgDir = isFilled ? svgDirFilled : svgDirOutline; const svgPath = path.join(svgDir, `${iconName}.svg`); if (!fs.existsSync(svgPath)) { continue; } css += `.phpdebugbar-icon-${iconName}::before {\n`; css += ` -webkit-mask-image: var(--debugbar-icon-${iconName});\n`; css += ` mask-image: var(--debugbar-icon-${iconName});\n`; css += `}\n\n`; } fs.writeFileSync(outputFile, css, 'utf8'); console.log(`✓ Generated ${outputFile} with ${icons.length} icons`); } try { generateIconsCSS(); } catch (error) { console.error('Error generating icons:', error); process.exit(1); } ================================================ FILE: composer.json ================================================ { "name": "fruitcake/laravel-debugbar", "description": "PHP Debugbar integration for Laravel", "keywords": [ "laravel", "debugbar", "profiler", "debug", "webprofiler", "barryvdh", "dev" ], "license": "MIT", "authors": [ { "name": "Fruitcake", "homepage": "https://fruitcake.nl" }, { "name": "Barry vd. Heuvel", "email": "barryvdh@gmail.com" } ], "require": { "php": "^8.2", "php-debugbar/php-debugbar": "^3.5", "php-debugbar/symfony-bridge": "^1.1", "illuminate/routing": "^11|^12|^13.0", "illuminate/session": "^11|^12|^13.0", "illuminate/support": "^11|^12|^13.0" }, "require-dev": { "mockery/mockery": "^1.3.3", "orchestra/testbench-dusk": "^9|^10|^11", "phpunit/phpunit": "^11", "larastan/larastan": "^3", "phpstan/phpstan-phpunit": "^2", "laravel/pint": "^1", "livewire/livewire": "^3.7|^4", "phpstan/phpstan-strict-rules": "^2.0", "shipmonk/phpstan-rules": "^4.3", "laravel/octane": "^2", "laravel/pennant": "^1", "php-debugbar/twig-bridge": "^2.0", "laravel/telescope": "^5.16" }, "replace": { "barryvdh/laravel-debugbar": "self.version" }, "autoload": { "psr-4": { "Fruitcake\\LaravelDebugbar\\": "src/" }, "files": [ "src/helpers.php" ] }, "autoload-dev": { "psr-4": { "Fruitcake\\LaravelDebugbar\\Tests\\": "tests" } }, "minimum-stability": "dev", "prefer-stable": true, "extra": { "branch-alias": { "dev-master": "4.1-dev" }, "laravel": { "providers": [ "Fruitcake\\LaravelDebugbar\\ServiceProvider" ], "aliases": { "Debugbar": "Fruitcake\\LaravelDebugbar\\Facades\\Debugbar" } } }, "scripts": { "analyse": "vendor/bin/phpstan analyse --memory-limit=1G", "check-style": "vendor/bin/pint --test", "fix-style": "vendor/bin/pint", "test": "vendor/bin/phpunit", "docs": "vendor/bin/phpunit --filter=testItInjectsOnDocs && php build/build-docs.php && mkdocs build" } } ================================================ FILE: config/debugbar.php ================================================ env('DEBUGBAR_ENABLED'), 'collect_jobs' => env('DEBUGBAR_COLLECT_JOBS', false), 'except' => [ 'telescope*', 'horizon*', '_boost/browser-logs', 'livewire-*/livewire.js', ], /* |-------------------------------------------------------------------------- | DataCollectors |-------------------------------------------------------------------------- | | Enable/disable DataCollectors | */ 'collectors' => [ 'phpinfo' => env('DEBUGBAR_COLLECTORS_PHPINFO', false), // Php version 'messages' => env('DEBUGBAR_COLLECTORS_MESSAGES', true), // Messages 'time' => env('DEBUGBAR_COLLECTORS_TIME', true), // Time Datalogger 'memory' => env('DEBUGBAR_COLLECTORS_MEMORY', true), // Memory usage 'exceptions' => env('DEBUGBAR_COLLECTORS_EXCEPTIONS', true), // Exception displayer 'log' => env('DEBUGBAR_COLLECTORS_LOG', true), // Logs from Monolog (merged in messages if enabled) 'db' => env('DEBUGBAR_COLLECTORS_DB', true), // Show database (PDO) queries and bindings 'views' => env('DEBUGBAR_COLLECTORS_VIEWS', true), // Views with their data 'route' => env('DEBUGBAR_COLLECTORS_ROUTE', false), // Current route information 'auth' => env('DEBUGBAR_COLLECTORS_AUTH', false), // Display Laravel authentication status 'gate' => env('DEBUGBAR_COLLECTORS_GATE', true), // Display Laravel Gate checks 'session' => env('DEBUGBAR_COLLECTORS_SESSION', false), // Display session data 'symfony_request' => env('DEBUGBAR_COLLECTORS_SYMFONY_REQUEST', true), // Default Request Data '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 'logs' => env('DEBUGBAR_COLLECTORS_LOGS', false), // Add the latest log messages 'config' => env('DEBUGBAR_COLLECTORS_CONFIG', false), // Display config settings 'cache' => env('DEBUGBAR_COLLECTORS_CACHE', true), // Display cache events 'models' => env('DEBUGBAR_COLLECTORS_MODELS', true), // Display models 'livewire' => env('DEBUGBAR_COLLECTORS_LIVEWIRE', true), // Display Livewire (when available) 'inertia' => env('DEBUGBAR_COLLECTORS_INERTIA', true), // Display Inertia (when available) 'jobs' => env('DEBUGBAR_COLLECTORS_JOBS', true), // Display dispatched jobs 'pennant' => env('DEBUGBAR_COLLECTORS_PENNANT', true), // Display Pennant feature flags 'http_client' => env('DEBUGBAR_COLLECTORS_HTTP_CLIENT', true), // Display HTTP Client requests ], /* |-------------------------------------------------------------------------- | Extra options |-------------------------------------------------------------------------- | | Configure some DataCollectors | */ '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 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) 'capture_dumps' => env('DEBUGBAR_OPTIONS_MESSAGES_CAPTURE_DUMPS', false), // Capture laravel `dump();` as message 'timeline' => env('DEBUGBAR_OPTIONS_MESSAGES_TIMELINE', true), // Add messages to the timeline ], 'memory' => [ 'reset_peak' => env('DEBUGBAR_OPTIONS_MEMORY_RESET_PEAK', false), // run memory_reset_peak_usage before collecting 'with_baseline' => env('DEBUGBAR_OPTIONS_MEMORY_WITH_BASELINE', false), // Set boot memory usage as memory peak baseline 'precision' => (int) env('DEBUGBAR_OPTIONS_MEMORY_PRECISION', 0), // Memory rounding precision ], 'auth' => [ 'show_name' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_NAME', true), // Also show the users name/email in the debugbar 'show_guards' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_GUARDS', true), // Show the guards that are used ], 'gate' => [ 'trace' => false, // Trace the origin of the Gate checks 'timeline' => env('DEBUGBAR_OPTIONS_GATE_TIMELINE', false), // Add mails to the timeline ], 'db' => [ 'with_params' => env('DEBUGBAR_OPTIONS_WITH_PARAMS', true), // Render SQL with the parameters substituted 'exclude_paths' => [ // Paths to exclude entirely from the collector //'vendor/laravel/framework/src/Illuminate/Session', // Exclude sessions queries ], 'backtrace' => env('DEBUGBAR_OPTIONS_DB_BACKTRACE', true), // Use a backtrace to find the origin of the query in your files. 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) 'timeline' => env('DEBUGBAR_OPTIONS_DB_TIMELINE', false), // Add the queries to the timeline 'duration_background' => env('DEBUGBAR_OPTIONS_DB_DURATION_BACKGROUND', true), // Show shaded background on each query relative to how long it took to execute. 'explain' => [ // Show EXPLAIN output on queries 'enabled' => env('DEBUGBAR_OPTIONS_DB_EXPLAIN_ENABLED', true), ], 'show_query_result' => env('DEBUGBAR_OPTIONS_DB_SHOW_QUERY_RESULT', false), // Show option to re-run SELECT queries and show the result 'only_slow_queries' => env('DEBUGBAR_OPTIONS_DB_ONLY_SLOW_QUERIES', true), // Only track queries that last longer than `slow_threshold` 'slow_threshold' => env('DEBUGBAR_OPTIONS_DB_SLOW_THRESHOLD', false), // Max query execution time (ms). Exceeding queries will be highlighted 'memory_usage' => env('DEBUGBAR_OPTIONS_DB_MEMORY_USAGE', false), // Show queries memory usage 'soft_limit' => (int) env('DEBUGBAR_OPTIONS_DB_SOFT_LIMIT', 100), // After the soft limit, no parameters/backtrace are captured 'hard_limit' => (int) env('DEBUGBAR_OPTIONS_DB_HARD_LIMIT', 500), // After the hard limit, queries are ignored ], 'mail' => [ 'timeline' => env('DEBUGBAR_OPTIONS_MAIL_TIMELINE', true), // Add mails to the timeline 'show_body' => env('DEBUGBAR_OPTIONS_MAIL_SHOW_BODY', true), ], 'views' => [ 'timeline' => env('DEBUGBAR_OPTIONS_VIEWS_TIMELINE', true), // Add the views to the timeline 'data' => env('DEBUGBAR_OPTIONS_VIEWS_DATA', false), // True for all data, 'keys' for only names, false for no parameters. 'group' => (int) env('DEBUGBAR_OPTIONS_VIEWS_GROUP', 50), // Group duplicate views. Pass value to auto-group, or true/false to force 'exclude_paths' => [ // Add the paths which you don't want to appear in the views 'vendor/filament', // Exclude Filament components by default ], ], 'inertia' => [ 'pages' => env('DEBUGBAR_OPTIONS_VIEWS_INERTIA_PAGES', 'js/Pages'), // Path for Inertia views ], 'route' => [ 'label' => env('DEBUGBAR_OPTIONS_ROUTE_LABEL', true), // Show complete route on bar ], 'session' => [ 'masked' => [], // List of keys that are masked ], 'symfony_request' => [ 'label' => env('DEBUGBAR_OPTIONS_SYMFONY_REQUEST_LABEL', true), // Show route on bar 'masked' => [], // List of keys that are masked ], 'events' => [ 'data' => env('DEBUGBAR_OPTIONS_EVENTS_DATA', false), // Collect events data 'listeners' => env('DEBUGBAR_OPTIONS_EVENTS_LISTENERS', false), // Add listeners to the events data 'excluded' => [], // Example: ['eloquent.*', 'composing', Illuminate\Cache\Events\CacheHit::class] ], 'logs' => [ 'file' => env('DEBUGBAR_OPTIONS_LOGS_FILE'), ], 'config' => [ 'masked' => [], ], 'cache' => [ 'values' => env('DEBUGBAR_OPTIONS_CACHE_VALUES', true), // Collect cache values 'timeline' => env('DEBUGBAR_OPTIONS_CACHE_TIMELINE', false), // Add cache events to the timeline ], 'http_client' => [ 'masked' => [], 'timeline' => env('DEBUGBAR_OPTIONS_HTTP_CLIENT_TIMELINE', true), // Add requests to the timeline ], ], /** * Add any additional DataCollectors by adding the class name of a DataCollector or invokable class. */ 'custom_collectors' => [ // MyCollector::class => env('DEBUGBAR_COLLECTORS_MYCOLLECTOR', true), ], /* |-------------------------------------------------------------------------- | Editor |-------------------------------------------------------------------------- | | Choose your preferred editor to use when clicking file name. | | Supported: "sublime", "textmate", "emacs", "macvim", "codelite", | "phpstorm", "phpstorm-remote", "idea", "idea-remote", | "vscode", "vscode-insiders", "vscode-remote", "vscode-insiders-remote", | "vscodium", "nova", "xdebug", "atom", "espresso", | "netbeans", "cursor", "windsurf", "zed", "antigravity" | */ 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), /* |-------------------------------------------------------------------------- | Capture Ajax Requests |-------------------------------------------------------------------------- | | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), | you can use this option to disable sending the data through the headers. | | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. | | Note for your request to be identified as ajax requests they must either send the header | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. | | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. | | You can defer loading the dataset, so it will be loaded with ajax after the request is done. (Experimental) */ 'capture_ajax' => 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), /* |-------------------------------------------------------------------------- | 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')), /* |-------------------------------------------------------------------------- | Storage settings |-------------------------------------------------------------------------- | | 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. Sqlite will | create a database file in the storage folder. | 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' => env('DEBUGBAR_STORAGE_ENABLED', true), 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. 'driver' => env('DEBUGBAR_STORAGE_DRIVER', 'file'), // redis, file, sqlite, pdo, custom 'path' => env('DEBUGBAR_STORAGE_PATH', storage_path('debugbar')), // For file driver 'connection' => env('DEBUGBAR_STORAGE_CONNECTION'), // Leave null for default connection (Redis/PDO) 'provider' => env('DEBUGBAR_STORAGE_PROVIDER', ''), // Instance of StorageInterface for custom driver ], /* |-------------------------------------------------------------------------- | Assets |-------------------------------------------------------------------------- | | 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: (none) | and for js: highlight.js | So if you want syntax highlighting, set it to true. | */ 'use_dist_files' => env('DEBUGBAR_USE_DIST_FILES', true), 'include_vendors' => env('DEBUGBAR_INCLUDE_VENDORS', true), /* |-------------------------------------------------------------------------- | Custom Error Handler for Deprecated warnings |-------------------------------------------------------------------------- | | When enabled, the Debugbar shows deprecated warnings for Symfony components | in the Messages tab. | | You can set a custom error reporting level to filter which errors are | handled. For example, to exclude deprecation warnings: | E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED | | To exclude notices, strict warnings, and deprecations: | E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED & ~E_USER_DEPRECATED | | Defaults to E_ALL (all errors). | */ 'error_handler' => env('DEBUGBAR_ERROR_HANDLER', false), 'error_level' => env('DEBUGBAR_ERROR_LEVEL', E_ALL), /* |-------------------------------------------------------------------------- | Clockwork integration |-------------------------------------------------------------------------- | | The Debugbar can emulate the Clockwork headers, so you can use the Chrome | Extension, without the server-side code. It uses Debugbar collectors instead. | */ 'clockwork' => env('DEBUGBAR_CLOCKWORK', false), /* |-------------------------------------------------------------------------- | Inject Debugbar in Response |-------------------------------------------------------------------------- | | Usually, the debugbar is added just before , by listening to the | Response after the App is done. If you disable this, you have to add them | in your template yourself. See http://phpdebugbar.com/docs/rendering.html | */ 'inject' => env('DEBUGBAR_INJECT', true), /* |-------------------------------------------------------------------------- | Debugbar route prefix |-------------------------------------------------------------------------- | | 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' => env('DEBUGBAR_ROUTE_PREFIX', '_debugbar'), /* |-------------------------------------------------------------------------- | Debugbar route middleware |-------------------------------------------------------------------------- | | Additional middleware to run on the Debugbar routes */ 'route_middleware' => [], /* |-------------------------------------------------------------------------- | Debugbar route domain |-------------------------------------------------------------------------- | | By default Debugbar route served from the same domain that request served. | To override default domain, specify it as a non-empty value. */ 'route_domain' => env('DEBUGBAR_ROUTE_DOMAIN'), /* |-------------------------------------------------------------------------- | 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), ]; ================================================ FILE: database/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php ================================================ string('id'); $table->longText('data'); $table->string('meta_utime'); $table->dateTime('meta_datetime'); $table->string('meta_uri'); $table->string('meta_ip'); $table->string('meta_method'); $table->primary('id'); $table->index('meta_utime'); $table->index('meta_datetime'); $table->index('meta_uri'); $table->index('meta_ip'); $table->index('meta_method'); }); } /** * Reverse the migrations. */ public function down() { Schema::drop('phpdebugbar'); } }; ================================================ FILE: docs/CNAME ================================================ laraveldebugbar.com ================================================ FILE: docs/assets/extra.css ================================================ :root { --md-primary-fg-color: #eb4432; --md-primary-fg-color--light: #eb4432; --md-primary-fg-color--dark: #eb4432; --md-accent-fg-color: #eb4432; --md-accent-fg-color--light: #eb4432; --md-accent-fg-color--dark: #eb4432; } .mdx-badge__icon{ background:var(--md-accent-fg-color--transparent); padding:.2rem } .mdx-badge__text{ font-size:.85em; box-shadow:0 0 0 1px inset var(--md-accent-fg-color--transparent); padding:.2rem .3rem } ================================================ FILE: docs/collectors.md ================================================ --- description: Laravel Debugbar contains a lot of collectors to help you debug or profile Database Queries, Log messages, View templates, Request and Route information, etc. preview_image: img/preview-usage.jpg --- !!! warning Debugbar can slow the application down (because it has to gather and render data). So when experiencing slowness, try disabling some of the collectors. # Collectors This package includes with these Collectors enabled by default: - [Queries](#db): Show all database queries - [Messages](#messages): Debug messages and objects - [Logger](#log): Show all Log messages (Show in Messages when available) - [Views](#views): Show the currently loaded views. - [Timeline](#time): Timeline with Booting and Application timing - [Route](#route): Show information about the current Route. - [Exceptions](#exceptions): Exceptions and Throwable with stacktrace - [Session](#session): Current session data - [Request](#request): Request data - [Livewire](#livewire): Only active when Livewire is used - [PhpInfo](#phpinfo): Current PHP version These collectors can be enabled in the config: - [Gate](#gate): Show the gates that are checked - [Events](#events): Show all events - [Auth](#auth): Logged in status - [Mail](#mail): Sent emails - [Laravel Info](#laravel): Show the Laravel version and Environment. - [Memory](#memory): Memory usage - [Config](#config): Display the values from the config files. - [Cache](#cache): Display all cache events. - [Models](#models): Loaded Models - [Jobs](#jobs): Sent emails - [Logs](#logs): Logs from the log files - [Pennant](#pennant): Show Pennant flags - [Files](#files): Show the files that are included/required by PHP. To enable or disable any of the collectors, set the configuration to `true` or `false`. Some collector have additional options in the configuration:
config/debugbar.php ```php /* |-------------------------------------------------------------------------- | DataCollectors |-------------------------------------------------------------------------- | | Enable/disable DataCollectors | */ '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' => false, // Display Laravel Gate checks 'session' => true, // Display session data 'symfony_request' => true, // Only one can be enabled.. 'mail' => false, // 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 'livewire' => true, // Display Livewire (when available) 'jobs' => false, // Display dispatched jobs 'pennant' => false, // Display Pennant feature flags ], ```
## Database Queries { #db } The Query Collector has the following features - Show the executed queries including timing - Show/mark duplicate queries - Show used parameters - Run on-demand 'EXPLAIN' queries and link to Visual Explain (disabled bu default) - Copy the query to clipboard - Show the source of the query and open in editor - Visualize the duration of the queries with bottom border - Add queries to the timeline (disabled by default) - Limit the number of queries to avoid slowing down the Debugbar. - Exclude paths (eg. for session or vendors) - Show memory usage (disabled by default) ![Query Collector](img/queries.png)
config/debugbar.php ```php 'options' => [ // ... 'db' => [ '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' => 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' => false, // Add the queries to the timeline '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, ], 'hints' => false, // Show hints for common mistakes 'show_copy' => true, // Show copy button next to the query, 'slow_threshold' => false, // Only track queries that last longer than this time in ms 'memory_usage' => false, // Show queries memory usage 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured 'hard_limit' => 500, // After the hard limit, queries are ignored ], // ... ], ```
### On-demand query EXPLAIN Enable the `options.db.explain` option to run on-demand EXPLAIN queries for any SELECT query in the Debugbar. This will update in the interface. You also have an option to navigate to mysqlexplain.com for a visual explain. ![Query On-demand Explain](img/query-explain.gif) ### Query limits With Query Hard & Soft limits, you can reduce the amount of queries shown by default. When the soft limit is reached, bindings will be excluded. When the hard limit is reached, the queries are excluded altogether to prevent loading too much data. If you want to avoid any limits, you can set the option to `null` ![Query Limits](img/query-limits.png) ## Messages { #messages } The Message collectors gathers all messages from `debug()` calls and anything written to the logs. You can pass multiple parameters to `debug()`, even complex object. ### Trace When calling `debug()`, the source of the call is shown and can be opened with your IDE. ![Messages Collector](img/messages.png) ## Logger { #log } When the [Messages Collector](#messages) is enabled, Log messages are added to the Messages tab. Otherwise a Monolog tab will show with just the log messages ![Monolog](img/monolog.png)
config/debugbar.php ```php 'options' => [ // ... 'db' => [ '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' => 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' => false, // Add the queries to the timeline '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, ], 'hints' => false, // Show hints for common mistakes 'show_copy' => true, // Show copy button next to the query, 'slow_threshold' => false, // Only track queries that last longer than this time in ms 'memory_usage' => false, // Show queries memory usage 'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured 'hard_limit' => 500, // After the hard limit, queries are ignored ], // ... ], ```
## Views { #views } The ViewCollector shows views and has the following features: - Show used templates and source - Optionally add them to the timeline - Group similar views (useful for components) - Exclude folders (eg. for Filament or other vendors) - Optionally show data (this can be resource heavy) ![ViewCollector](img/views.png) ```php 'options' => [ 'views' => [ 'timeline' => false, // Add the views to the timeline (Experimental) 'data' => false, //true for all data, 'keys' for only names, false for no parameters. 'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force 'exclude_paths' => [ // Add the paths which you don't want to appear in the views 'vendor/filament' // Exclude Filament components by default ], ], ] ``` ## Timeline { #time } ![Timeline Collector](img/timeline.png)
config/debugbar.php ```php 'options' => [ 'time' => [ 'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate ], ] ```
## Route { #route } This shows the current route and middleware. ![RouteCollector](img/route.png)
config/debugbar.php ```php 'options' => [ 'route' => [ 'label' => true, // show complete route on bar ], ], ```
## Exceptions { #exceptions } Show any errors from the application, including traces. You can manually add exceptions by calling `debugbar()->addThrowable($throwable);` ![ExceptionCollector](img/exceptions.png) ## Session { #session } A simple widget showing the current PHP Version. ![Session Collector](img/session.png) ## Request { #request } Show Request info, like headers, data, cookies etc. Sensitive data is hidden by default, but you can add your own sensitive data to the config. ![Request Collector](img/request.png)
config/debugbar.php ```php 'options' => [ 'symfony_request' => [ 'hiddens' => [], // hides sensitive values using array paths, example: request_request.password ], ], ```
## Livewire { #livewire } Show the Livewire components that are rendered on the page. ![Livewire Collector](img/livewire.png) ## PHP Info { #phpinfo } A simple widget showing the current PHP Version. ![PhpInfo Collector](img/phpinfo.png) ## Gate { #gate } The Gate Collector shows the checks that have passed or failed. ![Gate Collector](img/gate.png) ## Events { #events } This is similar to the Timeline buts adds all events. This can be a lot of data, so use with caution. ![Events](img/events.gif)
config/debugbar.php ```php 'options' => [ 'events' => [ 'data' => false, // collect events data, listeners ], ], ```
## Auth { #auth } A widget showing the current login status + a collector with more information. ![Auth Collector](img/auth.png)
config/debugbar.php ```php 'options' => [ 'auth' => [ 'show_name' => true, // Also show the users name/email in the debugbar 'show_guards' => true, // Show the guards that are used ], ], ```
## Mail { #mail } A collector showing the sent emails. ![Mail Collector](img/mail.png) ### Mail Preview You can open a rendered preview of the email when the body is attached, by clicking 'View Mail' ![Mail Preview](img/mail-preview.png) ## Laravel Info { #laravel } A widget showing the current Laravel Version, environment and locale. ![Laravel Collector](img/laravel-info.png) ## Memory Usage { #memory } Show the Memory Usage of the application
config/debugbar.php ```php 'options' => [ 'memory' => [ 'reset_peak' => false, // run memory_reset_peak_usage before collecting 'with_baseline' => false, // Set boot memory usage as memory peak baseline 'precision' => 0, // Memory rounding precision ], ] ```
![Memory Collector](img/memory.png) ## Config { #config } !!! warning Be careful when turning this on, as it can expose sensitive credentials. Make sure your app is not publicly available. Shows the loaded configuration values. ![Config Collector](img/config.png) ## Cache { #cache } Show the hits/misses of the Cache in a Timeline. ![Cache Collector](img/cache.png)
config/debugbar.php ```php 'options' => [ 'cache' => [ 'values' => true, // collect cache values ], ], ```
## Models { #models } Shows how often each Model is loaded. If this is high, you might want move some logic to SQL instead of processing large Collections. ![Models Collector](img/models.png) ## Jobs { #jobs } Show the Jobs that are dispatched from this request. ![Jobs Collector](img/jobs.png) ## Logs { #logs } Show the most recent logs from the log files in storage/logs ![Logs Collector](img/logs.png)
config/debugbar.php ```php 'options' => [ 'logs' => [ 'file' => null, // Additional files ], ], ```
## Pennant { #pennant } Shows all the Pennant flags that are checked during this request ![Pennant Collector](img/pennant.png) ## Files { #files } !!! deprecated This was mainly useful before OPcache was widely used, and this collector could be used for optimizing files. It's deprecated now. ![Files Collector](img/files.png) ================================================ FILE: docs/features.md ================================================ --- description: Laravel Debugbar contains features like collectors, capturing ajax requesting, browsing history etc. preview_image: img/preview-features.jpg --- ## Collectors See the [Collectors page](collectors.md) for available collectors. ## AJAX Requests Laravel Debugbar tracks AJAX/XHR request in your application. You can open these in the dropdown menu, or click the history button to show the requests. Tip: you can disable he 'autoshow' toggle in the history tab to keep the current dataset active, instead of switching. ![AJAX Request](img/ajax.gif)
config/debugbar.php ```php /* |-------------------------------------------------------------------------- | Capture Ajax Requests |-------------------------------------------------------------------------- | | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), | you can use this option to disable sending the data through the headers. | | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. | | Note for your request to be identified as ajax requests they must either send the header | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. | | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. */ 'capture_ajax' => true, 'add_ajax_timing' => false, 'ajax_handler_auto_show' => true, 'ajax_handler_enable_tab' => true, ```
## History browser By default, Debugbar stores request history. This is useful for non-browser requests, redirects or external requests. You can open it with the 'folder' button (3rd from the right). With the default settings, storage is only visible from your local IP. To enable browsing the history, change the `storage.open` setting or `DEBUGBAR_OPEN_STORAGE` env key. !!! warning Do not open the history outside your local environment, to avoid leaking credentials or sensitive data. ![History](img/history.gif)
config/debugbar.php ```php /* |-------------------------------------------------------------------------- | Storage settings |-------------------------------------------------------------------------- | | 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, 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. 'driver' => 'file', // redis, file, pdo, socket, custom 'path' => storage_path('debugbar'), // For file driver 'connection' => null, // Leave null for default connection (Redis/PDO) 'provider' => '', // Instance of StorageInterface for custom driver 'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver 'port' => 2304, // Port to use with the "socket" driver ], ```
## Light and Dark mode Theme Debugbar supports Dark and Light mode. It defaults to `auto` which uses the browser setting. You can set it with `DEBUGBAR_THEME` or change the config to force th `light` or `dark`. See Light Mode below: ![Light Theme](img/light-theme.png) ## Editor integration Debugbar can open links to views, exception, routes etc in your Editor directly, if you set this up correctly. By default this should just work for PHPStorm on local development. You can change your editor by setting `DEBUGBAR_EDITOR` or the config. If your working in a remote host or docker, you can change the mapping between remote and local paths. ![History](img/editor.gif)
config/debugbar.php ```php /* |-------------------------------------------------------------------------- | 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')), ```
## Configuraton ### Custom features { #custom } Custom features or collectors are not enabled by default, but can be enabled by enabling the configuration setting. This is usually because the target audience of a feature is not large enough. ### Configurable options { #config } Configurable features are not enabled by default, but can be enabled. This is usually because the target audience of a feature is not large enough. You can enable the feature by changing the mentioned value in [config/debugbar.php](https://github.com/fruitcake/laravel-debugbar/blob/master/config/debugbar.php) after [publishing the config](installation.md#publish-config). ### Experimental Features { #experimental } Some features are marked as 'Experimental'. This mostly means the feature is new and not enabled by default, but might become enable by default in the future. You are welcome to test this feature and report any issues. ================================================ FILE: docs/index.md ================================================ --- title: Debugbar for Laravel description: Laravel Debugbar is a package that integrates PHP Debug Bar with Laravel to debug database queries and profile other information. hide: - navigation - toc --- # Laravel Debugbar ![Unit Tests](https://github.com/fruitcake/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/fruitcake/laravel-debugbar?label=Stable)](https://packagist.org/packages/fruitcake/laravel-debugbar) [![Total Downloads](https://img.shields.io/packagist/dt/barryvdh/laravel-debugbar?label=Downloads)](https://packagist.org/packages/fruitcake/laravel-debugbar) [![Fruitcake](https://img.shields.io/badge/Powered%20By-Fruitcake-b2bc35.svg)](https://fruitcake.nl/) Laravel Debugbar is a package that integrates [PHP Debug Bar](https://github.com/php-debugbar/php-debugbar) with Laravel to debug [database queries](collectors.md#db) and [profile other information](collectors.md). ![Debugbar Dark Mode screenshot](img/debugbar.gif) !!! example "Live Demo" See an interactive example of the Laravel Debugbar in action below. (Don't worry, this is just dummy data) !!! tip "4.x release" In January 2026, a new major version of the Debugbar is released. See [fruitcake.nl/blog/laravel-debugbar-v4-release](https://fruitcake.nl/blog/laravel-debugbar-v4-release) and [UPGRADE.md](https://github.com/fruitcake/laravel-debugbar/blob/master/UPGRADE.md) [Get started](installation.md){ .md-button .md-button--primary } [View code :material-github:](https://github.com/fruitcake/laravel-debugbar){ .md-button } ================================================ FILE: docs/installation.md ================================================ --- description: Installing Laravel Debugbar in a project is simple. Use 'composer require fruitcake/laravel-debugbar --dev' to get started now preview_image: img/preview-install.jpg --- # Installation ## Install with composer !!! danger Use the Debugbar only in development. Do not use Debugbar on publicly accessible websites, as it will leak information from stored requests (by design). Require this package with composer. It is recommended to only require the package for development. ```shell composer require fruitcake/laravel-debugbar --dev ``` Laravel uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider. > If you use a catch-all/fallback route, make sure you load the Debugbar ServiceProvider before your own App ServiceProviders. ## Enable By default, Debugbar will be enabled when `APP_DEBUG` is `true`. 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` ```php /* |-------------------------------------------------------------------------- | Debugbar Settings |-------------------------------------------------------------------------- | | Debugbar is enabled by default, when debug is set to true in app.php. | You can override the value by setting enable to true or false instead of null. | | You can provide an array of URI's that must be ignored (eg. 'api/*') | */ 'enabled' => env('DEBUGBAR_ENABLED', null), 'hide_empty_tabs' => false, // Hide tabs until they have content 'except' => [ 'telescope*', 'horizon*', ], ``` ### Publish config ```shell php artisan vendor:publish --provider="Fruitcake\LaravelDebugbar\ServiceProvider" ``` ## Non-default installs ### Without auto-discovery If you don't use auto-discovery, add the ServiceProvider to the providers list. For Laravel 11 or newer, add the ServiceProvider in bootstrap/providers.php. For Laravel 10 or older, add the ServiceProvider in config/app.php. ```php Fruitcake\LaravelDebugbar\ServiceProvider::class, ``` If you want to use the facade to log messages, add this within the `register` method of `app/Providers/AppServiceProvider.php` class: ```php public function register(): void { $loader = \Illuminate\Foundation\AliasLoader::getInstance(); $loader->alias('Debugbar', \Fruitcake\LaravelDebugbar\Facades\Debugbar::class); } ``` ### With Octane Laravel Debugbar 4.x works out of the box with Octane. No need to add anything to your config. If you're upgrading from Laravel Debugbar 3.x, remove the 'flush' config for Debugbar in `config/octane.php`. ### With Lumen Lumen is not supported anymore, as it's no longer actively maintained. ================================================ FILE: docs/overrides/main.html ================================================ {% extends "base.html" %} {% block extrahead %} {{ super() }} {% if page.is_homepage %} {% elif page.meta and page.meta.title %} {% elif page.title and not page.is_homepage %} {% else %} {% endif %} {% if page.meta and page.meta.description %} {% elif config.site_description %} {% endif %} {% if page.canonical_url %} {% endif %} {% if page.meta and page.meta.preview_image %} {% else %} {% endif %} {% endblock %} {% block scripts %} {{ super() }} {% endblock %} ================================================ FILE: docs/overrides/shortcodes.py ================================================ # Copyright (c) 2016-2024 Martin Donath # Copy from https://github.com/squidfunk/mkdocs-material/blob/master/src/overrides/hooks/shortcodes.py # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. from __future__ import annotations import posixpath import re from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import File, Files from mkdocs.structure.pages import Page from re import Match # ----------------------------------------------------------------------------- # Hooks # ----------------------------------------------------------------------------- # @todo def on_page_markdown( markdown: str, *, page: Page, config: MkDocsConfig, files: Files ): # Replace callback def replace(match: Match): type, args = match.groups() args = args.strip() if type == "version": if args.startswith("insiders-"): return _badge_for_version_insiders(args, page, files) else: return _badge_for_version(args, page, files) elif type == "sponsors": return _badge_for_sponsors(page, files) elif type == "flag": return flag(args, page, files) elif type == "option": return option(args) elif type == "setting": return setting(args) elif type == "feature": return _badge_for_feature(args, page, files) elif type == "plugin": return _badge_for_plugin(args, page, files) elif type == "extension": return _badge_for_extension(args, page, files) elif type == "utility": return _badge_for_utility(args, page, files) elif type == "example": return _badge_for_example(args, page, files) elif type == "default": if args == "none": return _badge_for_default_none(page, files) elif args == "computed": return _badge_for_default_computed(page, files) else: return _badge_for_default(args, page, files) # Otherwise, raise an error raise RuntimeError(f"Unknown shortcode: {type}") # Find and replace all external asset URLs in current page return re.sub( r"", replace, markdown, flags = re.I | re.M ) # ----------------------------------------------------------------------------- # Helper functions # ----------------------------------------------------------------------------- # Create a flag of a specific type def flag(args: str, page: Page, files: Files): type, *_ = args.split(" ", 1) if type == "experimental": return _badge_for_experimental(page, files) elif type == "required": return _badge_for_required(page, files) elif type == "customization": return _badge_for_customization(page, files) elif type == "metadata": return _badge_for_metadata(page, files) elif type == "multiple": return _badge_for_multiple(page, files) raise RuntimeError(f"Unknown type: {type}") # Create a linkable option def option(type: str): _, *_, name = re.split(r"[.:]", type) return f"[`{name}`](#+{type}){{ #+{type} }}\n\n" # Create a linkable setting - @todo append them to the bottom of the page def setting(type: str): _, *_, name = re.split(r"[.*]", type) return f"`{name}` {{ #{type} }}\n\n[{type}]: #{type}\n\n" # ----------------------------------------------------------------------------- # Resolve path of file relative to given page - the posixpath always includes # one additional level of `..` which we need to remove def _resolve_path(path: str, page: Page, files: Files): path, anchor, *_ = f"{path}#".split("#") path = _resolve(files.get_file_from_path(path), page) return "#".join([path, anchor]) if anchor else path # Resolve path of file relative to given page - the posixpath always includes # one additional level of `..` which we need to remove def _resolve(file: File, page: Page): path = posixpath.relpath(file.src_uri, page.file.src_uri) return posixpath.sep.join(path.split(posixpath.sep)[1:]) # ----------------------------------------------------------------------------- # Create badge def _badge(icon: str, text: str = "", type: str = ""): classes = f"mdx-badge mdx-badge--{type}" if type else "mdx-badge" return "".join([ f"", *([f"{icon}"] if icon else []), *([f"{text}"] if text else []), f"", ]) # Create sponsors badge def _badge_for_sponsors(page: Page, files: Files): icon = "material-heart" href = _resolve_path("features.md", page, files) return _badge( icon = f"[:{icon}:]({href} 'Sponsors only')", type = "heart" ) # Create badge for version def _badge_for_version(text: str, page: Page, files: Files): spec = text path = f"https://github.com/fruitcake/laravel-debugbar/releases/tag/{spec}" # Return badge icon = "material-tag-outline" href = f"{path}" return _badge( icon = f"[:{icon}:]({href} 'Minimum version')", text = f"[{text}]({path})" if spec else "" ) # Create badge for feature def _badge_for_feature(text: str, page: Page, files: Files): icon = "material-toggle-switch" href = _resolve_path("features.md#config", page, files) return _badge( icon = f"[:{icon}:]({href} 'Configurable feature')", text = text ) # Create badge for plugin def _badge_for_plugin(text: str, page: Page, files: Files): icon = "material-floppy" href = _resolve_path("features.md#plugin", page, files) return _badge( icon = f"[:{icon}:]({href} 'Plugin')", text = text ) # Create badge for extension def _badge_for_extension(text: str, page: Page, files: Files): icon = "material-language-markdown" href = _resolve_path("features.md#extension", page, files) return _badge( icon = f"[:{icon}:]({href} 'Markdown extension')", text = text ) # Create badge for utility def _badge_for_utility(text: str, page: Page, files: Files): icon = "material-package-variant" href = _resolve_path("features.md#utility", page, files) return _badge( icon = f"[:{icon}:]({href} 'Third-party utility')", text = text ) # Create badge for example def _badge_for_example(text: str, page: Page, files: Files): return "\n".join([ _badge_for_example_download(text, page, files), _badge_for_example_view(text, page, files) ]) # Create badge for example view def _badge_for_example_view(text: str, page: Page, files: Files): icon = "material-folder-eye" href = f"https://mkdocs-material.github.io/examples/{text}/" return _badge( icon = f"[:{icon}:]({href} 'View example')", type = "right" ) # Create badge for example download def _badge_for_example_download(text: str, page: Page, files: Files): icon = "material-folder-download" href = f"https://mkdocs-material.github.io/examples/{text}.zip" return _badge( icon = f"[:{icon}:]({href} 'Download example')", text = f"[`.zip`]({href})", type = "right" ) # Create badge for default value def _badge_for_default(text: str, page: Page, files: Files): icon = "material-water" href = _resolve_path("features.md#config", page, files) return _badge( icon = f"[:{icon}:]({href} 'Default value')", text = text ) # Create badge for empty default value def _badge_for_default_none(page: Page, files: Files): icon = "material-water-outline" href = _resolve_path("features.md#config", page, files) return _badge( icon = f"[:{icon}:]({href} 'Default value is empty')" ) # Create badge for computed default value def _badge_for_default_computed(page: Page, files: Files): icon = "material-water-check" href = _resolve_path("features.md#default", page, files) return _badge( icon = f"[:{icon}:]({href} 'Default value is computed')" ) # Create badge for metadata property flag def _badge_for_metadata(page: Page, files: Files): icon = "material-list-box-outline" href = _resolve_path("features.md#metadata", page, files) return _badge( icon = f"[:{icon}:]({href} 'Metadata property')" ) # Create badge for required value flag def _badge_for_required(page: Page, files: Files): icon = "material-alert" href = _resolve_path("features.md#required", page, files) return _badge( icon = f"[:{icon}:]({href} 'Required value')" ) # Create badge for customization flag def _badge_for_customization(page: Page, files: Files): icon = "material-brush-variant" href = _resolve_path("features.md#custom", page, files) return _badge( icon = f"[:{icon}:]({href} 'Customization')" ) # Create badge for multiple instance flag def _badge_for_multiple(page: Page, files: Files): icon = "material-inbox-multiple" href = _resolve_path("features.md#multiple", page, files) return _badge( icon = f"[:{icon}:]({href} 'Multiple instances')" ) # Create badge for experimental flag def _badge_for_experimental(page: Page, files: Files): icon = "material-flask-outline" href = _resolve_path("features.md#experimental", page, files) return _badge( icon = f"[:{icon}:]({href} 'Experimental')" ) ================================================ FILE: docs/usage.md ================================================ --- description: Using Laravel Debugbar is simple. After installing, just enable Debug mode and you should be good. Read further for more options. preview_image: img/preview-usage.jpg --- # Usage ## Using the Debugbar When the Debugbar is enabled, the Debugbar is shown on the bottom of the screen, similar to the documentation preview. Based on your configuration, it shows the [Collectors](collectors.md) for the current request. You can open, close, restore or minimize the toolbar for your need. The state will be remembered. ![Usage](img/debugbar.gif) ## Debugbar Facade You can now add messages using the Facade (when added), using the PSR-3 levels (debug, info, notice, warning, error, critical, alert, emergency): ```php Debugbar::info($object); Debugbar::error('Error!'); Debugbar::warning('Watch out…'); Debugbar::addMessage('Another message', 'mylabel'); ``` And start/stop timing: ```php Debugbar::startMeasure('render','Time for rendering'); Debugbar::stopMeasure('render'); Debugbar::addMeasure('now', LARAVEL_START, microtime(true)); Debugbar::measure('My long operation', function() { // Do something… }); ``` Or log exceptions: ```php try { throw new Exception('foobar'); } catch (Exception $e) { Debugbar::addThrowable($e); } ``` ## Helpers There are also helper functions available for the most common calls: ```php // 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)); measure('My long operation', function() { // Do something… }); ``` If you want you can add your own DataCollectors, through the Container or the Facade: ```php Debugbar::addCollector(new DebugBar\DataCollector\MessagesCollector('my_messages')); //Or via the App container: $debugbar = App::make('debugbar'); $debugbar->addCollector(new DebugBar\DataCollector\MessagesCollector('my_messages')); ``` ## Collecting Queued Jobs If you want to collect jobs, set `debugbar.collect_jobs` to `true` in the config (or `DEBUGBAR_COLLECT_JOBS` in your `.env`). Use the browse button to view the processed jobs. ## Enabling/Disabling on run time You can enable or disable the debugbar during run time. ```php debugbar()->enable(); debugbar()->disable(); ``` 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. ## Console When using Console Commands, you can log data to the Debugbar by manually enabling the debugbar. You can then view the data by browsing the Debugbar requests in the UI. ```php debugber()->enable(); ``` ## 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 Laravel Debugbar comes with two Twig Extensions. These are tested with [rcrowe/TwigBridge](https://github.com/rcrowe/TwigBridge) 0.6.x Add the following extensions to your TwigBridge config/extensions.php (or register the extensions manually) ```php 'Fruitcake\LaravelDebugbar\Twig\Extension\Debug', 'Fruitcake\LaravelDebugbar\Twig\Extension\Dump', 'Fruitcake\LaravelDebugbar\Twig\Extension\Stopwatch', ``` The Dump extension will replace the [dump function](http://twig.sensiolabs.org/doc/functions/dump.html) to output variables using the DataFormatter. The Debug extension adds a `debug()` function which passes variables to the Message Collector, instead of showing it directly in the template. It dumps the arguments, or when empty; all context variables. ```twig {{ debug() }} {{ debug(user, categories) }} ``` The Stopwatch extension adds a [stopwatch tag](http://symfony.com/blog/new-in-symfony-2-4-a-stopwatch-tag-for-twig) similar to the one in Symfony/Silex Twigbridge. ```twig {% stopwatch "foo" %} …some things that gets timed {% endstopwatch %} ``` ================================================ FILE: eslint.config.js ================================================ import antfu from '@antfu/eslint-config'; import globals from 'globals'; export default antfu( { type: 'app', // Disable TypeScript, Vue, etc. since this is vanilla JS typescript: false, vue: false, react: false, jsonc: false, yaml: false, markdown: false, ignores: [ 'vendor/**', 'tests/**', 'docs/**', 'resources/vendor/**' ], // Stylistic formatting rules stylistic: { indent: 4, quotes: 'single', semi: true } }, // Custom rules for the project { rules: { // Allow console in debug library 'no-console': 'off', // Allow unused vars with _ prefix or Widget suffix 'unused-imports/no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^(_|.*Widget)$', caughtErrors: 'none' }], 'no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^(_|.*Widget)$', caughtErrors: 'none' }], // Code style 'style/brace-style': ['error', '1tbs'], 'style/comma-dangle': ['error', 'never'], 'style/no-mixed-operators': 'off', 'style/max-statements-per-line': 'off', // Relax some rules for legacy patterns 'no-prototype-builtins': 'off', 'no-sequences': 'off', 'no-unused-expressions': 'off', 'no-use-before-define': ['error', { functions: false, classes: true, variables: true }], 'unicorn/no-array-for-each': 'off', // JSDoc relaxed rules 'jsdoc/require-returns-description': 'off', 'jsdoc/check-param-names': 'off', // Allow function expressions (for Widget.extend pattern) 'func-style': 'off', 'antfu/consistent-list-newline': 'off', // Modern JavaScript requirements 'prefer-const': 'error', 'no-var': 'error', 'prefer-arrow-callback': 'warn', 'prefer-template': 'warn', 'object-shorthand': 'warn' } }, // Custom config for resources folder { files: ['resources/**/*.js'], languageOptions: { ecmaVersion: 2020, sourceType: 'script', globals: { ...globals.browser, PhpDebugBar: 'writable', hljs: 'readonly' } } } ); ================================================ FILE: mkdocs.yml ================================================ site_name: Laravel Debugbar site_author: Barry vd. Heuvel site_description: Debugbar for Laravel site_url: https://laraveldebugbar.com repo_url: https://github.com/barryvdh/laravel-debugbar edit_uri: ./edit/docs/docs copyright: Copyright © Barry vd. Heuvel nav: - Home: index.md - Install: installation.md - Usage: usage.md - Features: features.md - Collectors: collectors.md theme: name: material custom_dir: docs/overrides logo: assets/logo_white.png favicon: assets/favicon.png palette: - primary: custom accent: custom scheme: slate features: - navigation.tabs - navigation.tabs.sticky # - navigation.instant - navigation.tracking - navigation.indexes - navigation.top - navigation.footer - navigation.sections - navigation.expand - content.tooltips - content.code.copy - content.action.edit - toc.follow - toc.integrate - search.highlight - meta extra_css: - assets/extra.css - assets/dist/debugbar.css?v=1767612540 extra_javascript: - assets/dist/debugbar.js?v=1767612540 markdown_extensions: - admonition - abbr - attr_list - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: use_pygments: true anchor_linenums: true line_spans: __span pygments_lang_class: true extend_pygments_lang: - name: php lang: php options: startinline: true - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences - pymdownx.details - toc: permalink: true - pymdownx.magiclink: normalize_issue_symbols: true repo_url_shorthand: true user: barryvdh repo: laravel-debugbar extra: analytics: provider: google property: G-KVHB3EWT91 social: - icon: fontawesome/brands/github link: https://github.com/barryvdh/laravel-debugbar hooks: - docs/overrides/shortcodes.py ================================================ FILE: package.json ================================================ { "name": "php-debugbar", "version": "1.0.0", "description": "[![Latest Stable Version](https://img.shields.io/packagist/v/php-debugbar/php-debugbar?label=Stable)](https://packagist.org/packages/php-debugbar/php-debugbar) [![Total Downloads](https://img.shields.io/packagist/dt/maximebf/debugbar?label=Downloads)](https://packagist.org/packages/php-debugbar/php-debugbar) [![License](https://img.shields.io/badge/Licence-MIT-4d9283)](https://packagist.org/packages/php-debugbar/php-debugbar) [![Tests](https://github.com/maximebf/php-debugbar/actions/workflows/run-tests.yml/badge.svg)](https://github.com/php-debugbar/php-debugbar/actions/workflows/run-tests.yml)", "main": "index.js", "directories": { "doc": "docs", "test": "tests" }, "type": "module", "scripts": { "build": "npm run build:icons", "build:icons": "node build/build-icons.js", "lint": "eslint resources/**/*.js", "lint:fix": "eslint resources/**/*.js --fix", "lint:report": "eslint resources/**/*.js --output-file eslint-report.txt --format unix", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@antfu/eslint-config": "^6.7.1", "@eslint/js": "^9.39.2", "esbuild": "^0.27.2", "eslint": "^9.39.2", "eslint-plugin-jquery": "^1.5.1", "globals": "^16.5.0" }, "dependencies": { "@tabler/icons": "^3.36.0" } } ================================================ FILE: phpstan.neon ================================================ includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/shipmonk/phpstan-rules/rules.neon parameters: level: 5 paths: - src strictRules: dynamicCallOnStaticMethod: false booleansInConditions: false disallowedShortTernary: false noVariableVariables: false strictArrayFilter: false shipmonkRules: enableAllRules: false enforceNativeReturnTypehint: enabled: true tmpDir: build/phpstan excludePaths: - 'src/Twig' - 'src/Controllers/TelescopeController.php' - 'src/DataCollector/PennantCollector.php' ================================================ FILE: phpunit.xml.dist ================================================ tests src/ ================================================ FILE: pint.json ================================================ { "preset": "per", "rules": { "@autoPHPMigration": true, "@autoPHPUnitMigration:risky": true, "array_push": true, "modernize_strpos": true, "modernize_types_casting": true, "trim_array_spaces": true, "no_short_bool_cast": true, "cast_spaces": true, "no_leading_namespace_whitespace": true, "no_unused_imports": true, "single_space_after_construct": true, "no_unneeded_curly_braces": true, "no_useless_else": true, "no_useless_return": true, "no_extra_blank_lines": true, "no_superfluous_phpdoc_tags": true, "no_empty_phpdoc": true, "phpdoc_align": true, "phpdoc_separation": true, "declare_strict_types": true } } ================================================ FILE: readme.md ================================================ ## Debugbar for Laravel ![Unit Tests](https://github.com/fruitcake/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/fruitcake/laravel-debugbar?label=Stable)](https://packagist.org/packages/fruitcake/laravel-debugbar) [![Total Downloads](https://img.shields.io/packagist/dt/barryvdh/laravel-debugbar?label=Downloads)](https://packagist.org/packages/fruitcake/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 Ajax/Livewire Requests. (Shown in a dropdown) Read [the documentation](http://phpdebugbar.com/docs/) for more configuration options. ![Debugbar Dark Mode screenshot](https://github.com/fruitcake/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). > [!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 - RouteCollector: Show information about the current Route. - ViewCollector: Show the currently loaded views. (Optionally: display the shared data) - EventsCollector: Show all events - LaravelCollector: Show the Laravel version and Environment. (disabled by default) - SymfonyRequestCollector: replaces the RequestCollector with more information about the request/response - LogsCollector: Show the latest log entries from the storage logs. (disabled by default) - FilesCollector: Show the files that are included/required by PHP. (disabled by default) - ConfigCollector: Display the values from the config files. (disabled by default) - CacheCollector: Display all cache events. (disabled by default) Bootstraps the following collectors for Laravel: - LogCollector: Show all Log messages - SymfonyMailCollector for Mail And the default collectors: - PhpInfoCollector - MessagesCollector - TimeDataCollector (With Booting and Application timing) - MemoryCollector - ExceptionsCollector It also provides a facade interface (`Debugbar`) for easy logging Messages, Exceptions and Time ## Installation Require this package with composer. It is recommended to only require the package for development. ```shell composer require fruitcake/laravel-debugbar --dev ``` > Note: The package name has changed to `fruitcake/laravel-debugbar`. If you're using `barryvdh/laravel-debugbar`, > you can safely replace this with the new package name: `composer remove barryvdh/laravel-debugbar --dev --no-scripts` > Tip: Use 'composer require fruitcake/laravel-debugbar:"^4@beta" --dev' flag to try the new 4.x Beta version! 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` and when the environment is not `production` or `testing`. You can disable it in the config (`debugbar.enabled`) or by setting `DEBUGBAR_ENABLED` in your `.env`. See more options in `config/debugbar.php` You can also set in your config if you want to include/exclude the vendor files also (FontAwesome, Highlight.js and jQuery). If you already use them in your site, set it to false. You can also only display the js or css vendors, by setting it to 'js' or 'css'. (Highlight.js requires both css + js, so set to `true` for syntax highlighting) #### Copy the package config to your local config with the publish command: ```shell php artisan vendor:publish --provider='Fruitcake\LaravelDebugbar\ServiceProvider' ``` ### Laravel with Octane: Laravel Debugbar 4.x works out of the box with Octane. No need to add anything to your config. If you're upgrading from Laravel Debugbar 3.x, remove the 'flush' config for Debugbar in `config/octane.php`. ## Usage You can now add messages using the Facade (when added), using the PSR-3 levels (debug, info, notice, warning, error, critical, alert, emergency): ```php Debugbar::info($object); Debugbar::error('Error!'); Debugbar::warning('Watch out…'); Debugbar::addMessage('Another message', 'mylabel'); ``` And start/stop timing: ```php Debugbar::startMeasure('render','Time for rendering'); Debugbar::stopMeasure('render'); Debugbar::addMeasure('now', LARAVEL_START, microtime(true)); Debugbar::measure('My long operation', function() { // Do something… }); ``` Or log exceptions: ```php try { throw new Exception('foobar'); } catch (Exception $e) { Debugbar::addThrowable($e); } ``` There are also helper functions available for the most common calls: ```php // 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)); measure('My long operation', function() { // Do something… }); ``` If you want you can add your own DataCollectors, through the Container or the Facade: ```php Debugbar::addCollector(new DebugBar\DataCollector\MessagesCollector('my_messages')); //Or via the App container: $debugbar = App::make('debugbar'); $debugbar->addCollector(new DebugBar\DataCollector\MessagesCollector('my_messages')); ``` By default, the Debugbar is injected just before ``. If you want to inject the Debugbar yourself, set the config option 'inject' to false and use the renderer yourself and follow http://phpdebugbar.com/docs/rendering.html ```php $renderer = Debugbar::getJavascriptRenderer(); ``` Note: Not using the auto-inject, will disable the Request information, because that is added After the response. You can add the default_request datacollector in the config as alternative. ## Enabling/Disabling on run time You can enable or disable the debugbar during run time. ```php \Debugbar::enable(); \Debugbar::disable(); ``` 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 Laravel Debugbar comes with two Twig Extensions. These are tested with [rcrowe/TwigBridge](https://github.com/rcrowe/TwigBridge) 0.6.x Add the following extensions to your TwigBridge config/extensions.php (or register the extensions manually) ```php 'Fruitcake\LaravelDebugbar\Twig\Extension\Debug', 'Fruitcake\LaravelDebugbar\Twig\Extension\Dump', 'Fruitcake\LaravelDebugbar\Twig\Extension\Stopwatch', ``` The Dump extension will replace the [dump function](http://twig.sensiolabs.org/doc/functions/dump.html) to output variables using the DataFormatter. The Debug extension adds a `debug()` function which passes variables to the Message Collector, instead of showing it directly in the template. It dumps the arguments, or when empty; all context variables. ```twig {{ debug() }} {{ debug(user, categories) }} ``` The Stopwatch extension adds a [stopwatch tag](http://symfony.com/blog/new-in-symfony-2-4-a-stopwatch-tag-for-twig) similar to the one in Symfony/Silex Twigbridge. ```twig {% stopwatch "foo" %} …some things that gets timed {% endstopwatch %} ``` ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=fruitcake/laravel-debugbar&type=Date)](https://www.star-history.com/#fruitcake/laravel-debugbar&Date) ================================================ FILE: resources/cache/widget.js ================================================ (function () { const csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); /** * Widget for the displaying cache events * * Options: * - data */ class LaravelCacheWidget extends PhpDebugBar.Widgets.TimelineWidget { get tagName() { return 'ul'; } get className() { return csscls('timeline cache'); } onForgetClick(e, el) { e.stopPropagation(); fetch(el.getAttribute('data-url'), { method: 'DELETE' }).then((response) => { if (response.ok) { el.style.transition = 'opacity 200ms'; el.style.opacity = '0'; setTimeout(() => el.remove(), 200); } }).catch((err) => { console.error('Failed to forget cache key:', err); }); } render() { super.render(); this.bindAttr('data', function (data) { if (data.measures) { const lines = this.el.querySelectorAll(`.${csscls('measure')}`); for (let i = 0; i < data.measures.length; i++) { const measure = data.measures[i]; const m = lines[i]; if (measure.params && Object.keys(measure.params).length > 0) { if (measure.params.delete) { const nextElement = m.nextElementSibling; if (nextElement) { const deleteRow = Array.from(nextElement.querySelectorAll('td.phpdebugbar-widgets-name')) .find(td => td.textContent.includes('delete')); if (deleteRow) { deleteRow.closest('tr')?.remove(); } } } if (measure.delete_url && measure.params.key) { const forgetLink = document.createElement('a'); forgetLink.className = csscls('forget'); forgetLink.textContent = 'forget'; forgetLink.setAttribute('data-url', measure.delete_url); forgetLink.addEventListener('click', (e) => { this.onForgetClick(e, forgetLink); }, { once: true }); m.appendChild(forgetLink); } } } } }); } } PhpDebugBar.Widgets.LaravelCacheWidget = LaravelCacheWidget; })(); ================================================ FILE: resources/laravel-debugbar.css ================================================ div.phpdebugbar, div.phpdebugbar-openhandler, div.phpdebugbar-widgets-datasets-panel { --debugbar-red-vivid: #eb4432; /*--debugbar-background: #fff;*/ /*--debugbar-background-alt: #EFEFEF;*/ /*--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; --debugbar-icon-brand: var(--debugbar-icon-brand-laravel); } /* Dark mode */ div.phpdebugbar[data-theme='dark'], div.phpdebugbar-openhandler[data-theme='dark'], div.phpdebugbar-widgets-datasets-panel[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-400); --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-openhandler-overlay { cursor: pointer; } div.phpdebugbar-openhandler { border-top: 3px solid var(--debugbar-red-vivid); } div.phpdebugbar-resize-handle { height: 3px; margin-top: -3px; border-bottom: 0; background-color: var(--debugbar-red-vivid); } div.phpdebugbar-closed, div.phpdebugbar-minimized { border-top-color: var(--debugbar-border); } div.phpdebugbar code, div.phpdebugbar pre { color: var(--debugbar-text); } div.phpdebugbar pre.sf-dump { color: var(--debugbar-text); } div.phpdebugbar-body { border-top: 1px solid var(--debugbar-header-border); } a.phpdebugbar-restore-btn:after { background: var(--debugbar-red-vivid); } a.phpdebugbar-restore-btn { border-right-color: var(--debugbar-border) !important; } div.phpdebugbar-header .phpdebugbar-tab { border-left: 1px solid var(--debugbar-header-border); } dl.phpdebugbar-widgets-kvlist dt, dl.phpdebugbar-widgets-kvlist dd, table.phpdebugbar-widgets-tablevar td { border-top: 0px; } 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 { background-color: #6d6d6d; 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; } 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; } 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-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-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter:hover { color: #FFF; } a.phpdebugbar-tab.phpdebugbar-active { background: var(--debugbar-red-vivid); color: #fff !important; } a.phpdebugbar-tab.phpdebugbar-active span.phpdebugbar-badge { background-color: var(--debugbar-badge); color: var(--debugbar-badge-text); } a.phpdebugbar-tab span.phpdebugbar-badge { border-radius: 10px; padding: 2px 8px; background: var(--debugbar-red-vivid); color: var(--debugbar-badge-active-text); } .phpdebugbar-widgets-datasets-switcher-widget .phpdebugbar-widgets-datasets-badge-count { background: var(--debugbar-red-vivid); color: var(--debugbar-badge-active-text); border-radius: 10px; padding: 0 4px; } .phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-headers { border-left: 2px solid var(--debugbar-header); } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item { border: none; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-stmt-id a { color: #888; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-stmt-id a:hover { color: #aaa; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item table.phpdebugbar-widgets-params { border-left: 2px solid var(--debugbar-border); } div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params th { 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); } 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.phpdebugbar-widgets-error:before { color: var(--debugbar-red-vivid); } div.phpdebugbar-panel div.phpdebugbar-widgets-status { background: var(--debugbar-background-alt) !important; border-bottom: 1px solid var(--debugbar-border) !important; } div.phpdebugbar-panel div.phpdebugbar-widgets-status > * { color: var(--debugbar-header-text)!important; } div.phpdebugbar-panel div.phpdebugbar-widgets-status > span:first-child:before { background-color: var(--debugbar-icons); -webkit-mask-image: var(--debugbar-icon-info-circle); mask-image: var(--debugbar-icon-info-circle); } 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);*/ } ul.phpdebugbar-widgets-cache a.phpdebugbar-widgets-forget { float: right; font-size: 12px; padding: 0 4px; background: var(--debugbar-red-vivid); margin: 0 2px; border-radius: 4px; color: #fff; text-decoration: none; line-height: 1.25rem; } div.phpdebugbar-header-right > * { border-right: 1px solid var(--debugbar-header); } dl.phpdebugbar-widgets-kvlist > :nth-child(4n-1), dl.phpdebugbar-widgets-kvlist > :nth-child(4n) { background-color: var(--debugbar-background-alt); } div.phpdebugbar dl.phpdebugbar-widgets-kvlist > :nth-child(4n)::before { background-color: var(--debugbar-background-alt); } div.phpdebugbar-widgets-sqlqueries a.phpdebugbar-widgets-connection { background: #737373; color: #fff !important; } div.phpdebugbar-widgets-sqlqueries button.phpdebugbar-widgets-explain-btn { background: #383838; color: #fff; } div.phpdebugbar-widgets-explain-scroll { overflow: auto; max-height: 300px; } div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain { width: 100%; border-collapse: collapse; } div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain th { border-bottom: 1px solid var(--debugbar-border); padding: 2px 5px; position: sticky; top: 0; z-index: 1; } div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain td { color: var(--debugbar-text-muted); padding: 2px 5px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-explain td.phpdebugbar-widgets-cell-expanded { white-space: normal; word-break: break-all; max-width: none; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(even) table.phpdebugbar-widgets-explain th { background-color: #fff; } ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(odd) table.phpdebugbar-widgets-explain th { background-color: #f5f5f5; } div.phpdebugbar[data-theme='dark'] ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(even) table.phpdebugbar-widgets-explain th { background-color: #1e1e1e; } div.phpdebugbar[data-theme='dark'] ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(odd) table.phpdebugbar-widgets-explain th { background-color: #2a2a2a; } div.phpdebugbar-widgets-explain-btnbar { display: flex; gap: 5px; margin-bottom: 5px; } div.phpdebugbar-widgets-explain-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 100000010; display: flex; align-items: center; justify-content: center; } div.phpdebugbar-widgets-explain-popup { background: #fff; border: 1px solid var(--debugbar-border); border-radius: 4px; max-width: 90vw; max-height: 80vh; min-width: 400px; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); } div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-explain-popup { background: #1e1e1e; } div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-explain-popup-close { color: #999; } div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-explain-popup-close:hover { color: #fff; } div.phpdebugbar-widgets-explain-popup-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--debugbar-border); } span.phpdebugbar-widgets-explain-popup-title { font-family: monospace; font-size: 12px; color: var(--debugbar-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; margin-right: 10px; } button.phpdebugbar-widgets-explain-popup-close { background: none; border: none; color: var(--debugbar-text-muted); font-size: 16px; cursor: pointer; padding: 0 4px; } button.phpdebugbar-widgets-explain-popup-close:hover { color: var(--debugbar-text-color); } div.phpdebugbar-widgets-explain-popup-body { overflow: auto; flex: 1; } table.phpdebugbar-widgets-explain.phpdebugbar-widgets-explain-full td { max-width: none; white-space: normal; word-break: break-word; cursor: default; } div.phpdebugbar-widgets-explain-popup-body table.phpdebugbar-widgets-explain { width: 100%; } div.phpdebugbar-widgets-explain-popup-body table.phpdebugbar-widgets-explain th { padding: 6px 10px; position: sticky; top: 0; z-index: 1; background: #fff; border-bottom: 2px solid var(--debugbar-border); } div.phpdebugbar[data-theme='dark'] .phpdebugbar-widgets-explain-popup-body table.phpdebugbar-widgets-explain th { background: #1e1e1e; } div.phpdebugbar-widgets-explain-popup-body table.phpdebugbar-widgets-explain td { padding: 6px 10px; } span.phpdebugbar-widgets-visual-link a { color: var(--debugbar-text-muted); margin-left: 5px; font-size: 11px; } ================================================ FILE: resources/laravel-icons.css ================================================ /* Generated file - do not edit manually */ /* Generated from Tabler Icons */ :root { --debugbar-icon-archive: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M3%204m0%202a2%202%200%200%201%202%20-2h14a2%202%200%200%201%202%202v0a2%202%200%200%201%20-2%202h-14a2%202%200%200%201%20-2%20-2z%22%20%2F%3E%20%3Cpath%20d%3D%22M5%208v10a2%202%200%200%200%202%202h10a2%202%200%200%200%202%20-2v-10%22%20%2F%3E%20%3Cpath%20d%3D%22M10%2012l4%200%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-clipboard-text: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M9%205h-2a2%202%200%200%200%20-2%202v12a2%202%200%200%200%202%202h10a2%202%200%200%200%202%20-2v-12a2%202%200%200%200%20-2%20-2h-2%22%20%2F%3E%20%3Cpath%20d%3D%22M9%203m0%202a2%202%200%200%201%202%20-2h2a2%202%200%200%201%202%202v0a2%202%200%200%201%20-2%202h-2a2%202%200%200%201%20-2%20-2z%22%20%2F%3E%20%3Cpath%20d%3D%22M9%2012h6%22%20%2F%3E%20%3Cpath%20d%3D%22M9%2016h6%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-files: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M15%203v4a1%201%200%200%200%201%201h4%22%20%2F%3E%20%3Cpath%20d%3D%22M18%2017h-7a2%202%200%200%201%20-2%20-2v-10a2%202%200%200%201%202%20-2h4l5%205v7a2%202%200%200%201%20-2%202z%22%20%2F%3E%20%3Cpath%20d%3D%22M16%2017v2a2%202%200%200%201%20-2%202h-7a2%202%200%200%201%20-2%20-2v-10a2%202%200%200%201%202%20-2h2%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-lock: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M5%2013a2%202%200%200%201%202%20-2h10a2%202%200%200%201%202%202v6a2%202%200%200%201%20-2%202h-10a2%202%200%200%201%20-2%20-2v-6z%22%20%2F%3E%20%3Cpath%20d%3D%22M11%2016a1%201%200%201%200%202%200a1%201%200%200%200%20-2%200%22%20%2F%3E%20%3Cpath%20d%3D%22M8%2011v-4a4%204%200%201%201%208%200v4%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-user: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M8%207a4%204%200%201%200%208%200a4%204%200%200%200%20-8%200%22%20%2F%3E%20%3Cpath%20d%3D%22M6%2021v-2a4%204%200%200%201%204%20-4h4a4%204%200%200%201%204%204v2%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-share-3: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M13%204v4c-6.575%201.028%20-9.02%206.788%20-10%2012c-.037%20.206%205.384%20-5.962%2010%20-6v4l8%20-7l-8%20-7z%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-subtask: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M6%209l6%200%22%20%2F%3E%20%3Cpath%20d%3D%22M4%205l4%200%22%20%2F%3E%20%3Cpath%20d%3D%22M6%205v11a1%201%200%200%200%201%201h5%22%20%2F%3E%20%3Cpath%20d%3D%22M12%207m0%201a1%201%200%200%201%201%20-1h6a1%201%200%200%201%201%201v2a1%201%200%200%201%20-1%201h-6a1%201%200%200%201%20-1%20-1z%22%20%2F%3E%20%3Cpath%20d%3D%22M12%2015m0%201a1%201%200%200%201%201%20-1h6a1%201%200%200%201%201%201v2a1%201%200%200%201%20-1%201h-6a1%201%200%200%201%20-1%20-1z%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-brand-laravel: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%221%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M3%2017l8%205l7%20-4v-8l-4%20-2.5l4%20-2.5l4%202.5v4l-11%206.5l-4%20-2.5v-7.5l-4%20-2.5z%22%20%2F%3E%20%3Cpath%20d%3D%22M11%2018v4%22%20%2F%3E%20%3Cpath%20d%3D%22M7%2015.5l7%20-4%22%20%2F%3E%20%3Cpath%20d%3D%22M14%207.5v4%22%20%2F%3E%20%3Cpath%20d%3D%22M14%2011.5l4%202.5%22%20%2F%3E%20%3Cpath%20d%3D%22M11%2013v-7.5l-4%20-2.5l-4%202.5%22%20%2F%3E%20%3Cpath%20d%3D%22M7%208l4%20-2.5%22%20%2F%3E%20%3Cpath%20d%3D%22M18%2010l4%20-2.5%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-brand-livewire: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%221%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M20.982%2018.777c-.372%20.548%20-.652%201.223%20-1.406%201.223c-1.269%200%20-1.337%20-1.913%20-2.607%20-1.913c-1.27%200%20-1.2%201.913%20-2.47%201.913c-1.268%200%20-1.337%20-1.913%20-2.607%20-1.913c-1.269%200%20-1.2%201.913%20-2.47%201.913c-1.268%200%20-1.337%20-1.913%20-2.607%20-1.913c-1.27%200%20-1.2%201.913%20-2.47%201.913c-.398%200%20-.679%20-.189%20-.915%20-.448a10.414%2010.414%200%200%201%20-1.43%20-5.29c0%20-5.669%204.477%20-10.262%2010%20-10.262c5.524%200%2010%204.594%2010%2010.261c0%201.62%20-.366%203.152%20-1.018%204.516z%22%20%2F%3E%20%3Cpath%20d%3D%22M20.982%2018.777c-.372%20.548%20-.652%201.223%20-1.406%201.223c-1.269%200%20-1.337%20-1.913%20-2.607%20-1.913c-1.27%200%20-1.2%201.913%20-2.47%201.913c-1.268%200%20-1.337%20-1.913%20-2.607%20-1.913c-1.269%200%20-1.2%201.913%20-2.47%201.913c-1.268%200%20-1.337%20-1.913%20-2.607%20-1.913c-1.27%200%20-1.2%201.913%20-2.47%201.913c-.398%200%20-.679%20-.189%20-.915%20-.448a10.414%2010.414%200%200%201%20-1.43%20-5.29c0%20-5.669%204.477%20-10.262%2010%20-10.262c5.524%200%2010%204.594%2010%2010.261c0%201.62%20-.366%203.152%20-1.018%204.516z%22%20%2F%3E%20%3Cpath%20d%3D%22M11.5%2016c3.167%200%204.5%20-1.748%204.5%20-4.231c0%20-2.484%20-2.014%20-4.769%20-4.5%20-4.769c-2.485%200%20-4.5%202.286%20-4.5%204.769s1.333%204.231%204.5%204.231z%22%20%2F%3E%20%3Cpath%20d%3D%22M10%2011a1%201%200%201%200%200%20-2a1%201%200%200%200%200%202z%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-brand-inertia: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%221%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M12.5%208l4%204l-4%204h4.5l4%20-4l-4%20-4z%22%20%2F%3E%20%3Cpath%20d%3D%22M3.5%208l4%204l-4%204h4.5l4%20-4l-4%20-4z%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-pin: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M15%204.5l-4%204l-4%201.5l-1.5%201.5l7%207l1.5%20-1.5l1.5%20-4l4%20-4%22%20%2F%3E%20%3Cpath%20d%3D%22M9%2015l-4.5%204.5%22%20%2F%3E%20%3Cpath%20d%3D%22M14.5%204l5.5%205.5%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-help-circle: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M3%2012a9%209%200%201%200%2018%200a9%209%200%200%200%20-18%200%22%20%2F%3E%20%3Cpath%20d%3D%22M12%2016v.01%22%20%2F%3E%20%3Cpath%20d%3D%22M12%2013a2%202%200%200%200%20.914%20-3.782a1.98%201.98%200%200%200%20-2.414%20.483%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-list: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M9%206l11%200%22%20%2F%3E%20%3Cpath%20d%3D%22M9%2012l11%200%22%20%2F%3E%20%3Cpath%20d%3D%22M9%2018l11%200%22%20%2F%3E%20%3Cpath%20d%3D%22M5%206l0%20.01%22%20%2F%3E%20%3Cpath%20d%3D%22M5%2012l0%20.01%22%20%2F%3E%20%3Cpath%20d%3D%22M5%2018l0%20.01%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-gauge: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M12%2012m-9%200a9%209%200%201%200%2018%200a9%209%200%201%200%20-18%200%22%20%2F%3E%20%3Cpath%20d%3D%22M12%2012m-1%200a1%201%200%201%200%202%200a1%201%200%201%200%20-2%200%22%20%2F%3E%20%3Cpath%20d%3D%22M13.41%2010.59l2.59%20-2.59%22%20%2F%3E%20%3Cpath%20d%3D%22M7%2012a5%205%200%200%201%205%20-5%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-star: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22currentColor%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M8.243%207.34l-6.38%20.925l-.113%20.023a1%201%200%200%200%20-.44%201.684l4.622%204.499l-1.09%206.355l-.013%20.11a1%201%200%200%200%201.464%20.944l5.706%20-3l5.693%203l.1%20.046a1%201%200%200%200%201.352%20-1.1l-1.091%20-6.355l4.624%20-4.5l.078%20-.085a1%201%200%200%200%20-.633%20-1.62l-6.38%20-.926l-2.852%20-5.78a1%201%200%200%200%20-1.794%200l-2.853%205.78z%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-info-circle: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M3%2012a9%209%200%201%200%2018%200a9%209%200%200%200%20-18%200%22%20%2F%3E%20%3Cpath%20d%3D%22M12%209h.01%22%20%2F%3E%20%3Cpath%20d%3D%22M11%2012h1v4h1%22%20%2F%3E%20%3C%2Fsvg%3E'); --debugbar-icon-external-link: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20%3E%20%3Cpath%20stroke%3D%22none%22%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%20%3Cpath%20d%3D%22M12%206h-6a2%202%200%200%200%20-2%202v10a2%202%200%200%200%202%202h10a2%202%200%200%200%202%20-2v-6%22%20%2F%3E%20%3Cpath%20d%3D%22M11%2013l9%20-9%22%20%2F%3E%20%3Cpath%20d%3D%22M15%204h5v5%22%20%2F%3E%20%3C%2Fsvg%3E'); } .phpdebugbar-icon-archive::before { -webkit-mask-image: var(--debugbar-icon-archive); mask-image: var(--debugbar-icon-archive); } .phpdebugbar-icon-clipboard-text::before { -webkit-mask-image: var(--debugbar-icon-clipboard-text); mask-image: var(--debugbar-icon-clipboard-text); } .phpdebugbar-icon-files::before { -webkit-mask-image: var(--debugbar-icon-files); mask-image: var(--debugbar-icon-files); } .phpdebugbar-icon-lock::before { -webkit-mask-image: var(--debugbar-icon-lock); mask-image: var(--debugbar-icon-lock); } .phpdebugbar-icon-user::before { -webkit-mask-image: var(--debugbar-icon-user); mask-image: var(--debugbar-icon-user); } .phpdebugbar-icon-share-3::before { -webkit-mask-image: var(--debugbar-icon-share-3); mask-image: var(--debugbar-icon-share-3); } .phpdebugbar-icon-subtask::before { -webkit-mask-image: var(--debugbar-icon-subtask); mask-image: var(--debugbar-icon-subtask); } .phpdebugbar-icon-brand-laravel::before { -webkit-mask-image: var(--debugbar-icon-brand-laravel); mask-image: var(--debugbar-icon-brand-laravel); } .phpdebugbar-icon-brand-livewire::before { -webkit-mask-image: var(--debugbar-icon-brand-livewire); mask-image: var(--debugbar-icon-brand-livewire); } .phpdebugbar-icon-brand-inertia::before { -webkit-mask-image: var(--debugbar-icon-brand-inertia); mask-image: var(--debugbar-icon-brand-inertia); } .phpdebugbar-icon-pin::before { -webkit-mask-image: var(--debugbar-icon-pin); mask-image: var(--debugbar-icon-pin); } .phpdebugbar-icon-help-circle::before { -webkit-mask-image: var(--debugbar-icon-help-circle); mask-image: var(--debugbar-icon-help-circle); } .phpdebugbar-icon-list::before { -webkit-mask-image: var(--debugbar-icon-list); mask-image: var(--debugbar-icon-list); } .phpdebugbar-icon-gauge::before { -webkit-mask-image: var(--debugbar-icon-gauge); mask-image: var(--debugbar-icon-gauge); } .phpdebugbar-icon-star::before { -webkit-mask-image: var(--debugbar-icon-star); mask-image: var(--debugbar-icon-star); } .phpdebugbar-icon-info-circle::before { -webkit-mask-image: var(--debugbar-icon-info-circle); mask-image: var(--debugbar-icon-info-circle); } .phpdebugbar-icon-external-link::before { -webkit-mask-image: var(--debugbar-icon-external-link); mask-image: var(--debugbar-icon-external-link); } ================================================ FILE: resources/queries/widget.js ================================================ (function () { const csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); /** * Widget for displaying sql queries with Laravel-specific features. * Extends the base SQLQueriesWidget to add EXPLAIN functionality. * * Options: * - data */ class LaravelQueriesWidget extends PhpDebugBar.Widgets.SQLQueriesWidget { buildTable(rows, opts = {}) { const headings = []; for (const key in rows[0]) { const th = document.createElement('th'); th.textContent = key; headings.push(th); } const values = []; for (const row of rows) { const tr = document.createElement('tr'); for (const key in row) { const td = document.createElement('td'); const text = row[key] == null ? '' : String(row[key]); td.textContent = text; if (!opts.expanded) { td.title = text; td.addEventListener('click', (e) => { e.stopPropagation(); td.classList.toggle(csscls('cell-expanded')); if (td.classList.contains(csscls('cell-expanded'))) { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(td); selection.removeAllRanges(); selection.addRange(range); } }); } tr.append(td); } values.push(tr); } const table = document.createElement('table'); table.classList.add(csscls('explain')); if (opts.expanded) { table.classList.add(csscls('explain-full')); } const thead = document.createElement('thead'); const tbody = document.createElement('tbody'); const headerRow = document.createElement('tr'); headerRow.append(...headings); thead.append(headerRow); tbody.append(...values); table.append(thead, tbody); return table; } buildPgsqlTable(rows, opts = {}) { const values = []; for (const row of rows) { const tr = document.createElement('tr'); const td = document.createElement('td'); td.textContent = row; tr.append(td); values.push(tr); } const table = document.createElement('table'); table.classList.add(csscls('explain')); if (opts.expanded) { table.classList.add(csscls('explain-full')); } const tbody = document.createElement('tbody'); tbody.append(...values); table.append(tbody); return table; } actionButton(label, onClick) { const btn = document.createElement('button'); btn.textContent = label; btn.classList.add(csscls('explain-btn')); btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onClick(e); }); return btn; } fetchQuery(statement, mode, format) { const body = { connection: statement.explain.connection, query: statement.explain.query, bindings: statement.params, hash: statement.explain.hash, mode: mode, }; if (format) { body.format = format; } return fetch(statement.explain.url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then((response) => response.json().then((json) => { if (!response.ok) throw new Error(json.message || 'Request failed'); return json; }) ); } renderResult(container, statement, data, btnBar) { container.innerHTML = ''; const result = data.result; if (Array.isArray(result) && result.length > 0 && typeof result[0] === 'object') { const wrapper = document.createElement('div'); wrapper.classList.add(csscls('explain-scroll')); wrapper.append(this.buildTable(result)); container.append(wrapper); btnBar.append(this.actionButton('Expand', () => { this.showPopup(statement.explain.query, this.buildTable(result, { expanded: true })); })); } else { const empty = document.createElement('em'); empty.textContent = 'No results'; container.append(empty); } container.prepend(btnBar); } renderDump(container, statement, data, btnBar) { container.innerHTML = ''; PhpDebugBar.Widgets.renderValueInto(container, data.result); PhpDebugBar.utils.sfDump(container); container.prepend(btnBar); } renderExplain(container, statement, data, driver, btnBar) { container.innerHTML = ''; const rows = data; const buildFn = driver === 'pgsql' ? 'buildPgsqlTable' : 'buildTable'; const wrapper = document.createElement('div'); wrapper.classList.add(csscls('explain-scroll')); wrapper.append(this[buildFn](rows)); container.append(wrapper); btnBar.append(this.actionButton('Expand', () => { this.showPopup(statement.explain.query, this[buildFn](rows, { expanded: true })); })); container.prepend(btnBar); } showPopup(query, contentEl) { const overlay = document.createElement('div'); overlay.classList.add(csscls('explain-overlay')); const popup = document.createElement('div'); popup.classList.add(csscls('explain-popup')); const header = document.createElement('div'); header.classList.add(csscls('explain-popup-header')); const title = document.createElement('span'); title.innerHTML = PhpDebugBar.Widgets.highlight(query.length > 120 ? query.substring(0, 120) + '...' : query, 'sql'); title.classList.add(csscls('explain-popup-title')); const closeBtn = document.createElement('button'); closeBtn.textContent = '\u2715'; closeBtn.classList.add(csscls('explain-popup-close')); closeBtn.addEventListener('click', () => overlay.remove()); header.append(title, closeBtn); const body = document.createElement('div'); body.classList.add(csscls('explain-popup-body')); body.append(contentEl); popup.append(header, body); overlay.append(popup); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); document.addEventListener('keydown', function onEsc(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onEsc); } }); document.querySelector('div.phpdebugbar').append(overlay); } itemRenderer(li, stmt, filters) { // Call parent's item renderer first super.itemRenderer(li, stmt, filters); // Add explain button if available if (stmt.explain) { let table = li.querySelector(`.${csscls('params')}`); table.style.display = ''; if (stmt.explain.modes.includes('result')) { this.renderDetailSection(table, 'Result', stmt, 'result'); } if (stmt.explain.modes.includes('explain')) { this.renderDetailSection(table, 'Performance', stmt, 'explain'); } } } renderDetailSection(table, caption, statement, mode) { const thead = document.createElement('thead'); const tr = document.createElement('tr'); const th = document.createElement('th'); th.colSpan = 2; th.classList.add(csscls('name')); th.textContent = caption; tr.append(th); thead.append(tr); table.append(thead); const tbody = document.createElement('tbody'); const bodyTr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 2; const driver = statement.explain.driver; if (mode === 'result') { const makeBtnBar = () => { const bar = document.createElement('div'); bar.classList.add(csscls('explain-btnbar')); bar.append( this.actionButton('Run SELECT', () => { const btnBar = makeBtnBar(); this.fetchQuery(statement, 'result').then((json) => { this.renderResult(td, statement, json.data, btnBar); }).catch((e) => alert(e.message)); // eslint-disable-line no-alert }), this.actionButton('Run SELECT (dump)', () => { const btnBar = makeBtnBar(); this.fetchQuery(statement, 'result', 'dump').then((json) => { this.renderDump(td, statement, json.data, btnBar); }).catch((e) => alert(e.message)); // eslint-disable-line no-alert }) ); return bar; }; td.append(makeBtnBar()); } else { const run = () => { this.fetchQuery(statement, 'explain').then((json) => { const btnBar = document.createElement('div'); btnBar.classList.add(csscls('explain-btnbar')); btnBar.append(this.actionButton('Re-run EXPLAIN', run)); if (json.visual) { btnBar.append(this.buildVisualExplainButton(statement, json.visual.confirm)); } this.renderExplain(td, statement, json.data, driver, btnBar); }).catch((e) => alert(e.message)); // eslint-disable-line no-alert }; td.append(this.actionButton('Run EXPLAIN', run)); } bodyTr.append(td); tbody.append(bodyTr); table.append(tbody); } buildVisualExplainButton(statement, confirmMessage) { const linkContainer = document.createElement('span'); linkContainer.classList.add(csscls('visual-link')); const btn = this.actionButton('Visual Explain', () => { if (!confirm(confirmMessage)) // eslint-disable-line no-alert return; this.fetchQuery(statement, 'visual').then((json) => { linkContainer.innerHTML = ''; const link = document.createElement('a'); link.href = json.data; link.textContent = json.data; link.target = '_blank'; link.rel = 'noopener'; linkContainer.append(link); window.open(json.data, '_blank', 'noopener'); }).catch((e) => alert(e.message)); // eslint-disable-line no-alert }); const wrapper = document.createDocumentFragment(); wrapper.append(btn, linkContainer); return wrapper; } } PhpDebugBar.Widgets.LaravelQueriesWidget = LaravelQueriesWidget; })(); ================================================ FILE: src/CollectorProviders/AbstractCollectorProvider.php ================================================ debugbar->addCollector($collector); } public function hasCollector(string $name): bool { return $this->debugbar->hasCollector($name); } public function getCollector(string $name): DataCollectorInterface { return $this->debugbar->getCollector($name); } protected function addCollectorException(string $message, \Throwable $exception): void { $this->addThrowable( new \RuntimeException( $message . ' on Laravel Debugbar: ' . $exception->getMessage(), (int) $exception->getCode(), $exception, ), ); } /** * Adds an exception to be profiled in the debug bar */ public function addThrowable(\Throwable $e): void { if ($this->hasCollector('exceptions')) { /** @var \DebugBar\DataCollector\ExceptionsCollector $collector */ $collector = $this->getCollector('exceptions'); $collector->addThrowable($e); } } } ================================================ FILE: src/CollectorProviders/AuthCollectorProvider.php ================================================ addCollector($authCollector); $authCollector->setShowName($options['show_name'] ?? false); $authCollector->setShowGuardsData($options['show_guards'] ?? true); } } ================================================ FILE: src/CollectorProviders/CacheCollectorProvider.php ================================================ server('REQUEST_TIME_FLOAT'); $cacheCollector = new CacheCollector($startTime, $collectValues); $this->addCollector($cacheCollector); if ($options['timeline'] ?? false) { $cacheCollector->setTimeDataCollector($this->debugbar->getTimeCollector()); } $classMap = $cacheCollector->getCacheEvents(); foreach (array_keys($classMap) as $eventClass) { $events->listen($eventClass, function ($event) use ($cacheCollector): void { if ($this->debugbar->isEnabled()) { $cacheCollector->onCacheEvent($event); } }); } $startEvents = array_unique(array_filter(array_map( fn($values) => $values[1] ?? null, array_values($classMap), ))); foreach ($startEvents as $eventClass) { $events->listen($eventClass, function ($event) use ($cacheCollector): void { if ($this->debugbar->isEnabled()) { $cacheCollector->onStartCacheEvent($event); } }); } } } ================================================ FILE: src/CollectorProviders/ConfigCollectorProvider.php ================================================ addMaskedKeys(array_merge($masked, $options['masked'] ?? [])); $this->addCollector($configCollector); } } ================================================ FILE: src/CollectorProviders/DatabaseCollectorProvider.php ================================================ debugbar->getTimeCollector(); $queryCollector->setTimeDataCollector($timeCollector); } $queryCollector->setLimits($options['soft_limit'] ?? 100, $options['hard_limit'] ?? 500); $queryCollector->setDurationBackground($options['duration_background'] ?? true); $threshold = $options['slow_threshold'] ?? false; if ($threshold && !($options['only_slow_queries'] ?? true)) { $queryCollector->setSlowThreshold($threshold); } if ($options['with_params'] ?? true) { $queryCollector->setRenderSqlWithParams(true); } if ($backtrace = ($options['backtrace'] ?? true)) { $queryCollector->setFindSource($backtrace, $router->getMiddleware()); } if ($excludePaths = ($options['exclude_paths'] ?? [])) { $queryCollector->mergeExcludePaths($excludePaths); } if ($excludeBacktracePaths = ($options['backtrace_exclude_paths'] ?? [])) { $queryCollector->mergeBacktraceExcludePaths($excludeBacktracePaths); } if (($options['explain']['enabled'] ?? false) && $this->debugbar->isStorageOpen($request)) { $queryCollector->setExplainQuery(true); } if (($options['show_query_result'] ?? false) && $this->debugbar->isStorageOpen($request)) { $queryCollector->setShowQueryResult(true); } $this->addCollector($queryCollector); try { $events->listen( function (QueryExecuted $query) use ($queryCollector, $options): void { // In case Debugbar is disabled after the listener was attached if (!$this->debugbar->shouldCollect('db', true) || !$this->debugbar->isEnabled()) { return; } $threshold = $options['slow_threshold'] ?? false; $onlyThreshold = $options['only_slow_queries'] ?? true; //allow collecting only queries slower than a specified amount of milliseconds if (!$onlyThreshold || !$threshold || $query->time > $threshold) { $queryCollector->addQuery($query); } }, ); } catch (\Throwable $e) { $this->addCollectorException('Cannot listen to Queries', $e); } try { $events->listen( TransactionBeginning::class, fn($transaction) => $queryCollector->collectTransactionEvent('Begin Transaction', $transaction->connection), ); $events->listen( TransactionCommitted::class, fn($transaction) => $queryCollector->collectTransactionEvent('Commit Transaction', $transaction->connection), ); $events->listen( TransactionRolledBack::class, fn($transaction) => $queryCollector->collectTransactionEvent('Rollback Transaction', $transaction->connection), ); $events->listen( 'connection.*.beganTransaction', fn($event, $params) => $queryCollector->collectTransactionEvent('Begin Transaction', $params[0]), ); $events->listen( 'connection.*.committed', fn($event, $params) => $queryCollector->collectTransactionEvent('Commit Transaction', $params[0]), ); $events->listen( 'connection.*.rollingBack', fn($event, $params) => $queryCollector->collectTransactionEvent('Rollback Transaction', $params[0]), ); $events->listen( function (ConnectionEstablished $event) use ($queryCollector, $options): void { $queryCollector->collectTransactionEvent('Connection Established', $event->connection); if ($options['memory_usage'] ?? false) { $event->connection->beforeExecuting(function () use ($queryCollector): void { $queryCollector->startMemoryUsage(); }); } }, ); } catch (\Throwable $e) { $this->addCollectorException('Cannot listen to Queries', $e); } } } ================================================ FILE: src/CollectorProviders/EventsCollectorCollectorProvider.php ================================================ server('REQUEST_TIME_FLOAT'); $collectData = $options['data'] ?? false; $collectListeners = $options['listeners'] ?? false; $excludedEvents = $options['excluded'] ?? []; $eventCollector = new EventCollector($startTime ? (float) $startTime : null); if ($collectData) { $eventCollector->setCollectValues($collectData); } if ($collectListeners) { $eventCollector->setCollectListeners($collectListeners); } if ($excludedEvents) { $eventCollector->setExcludedEvents($excludedEvents); } $this->addCollector($eventCollector); $events->listen('*', function ($event, $data = []) use ($eventCollector): void { if ($this->debugbar->isEnabled()) { $eventCollector->onWildcardEvent($event, $data); } }); } } ================================================ FILE: src/CollectorProviders/ExceptionsCollectorProvider.php ================================================ debugbar->getExceptionsCollector(); $this->addCollector($exceptionCollector); $exceptionCollector->setChainExceptions($options['chain'] ?? true); } } ================================================ FILE: src/CollectorProviders/GateCollectorProvider.php ================================================ addCollector($gateCollector); if ($options['trace'] ?? false) { $gateCollector->collectFileTrace(true); $gateCollector->addBacktraceExcludePaths($options['exclude_paths'] ?? []); } if ($options['timeline'] ?? false) { $gateCollector->setTimeDataCollector($this->debugbar->getTimeCollector()); } $events->listen(GateEvaluated::class, fn(GateEvaluated $event) => $gateCollector->addCheck($event->user, $event->ability, $event->result, $event->arguments)); } } ================================================ FILE: src/CollectorProviders/HttpClientCollectorProvider.php ================================================ hasCollector('time') && ($options['timeline'] ?? true)) { /** @var TimeDataCollector $timeCollector */ $timeCollector = $this->getCollector('time'); $httpClientCollector->setTimeDataCollector($timeCollector); } $this->httpClientCollector = $httpClientCollector; $masked = $options['masked'] ?? []; $httpClientCollector->addMaskedKeys($masked); $this->addCollector($httpClientCollector); $events->listen(ResponseReceived::class, fn(ResponseReceived $e) => $this->addEvent($e)); $events->listen(ConnectionFailed::class, fn(ConnectionFailed $e) => $this->addEvent($e)); } protected function addEvent(ResponseReceived|ConnectionFailed $event): void { try { $this->httpClientCollector->addEvent($event); } catch (\Throwable $e) { $this->addThrowable($e); } } } ================================================ FILE: src/CollectorProviders/InertiaCollectorProvider.php ================================================ bound('inertia.view-finder')) { $inertiaCollector = new InertiaCollector(true, [], false); $this->addCollector($inertiaCollector); $events->listen(ResponsePrepared::class, fn(ResponsePrepared $e) => $inertiaCollector->addFromResponse($e->response)); $events->listen('composing:*', fn($event, $params) => $inertiaCollector->addFromView($params[0])); } } } ================================================ FILE: src/CollectorProviders/JobsCollectorProvider.php ================================================ addCollector($jobs); $events->listen(JobQueued::class, function ($event) use ($jobs): void { $jobs->countClass($event->job); }); } } ================================================ FILE: src/CollectorProviders/LaravelCollectorProvider.php ================================================ addCollector(new LaravelCollector()); } } ================================================ FILE: src/CollectorProviders/LivewireCollectorProvider.php ================================================ bound('livewire')) { $livewireCollector = new LivewireCollector(true, [], false); $this->addCollector($livewireCollector); Livewire::listen('render', fn(Component $component) => $livewireCollector->addLivewireComponent($component, $request)); } } } ================================================ FILE: src/CollectorProviders/LogCollectorProvider.php ================================================ hasCollector('messages')) { /** @var MessagesCollector $messagesCollector */ $messagesCollector = $this->getCollector('messages'); $messagesCollector->aggregate($logCollector); } else { $this->addCollector($logCollector); } $logger->listen( function (MessageLogged $log) use ($logCollector): void { try { $logMessage = $log->message; if (mb_check_encoding($logMessage, 'UTF-8')) { $context = $log->context; $logMessage .= ($context ? ' ' . json_encode($context, JSON_PRETTY_PRINT) : ''); } else { $logMessage = "[INVALID UTF-8 DATA]"; } } catch (\Throwable $e) { $logMessage = "[Exception: " . $e->getMessage() . "]"; } $logCollector->log( $log->level, '[' . date('H:i:s') . '] ' . "LOG.{$log->level}: " . $logMessage, $log->context, ); }, ); } } ================================================ FILE: src/CollectorProviders/LogsCollectorProvider.php ================================================ addCollector(new LogsCollector($file)); } } ================================================ FILE: src/CollectorProviders/MailCollectorProvider.php ================================================ addCollector($mailCollector); $events->listen(function (MessageSent $event) use ($mailCollector): void { $mailCollector->addSymfonyMessage($event->sent->getSymfonySentMessage()); }); if (($options['show_body'] ?? true) || ($options['full_log'] ?? false)) { $mailCollector->showMessageBody(); } if ($options['timeline'] ?? true) { $timeCollector = $this->debugbar->getTimeCollector(); $events->listen(MessageSending::class, fn(MessageSending $e) => $timeCollector->startMeasure('Mail: ' . $e->message->getSubject())); $events->listen(MessageSent::class, function (MessageSent $e) use ($timeCollector): void { $name = 'Mail: ' . $e->message->getSubject(); if ($timeCollector->hasStartedMeasure($name)) { $timeCollector->stopMeasure($name); } else { $timeCollector->addMeasure($name); } }); } } } ================================================ FILE: src/CollectorProviders/MemoryCollectorProvider.php ================================================ addCollector($memoryCollector); $memoryCollector->setPrecision($options['precision'] ?? 0); if (function_exists('memory_reset_peak_usage') && ($options['reset_peak_usage'] ?? false)) { memory_reset_peak_usage(); } if ($options['with_baseline'] ?? false) { $memoryCollector->resetMemoryBaseline(); } } } ================================================ FILE: src/CollectorProviders/MessagesCollectorProvider.php ================================================ debugbar->getMessagesCollector(); $this->addCollector($messageCollector); if ($options['trace'] ?? true) { $messageCollector->collectFileTrace(true); $excludePaths = $options['backtrace_exclude_paths'] ?? []; if ($excludePaths) { $messageCollector->addBacktraceExcludePaths($excludePaths); } } if ($options['timeline'] ?? true) { $messageCollector->setTimeDataCollector($this->debugbar->getTimeCollector()); } if ($options['capture_dumps'] ?? false) { $originalHandler = \Symfony\Component\VarDumper\VarDumper::setHandler(function ($var) use (&$originalHandler, $messageCollector): void { if ($originalHandler) { $originalHandler($var); } $messageCollector->addMessage($var); }); } } } ================================================ FILE: src/CollectorProviders/ModelsCollectorProvider.php ================================================ addCollector($modelsCollector); $eventList = ['retrieved', 'created', 'updated', 'deleted']; $modelsCollector->setKeyMap(array_combine($eventList, array_map('ucfirst', $eventList))); $modelsCollector->collectCountSummary(true); foreach ($eventList as $event) { $events->listen("eloquent.{$event}: *", function ($event, $models) use ($modelsCollector): void { $event = explode(': ', $event); $count = count(array_filter($models)); $modelsCollector->countClass($event[1], $count, explode('.', $event[0])[1]); }); } } } ================================================ FILE: src/CollectorProviders/PennantCollectorProvider.php ================================================ bound(FeatureManager::class) ) { $this->addCollector(new PennantCollector()); } } } ================================================ FILE: src/CollectorProviders/PhpInfoCollectorProvider.php ================================================ addCollector(new PhpInfoCollector()); } } ================================================ FILE: src/CollectorProviders/RequestCollectorProvider.php ================================================ addMaskedKeys($hiddens); $requestCollector->addMaskedKeys($masked); $this->addCollector($requestCollector); $events->listen(ResponsePrepared::class, fn(ResponsePrepared $e) => $requestCollector->setResponse($e->response)); } } ================================================ FILE: src/CollectorProviders/RouteCollectorProvider.php ================================================ addCollector(new RouteCollector()); } } ================================================ FILE: src/CollectorProviders/SessionCollectorProvider.php ================================================ addMaskedKeys($hiddens); $sessionCollector->addMaskedKeys((array) ($options['masked'] ?? [])); $this->addCollector($sessionCollector); } } ================================================ FILE: src/CollectorProviders/TimeCollectorProvider.php ================================================ hasCollector('time')) { /** @var TimeDataCollector $timeCollector */ $timeCollector = $this['time']; } else { $timeCollector = $this->debugbar->getTimeCollector(); $this->addCollector($timeCollector); } if ($options['memory_usage'] ?? false) { $timeCollector->showMemoryUsage(); } $events->listen(Routing::class, fn() => $timeCollector->startMeasure('Routing')); $events->listen(RouteMatched::class, fn() => $timeCollector->stopMeasure('Routing')); $events->listen(PreparingResponse::class, fn() => $timeCollector->startMeasure('Preparing Response')); $events->listen(ResponsePrepared::class, fn() => $timeCollector->stopMeasure('Preparing Response')); } } ================================================ FILE: src/CollectorProviders/ViewsCollectorProvider.php ================================================ debugbar->getTimeCollector(); $viewCollector->setTimeDataCollector($timeCollector); } $this->addCollector($viewCollector); $events->listen( 'composing:*', function ($event, $params) use ($viewCollector): void { $viewCollector->addView($params[0]); }, ); } } ================================================ FILE: src/Console/ClearCommand.php ================================================ boot(); if ($storage = $debugbar->getStorage()) { try { $storage->clear(); } catch (\InvalidArgumentException $e) { // hide InvalidArgumentException if storage location does not exist if (!str_contains($e->getMessage(), 'does not exist')) { throw $e; } } $this->info('Debugbar Storage cleared!'); } else { $this->error('No Debugbar Storage found..'); } } } ================================================ FILE: src/Controllers/AssetController.php ================================================ validated('type'); $response = new Response(); $driver = $debugbar->getHttpDriver(); if ($driver instanceof LaravelHttpDriver || $driver instanceof SymfonyHttpDriver) { $driver->setResponse($response); } $assetHandler->handle([ 'type' => $type, ]); return $response; } } ================================================ FILE: src/Controllers/CacheController.php ================================================ validated('tags')) { $cache = $cache->tags($tags); } $success = $cache->forget($key); return response()->json(compact('success')); } } ================================================ FILE: src/Controllers/OpenHandlerController.php ================================================ validated('op') !== 'get' && !$debugbar->isStorageOpen($request)) { return new JsonResponse([ [ '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), ], ]); } $response = new Response(); $driver = $debugbar->getHttpDriver(); if ($driver instanceof LaravelHttpDriver || $driver instanceof SymfonyHttpDriver) { $driver->setResponse($response); } $openHandler->handle($request->input()); return $response; } /** * Return Clockwork output * * @throws \DebugBar\DebugBarException */ public function clockwork(OpenHandler $openHandler, $id): \Illuminate\Http\JsonResponse { $request = [ 'op' => 'get', 'id' => $id, ]; $data = $openHandler->handle($request, false, false); // Convert to Clockwork $converter = new Converter(); $output = $converter->convert(json_decode($data, true)); return response()->json($output); } } ================================================ FILE: src/Controllers/QueriesController.php ================================================ validated(); if (($validated['mode'] ?? null) === 'result') { if (!config('debugbar.options.db.show_query_result', false) || !$debugbar->isStorageOpen($request)) { return response()->json([ 'success' => false, 'message' => 'Query result is currently disabled in the Debugbar.', ], 400); } return response()->json([ 'success' => true, 'data' => $explain->generateSelectResult($validated['connection'], $validated['query'], $validated['bindings'] ?? null, $validated['hash'], $validated['format'] ?? null), ]); } if (!config('debugbar.options.db.explain.enabled', false) || !$debugbar->isStorageOpen($request)) { return response()->json([ 'success' => false, 'message' => 'EXPLAIN is currently disabled in the Debugbar.', ], 400); } try { if (($validated['mode'] ?? null) === 'visual') { return response()->json([ 'success' => true, 'data' => $explain->generateVisualExplain($validated['connection'], $validated['query'], $validated['bindings'] ?? null, $validated['hash']), ]); } return response()->json([ 'success' => true, 'data' => $explain->generateRawExplain($validated['connection'], $validated['query'], $validated['bindings'] ?? null, $validated['hash']), 'visual' => $explain->isVisualExplainSupported($validated['connection']) ? [ 'confirm' => $explain->confirmVisualExplain($validated['connection']), ] : null, ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } } ================================================ FILE: src/Controllers/TelescopeController.php ================================================ find($uuid); $result = $storage->get('request', (new EntryQueryOptions())->batchId($entry->batchId))->first(); return redirect(config('telescope.domain') . '/' . config('telescope.path') . '/requests/' . $result->id); } } ================================================ FILE: src/DataCollector/CacheCollector.php ================================================ ['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(float $requestStartTime, bool $collectValues) { parent::__construct($requestStartTime); $this->collectValues = $collectValues; $this->memoryMeasure = true; } public function getCacheEvents(): array { return $this->classMap; } public function onCacheEvent(CacheEvent|CacheFailedOver|CacheFlushed|CacheFlushFailed|CacheFlushing $event): void { $class = get_class($event); $params = get_object_vars($event); $label = $this->classMap[$class][0]; if (isset($params['value'])) { if (!($params['value'] instanceof \Closure || is_resource($params['value']))) { try { $params['memoryUsage'] = strlen(serialize($params['value'])) * 8; } catch (Throwable) { } } if (!$this->collectValues) { unset($params['value']); } } $time = microtime(true); $startHashKey = $this->getEventHash($this->classMap[$class][1] ?? '', $params); $startTime = $this->eventStarts[$startHashKey] ?? $time; $this->addMeasure($label . "\t" . ($params['key'] ?? ''), $startTime, $time, $params); if ($this->hasTimeDataCollector()) { $this->addTimeMeasure('Cache ' . $label . "\t" . ($params['key'] ?? ''), $startTime, $time); } if (isset($event->key) && in_array($label, ['hit', 'written'], true) && Route::has('debugbar.cache.delete')) { $measureIndex = array_key_last($this->measures); $this->measures[$measureIndex]['delete_url'] = url()->signedRoute('debugbar.cache.delete', [ 'key' => urlencode((string) $event->key), 'tags' => $params['tags'] ?? [], ]); } } public function onStartCacheEvent(mixed $event): void { $startHashKey = $this->getEventHash(get_class($event), get_object_vars($event)); $this->eventStarts[$startHashKey] = microtime(true); } protected function getEventHash(string $class, array $params): string { unset($params['value']); return $class . ':' . substr(hash('sha256', json_encode($params)), 0, 12); } public function collect(): array { $data = parent::collect(); $data['nb_measures'] = $data['count'] = count($data['measures']); return $data; } public function reset(): void { parent::reset(); $this->eventStarts = []; } public function getName(): string { return 'cache'; } public function getWidgets(): array { return [ 'cache' => [ 'icon' => 'clipboard-text', 'widget' => 'PhpDebugBar.Widgets.LaravelCacheWidget', 'map' => 'cache', 'default' => '{}', ], 'cache:badge' => [ 'map' => 'cache.nb_measures', 'default' => 'null', ], ]; } public function getAssets(): array { return [ 'js' => __DIR__ . '/../../resources/cache/widget.js', ]; } } ================================================ FILE: src/DataCollector/ConfigCollector.php ================================================ setData(config()->all()); return parent::collect(); } } ================================================ FILE: src/DataCollector/EventCollector.php ================================================ collectValues = $collectValues; } public function setCollectListeners(bool $collectListeners = true): void { $this->collectListeners = $collectListeners; } public function setExcludedEvents(array $excludedEvents): void { $this->excludedEvents = $excludedEvents; } public function onWildcardEvent(?string $name = null, array $data = []): void { $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 = $data; if ($this->collectListeners) { $params['listeners'] = Event::getListeners($name); } $this->addMeasure($name, $currentTime, $currentTime, $params, null, $eventClass); } public function collect(): array { $data = parent::collect(); $data['nb_measures'] = $data['count'] = count($data['measures']); return $data; } public function getName(): string { return 'event'; } public function getWidgets(): array { return [ "events" => [ "icon" => "subtask", "widget" => "PhpDebugBar.Widgets.TimelineWidget", "map" => "event", "default" => "{}", ], 'events:badge' => [ 'map' => 'event.nb_measures', 'default' => 0, ], ]; } } ================================================ FILE: src/DataCollector/GateCollector.php ================================================ 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); } $arguments[0] = $target; } elseif (is_string($arguments[0])) { $target = $arguments[0]; } } $this->addMessage("{ability} {target}", $label, [ 'ability' => $ability, 'target' => $target, 'result' => $result, $userKey => $userId, 'arguments' => $arguments, ]); } protected function getStackTraceItem(array $stacktrace): array { 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 */ protected function findControllerFromDispatcher(array $trace): array { /** @var \Closure|string|array $action */ $action = app(Router::class)->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. */ protected function findViewFromHash(string $hash): ?string { $finder = app('view')->getFinder(); if (isset($this->reflection['viewfinderViews'])) { $property = $this->reflection['viewfinderViews']; } else { $reflection = new \ReflectionClass($finder); $property = $reflection->getProperty('views'); $this->reflection['viewfinderViews'] = $property; } $xxh128Exists = in_array('xxh128', hash_algos(), true); foreach ($property->getValue($finder) as $name => $path) { if (($xxh128Exists && hash('xxh128', 'v2' . $path) === $hash) || sha1('v2' . $path) === $hash) { return $path; } } return null; } public function reset(): void { $this->reflection = []; } } ================================================ FILE: src/DataCollector/HttpClientCollector.php ================================================ hideMaskedValues($event->request->headers()); if ($event->request->isMultipart()) { $requestData = '[MULTIPART]'; } else { $requestData = $this->hideMaskedValues($event->request->data()); } $status = null; $duration = null; $details = [ 'request_data' => $requestData, 'request_headers' => $headers, ]; if ($event instanceof ResponseReceived) { $status = $event->response->status(); $duration = $event->response->transferStats?->getTransferTime(); $details['response'] = $this->parseResponse($event->response); $details['response_headers'] = $this->hideMaskedValues($event->response->headers()); } // @phpstan-ignore-next-line because exception might not be set in Laravel 10 if ($event instanceof ConnectionFailed && isset($event->exception)) { $details['exception'] = $event->exception; } $this->addRequest( $event->request->method(), $event->request->url(), $status, $duration, $details ); } protected function parseResponse(Response $response): string|array { if ($response->redirect()) { return 'Redirect: ' . $response->header('Location'); } // Check if stream $stream = $response->toPsrResponse()->getBody(); if (! $stream->isSeekable()) { return '[STREAM]'; } $content = $response->body(); $stream->rewind(); if ($content === '') { return '[EMPTY]'; } $json = json_decode($content, true); if ($json) { return $json; } return Str::limit($content, 1024); } } ================================================ FILE: src/DataCollector/InertiaCollector.php ================================================ getData(); if (isset($data['page']['component'])) { $this->addInertiaTemplate($data['page'], $view->getName(), $view->getPath()); } } public function addFromResponse(Response $response): void { if (!$response->headers->has('X-Inertia') || $response->headers->get('Content-Type') !== 'application/json') { return; } $content = $response->getContent(); if (is_string($content)) { $content = json_decode($content, true); } if (is_array($content)) { $this->addInertiaTemplate($content); } } private function addInertiaTemplate(array $page, ?string $name = null, ?string $path = null): void { if (!isset($page['component'])) { return; } $type = ''; $component = $page['component']; $props = $page['props'] ?? []; $pagePath = config('debugbar.options.inertia.pages', 'js/Pages'); if ($files = glob(resource_path($pagePath . '/' . $name . '.*'))) { $path = $files[0]; $type = pathinfo($path, PATHINFO_EXTENSION); if (in_array($type, ['js', 'jsx'], true)) { $type = 'react'; } } $this->addTemplate($component, $props, $type, $path); } public function collect(): array { $data = parent::collect(); $data['sentence'] = 'Inertia page' . ($data['nb_templates'] !== 1 ? 's' : ''); return $data; } /** * {@inheritDoc} */ public function getName(): string { return 'inertia'; } public function getWidgets(): array { $widgets = parent::getWidgets(); $widgets[$this->getName()]['icon'] = 'brand-inertia'; return $widgets; } } ================================================ FILE: src/DataCollector/LaravelCollector.php ================================================ Str::of($app->version())->explode('.')->first() . '.x', 'tooltip' => array_filter([ 'Laravel Version' => $app->version(), 'PHP Version' => phpversion(), 'Environment' => $app->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'), 'Cached' => implode(', ', array_filter([ $app->configurationIsCached() ? 'Configs' : null, $app->routesAreCached() ? 'Routes' : null, $app->eventsAreCached() ? 'Events' : null, ])), ]), ]; } /** * {@inheritDoc} */ public function getName(): string { return 'laravel'; } /** * {@inheritDoc} */ public function getWidgets(): array { return [ "version" => [ "icon" => "brand-laravel", "map" => "laravel.version", "default" => "", ], "version:tooltip" => [ "map" => "laravel.tooltip", "default" => "{}", ], ]; } } ================================================ FILE: src/DataCollector/LivewireCollector.php ================================================ getId(); $data = $component->all(); if ((new \ReflectionClass($component))->isAnonymous()) { $key = Str::ascii($component->getName()) . ' #' . $id; } else { $key = get_class($component) . ' ' . $component->getName() . ' #' . $id; } if ($request && $request->request->get('id') === $id) { $data['#oldData'] = $request->request->get('data'); $data['#actionQueue'] = $request->request->get('actionQueue'); } $data['#name'] = $component->getName(); $data['#component'] = get_class($component); $data['#id'] = $id; $path = (new \ReflectionClass($component))->getFileName(); $this->addTemplate($key, $data, 'livewire', $path); } /** * @return array{nb_templates: int, templates: array, type: string, xdebug_link?: string}>, sentence: string} */ public function collect(): array { $data = parent::collect(); $data['sentence'] = 'Livewire component' . ($data['nb_templates'] !== 1 ? 's' : ''); return $data; } /** * {@inheritDoc} */ public function getName(): string { return 'livewire'; } /** * @return array */ public function getWidgets(): array { $widgets = parent::getWidgets(); $widgets[$this->getName()]['icon'] = 'brand-livewire'; return $widgets; } } ================================================ FILE: src/DataCollector/LogsCollector.php ================================================ 0) { $this->paths = $path; } elseif (is_string($path)) { $this->paths = [$path]; } else { $this->paths = [ storage_path('logs/laravel.log'), storage_path('logs/laravel-' . date('Y-m-d') . '.log'), // for daily driver ]; } } public function collect(): array { foreach ($this->paths as $logPath) { $this->getStorageLogs($logPath); } return parent::collect(); } /** * get logs apache in app/storage/logs * only 24 last of current day */ public function getStorageLogs(string $path): void { if (!file_exists($path)) { return; } //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->messages[] = [ 'message' => trim($log['header'] . $log['stack']), 'label' => $log['level'], 'time' => substr($log['header'], 1, 19), 'collector' => $basename, 'is_string' => false, ]; } } /** * By Ain Tohvri (ain) * http://tekkie.flashbit.net/php/tail-functionality-in-php */ protected function tailFile(string $file, int $lines): array { $handle = fopen($file, "r"); $linecounter = $lines; $pos = -2; $beginning = false; $text = []; try { while ($linecounter > 0) { $t = " "; while ($t !== "\n") { if (fseek($handle, $pos, SEEK_END) === -1) { $beginning = true; break; } $t = fgetc($handle); $pos--; } $linecounter--; if ($beginning) { rewind($handle); } $text[$lines - $linecounter - 1] = fgets($handle); if ($beginning) { break; } } } finally { fclose($handle); } return array_reverse($text); } /** * Search a string for log entries * Based on https://github.com/mikemand/logviewer/blob/master/src/Kmd/Logviewer/Logviewer.php by mikemand */ public function getLogs(string $file): array { $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 = []; foreach ($headings as $h) { for ($i = 0, $j = count($h); $i < $j; $i++) { foreach ($log_levels as $ll) { if (str_contains(strtolower($h[$i]), strtolower('.' . $ll))) { $log[] = ['level' => $ll, 'header' => $h[$i], 'stack' => $log_data[$i] ?? '']; } } } } return $log; } public function getMessages(): array { 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 */ public function getLevels(): array { $class = new ReflectionClass(new LogLevel()); return $class->getConstants(); } } ================================================ FILE: src/DataCollector/MultiAuthCollector.php ================================================ guards = $guards; } /** * Set to show the users name/email */ public function setShowName(bool $showName): void { $this->showName = $showName; } /** * Set to hide the guards tab, and show only name */ public function setShowGuardsData(bool $showGuardsData): void { $this->showGuardsData = $showGuardsData; } /** * @{inheritDoc} */ public function collect(): array { $data = [ 'guards' => [], ]; $names = ''; foreach ($this->guards as $guardName => $config) { try { $guard = auth()->guard($guardName); if ($guard->hasUser()) { $user = $guard->user(); if (!is_null($user)) { $data['guards'][$guardName] = $this->getUserInformation($user); $names .= $guardName . ": " . $data['guards'][$guardName]['name'] . ', '; } } else { $data['guards'][$guardName] = null; } } catch (\Exception $e) { continue; } } foreach ($data['guards'] as $key => $var) { $data['guards'][$key] = $this->getDataFormatter()->formatVar($var); } $data['names'] = rtrim($names, ', '); if (!$this->showGuardsData) { unset($data['guards']); } return $data; } /** * Get displayed user information */ protected function getUserInformation(mixed $user = null): array { // Defaults if (is_null($user)) { return [ 'name' => 'Guest', 'user' => ['guest' => true], ]; } // The default auth identifer is the ID number, which isn't all that // useful. Try username, email and name. $identifier = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user->getKey(); if (is_numeric($identifier) || Str::isUuid($identifier) || Str::isUlid($identifier)) { try { if (isset($user->username)) { $identifier = $user->username; } elseif (isset($user->email)) { $identifier = $user->email; } elseif (isset($user->name)) { $identifier = Str::limit($user->name, 24); } } catch (\Throwable $e) { } } return [ 'name' => $identifier, 'user' => $user instanceof Arrayable ? $user->toArray() : $user, ]; } /** * @{inheritDoc} */ public function getName(): string { return 'auth'; } /** * @{inheritDoc} */ public function getWidgets(): array { $widgets = []; if ($this->showGuardsData) { $widget = match (true) { $this->isJsonVarDumperUsed() => "PhpDebugBar.Widgets.JsonVariableListWidget", $this->isHtmlVarDumperUsed() => "PhpDebugBar.Widgets.HtmlVariableListWidget", default => "PhpDebugBar.Widgets.VariableListWidget", }; $widgets["auth"] = [ "icon" => "lock", "widget" => $widget, "map" => "auth.guards", "default" => "{}", ]; } if ($this->showName) { $widgets['auth.name'] = [ 'icon' => 'user', 'tooltip' => 'Auth status', 'map' => 'auth.names', 'default' => '', ]; } return $widgets; } } ================================================ FILE: src/DataCollector/PennantCollector.php ================================================ [ "icon" => "flag", "widget" => "PhpDebugBar.Widgets.VariableListWidget", "map" => "pennant", "default" => "{}", ], ]; } } ================================================ FILE: src/DataCollector/QueryCollector.php ================================================ queryFormatter === null) { $this->queryFormatter = new QueryFormatter(); } return $this->queryFormatter; } /** * @param int|null $softLimit After the soft limit, no parameters/backtrace are captured * @param int|null $hardLimit After the hard limit, queries are ignored */ public function setLimits(?int $softLimit, ?int $hardLimit): void { $this->softLimit = $softLimit; $this->hardLimit = $hardLimit; } /** * Renders the SQL of traced statements with params embedded */ public function setRenderSqlWithParams(bool $enabled = true): void { $this->renderSqlWithParams = $enabled; } /** * Enable/disable finding the source */ public function setFindSource(bool|int $value, array $middleware): void { $this->findSource = $value; $this->middleware = $middleware; } public function mergeExcludePaths(array $excludePaths): void { $this->excludePaths = array_merge($this->excludePaths, $excludePaths); } /** * Set additional paths to exclude from the backtrace */ public function mergeBacktraceExcludePaths(array $excludePaths): void { $this->backtraceExcludePaths = array_merge($this->backtraceExcludePaths, $excludePaths); } /** * Enable/disable the shaded duration background on queries */ public function setDurationBackground(bool $enabled): void { $this->durationBackground = $enabled; } /** * Highlights queries that exceed the threshold * * @param int|float $threshold miliseconds value */ public function setSlowThreshold(int|float $threshold): void { $this->slowThreshold = $threshold / 1000; } public function isSqlRenderedWithParams(): bool { return $this->renderSqlWithParams; } /** * Enable/disable the EXPLAIN queries * * @deprecated use setExplainQuery() */ public function setExplainSource(bool $enabled): void { $this->setExplainQuery($enabled); } /** * Enable/disable the EXPLAIN queries */ public function setExplainQuery(bool $enabled): void { $this->explainQuery = $enabled; } /** * Enable/disable the EXPLAIN queries */ public function setShowQueryResult(bool $enabled): void { $this->showQueryResult = $enabled; } public function startMemoryUsage(): void { $this->lastMemoryUsage = memory_get_usage(false); } public function addQuery(QueryExecuted $query): void { $this->queryCount++; if ($this->hardLimit && $this->queryCount > $this->hardLimit) { return; } $limited = $this->softLimit && $this->queryCount > $this->softLimit; $sql = $query->sql; $time = $query->time / 1000; $endTime = microtime(true); $startTime = $endTime - $time; $source = []; if (!$limited && $this->findSource) { try { $source = $this->findSource(); } catch (\Exception $e) { } } $bindings = match (true) { $limited && filled($query->bindings) => null, default => $query->connection->prepareBindings($query->bindings), }; $this->queries[] = [ 'query' => $sql, 'type' => 'query', 'bindings' => $bindings, 'start' => $startTime, 'time' => $time, 'memory' => $this->lastMemoryUsage ? memory_get_usage(false) - $this->lastMemoryUsage : 0, 'source' => $source, 'connection' => $query->connection, 'driver' => $query->connection->getConfig('driver'), ]; if ($this->hasTimeDataCollector()) { $this->addTimeMeasure(Str::limit($sql, 100), $startTime, $endTime, [], 'Database Query'); } } /** * Use a backtrace to search for the origins of the query. */ protected function findSource(): array { $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, app('config')->get('debugbar.debug_backtrace_limit', 50)); $sources = []; foreach ($stack as $index => $trace) { $sources[] = $this->parseTrace($index, $trace); } return array_slice(array_filter($sources), 0, is_int($this->findSource) ? $this->findSource : 5); } /** * Parse a trace element from the backtrace stack. */ protected function parseTrace(int $index, array $trace): object|bool { $frame = (object) [ 'index' => $index, 'namespace' => null, 'name' => null, 'file' => null, 'line' => $trace['line'] ?? '1', ]; if (isset($trace['function']) && $trace['function'] === 'substituteBindings') { $frame->name = 'Route binding'; return $frame; } if ( isset($trace['class']) && isset($trace['file']) && !$this->fileIsInExcludedPath($trace['file']) ) { $frame->file = $trace['file']; if (isset($trace['object']) && is_a($trace['object'], '\Twig\Template')) { [$frame->file, $frame->line] = $this->getTwigInfo($trace); } elseif (str_contains($frame->file, storage_path())) { $hash = pathinfo($frame->file, PATHINFO_FILENAME); if ($frame->name = $this->findViewFromHash($hash)) { $frame->file = $frame->name[1]; $frame->name = $frame->name[0]; } else { $frame->name = $hash; } $frame->namespace = 'view'; return $frame; } elseif (str_contains($frame->file, 'Middleware')) { $frame->name = $this->findMiddlewareFromFile($frame->file); if ($frame->name) { $frame->namespace = 'middleware'; } else { $frame->name = $this->normalizeFilePath($frame->file); } return $frame; } $frame->name = $this->normalizeFilePath($frame->file); return $frame; } return false; } /** * Check if the given file is to be excluded from analysis */ protected function fileIsInExcludedPath(string $file): bool { $normalizedPath = str_replace('\\', '/', $file); foreach ($this->backtraceExcludePaths as $excludedPath) { if (str_contains($normalizedPath, $excludedPath)) { return true; } } return false; } /** * Find the middleware alias from the file. */ protected function findMiddlewareFromFile(string $file): ?string { $filename = pathinfo($file, PATHINFO_FILENAME); foreach ($this->middleware as $alias => $class) { if (is_string($class) && str_contains($class, $filename)) { return $alias; } } return null; } /** * Find the template name from the hash. */ protected function findViewFromHash(string $hash): ?array { $finder = app('view')->getFinder(); if (isset($this->reflection['viewfinderViews'])) { $property = $this->reflection['viewfinderViews']; } else { $reflection = new \ReflectionClass($finder); $property = $reflection->getProperty('views'); $this->reflection['viewfinderViews'] = $property; } $xxh128Exists = in_array('xxh128', hash_algos(), true); foreach ($property->getValue($finder) as $name => $path) { if (($xxh128Exists && hash('xxh128', 'v2' . $path) === $hash) || sha1('v2' . $path) === $hash) { return [$name, $path]; } } return null; } /** * Get the filename/line from a Twig template trace */ protected function getTwigInfo(array $trace): array { $file = $trace['object']->getTemplateName(); if (isset($trace['line'])) { foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) { if ($codeLine <= $trace['line']) { return [$file, $templateLine]; } } } return [$file, -1]; } /** * Adds a custom message to statements. */ public function addMessage(string $message): void { $this->infoStatements++; $source = []; if ($this->findSource) { try { $source = $this->findSource(); } catch (\Exception $e) { } } $this->queries[] = [ 'sql' => $message, 'type' => 'message', 'start' => microtime(true), ...(count($source) ? ['xdebug_link' => $source[0]] : []), ]; } /** * Collect a database transaction event. */ public function collectTransactionEvent(string $event, mixed $connection): void { $this->transactionEventsCount++; $source = []; if ($this->findSource) { try { $source = $this->findSource(); } catch (\Exception $e) { } } $this->queries[] = [ 'query' => $event, 'type' => 'transaction', 'bindings' => [], 'start' => microtime(true), 'time' => 0, 'memory' => 0, 'source' => $source, 'connection' => $connection, 'driver' => $connection->getConfig('driver'), ]; } /** * Reset the queries. */ public function reset(): void { $this->queries = []; $this->queryCount = 0; $this->infoStatements = 0 ; $this->transactionEventsCount = 0; $this->reflection = []; } /** * {@inheritDoc} */ public function collect(): array { $totalTime = 0; $totalMemory = 0; $queries = $this->queries; $statements = []; $explain = (new Explain()); foreach ($queries as $query) { if ($query['type'] === 'message') { if (isset($query['xdebug_link'])) { $source = $query['xdebug_link']; $query['xdebug_link'] = $this->getXdebugLink($source->file ?: '', $source->line); } $statements[] = $query; continue; } $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 ($connectionName && str_ends_with($connectionName, '.sqlite')) { $connectionName = $this->normalizeFilePath($connectionName); } $explainModes = []; $isReadonly = $explain->isReadOnlyQuery($query['query'] ?? ''); $canRunQuery = $this->showQueryResult && $isReadonly; if ($canRunQuery) { $explainModes[] = 'result'; } if ($isReadonly && $this->explainQuery && $explain->isRawExplainSupported($query['driver'], $query['bindings'])) { $explainModes[] = 'explain'; } $statements[] = [ 'sql' => $this->getSqlQueryToDisplay($query), 'type' => $query['type'], 'params' => $query['bindings'] ?? [], 'backtrace' => array_values($query['source']), 'start' => $query['start'] ?? null, 'duration' => $query['time'], 'duration_str' => ($query['type'] === 'transaction') ? '' : $this->getDataFormatter()->formatDuration($query['time']), 'slow' => $this->slowThreshold && $this->slowThreshold <= $query['time'], 'memory' => $query['memory'], 'memory_str' => $query['memory'] ? $this->getDataFormatter()->formatBytes($query['memory']) : null, 'filename' => $source ? $this->getQueryFormatter()->formatSource($source, true) : null, 'source' => $source, 'xdebug_link' => is_object($source) ? $this->getXdebugLink($source->file ?: '', $source->line) : null, 'connection' => $connectionName, 'explain' => $explainModes ? [ 'url' => route('debugbar.queries.explain'), 'driver' => $query['driver'], 'connection' => $query['connection']->getName(), 'query' => $query['query'], 'modes' => $explainModes, 'hash' => $explain->hash($query['connection']->getName(), $query['query'], $query['bindings']), ] : null, ]; } if ($this->durationBackground) { if ($totalTime > 0) { // For showing background measure on Queries tab $start_percent = 0; foreach ($statements as $i => $statement) { if (!isset($statement['duration'])) { continue; } $width_percent = $statement['duration'] / $totalTime * 100; $statements[$i] = array_merge($statement, [ 'start_percent' => round($start_percent, 3), 'width_percent' => round($width_percent, 3), ]); $start_percent += $width_percent; } } } if ($this->softLimit && $this->hardLimit && ($this->queryCount > $this->softLimit && $this->queryCount > $this->hardLimit)) { array_unshift($statements, [ 'sql' => '# Query soft and hard limit for Debugbar are reached. Only the first ' . $this->softLimit . ' queries show details. Queries after the first ' . $this->hardLimit . ' are ignored. Limits can be raised in the config (debugbar.options.db.soft/hard_limit).', 'type' => 'info', ]); $statements[] = [ 'sql' => '... ' . ($this->queryCount - $this->hardLimit) . ' additional queries are executed but now shown because of Debugbar query limits. Limits can be raised in the config (debugbar.options.db.soft/hard_limit)', 'type' => 'info', ]; $this->infoStatements += 2; } elseif ($this->hardLimit && $this->queryCount > $this->hardLimit) { array_unshift($statements, [ 'sql' => '# Query hard limit for Debugbar is reached after ' . $this->hardLimit . ' queries, additional ' . ($this->queryCount - $this->hardLimit) . ' queries are not shown.. Limits can be raised in the config (debugbar.options.db.hard_limit)', 'type' => 'info', ]); $statements[] = [ 'sql' => '... ' . ($this->queryCount - $this->hardLimit) . ' additional queries are executed but now shown because of Debugbar query limits. Limits can be raised in the config (debugbar.options.db.hard_limit)', 'type' => 'info', ]; $this->infoStatements += 2; } elseif ($this->softLimit && $this->queryCount > $this->softLimit) { array_unshift($statements, [ 'sql' => '# Query soft limit for Debugbar is reached after ' . $this->softLimit . ' queries, additional ' . ($this->queryCount - $this->softLimit) . ' queries only show the query. Limits can be raised in the config (debugbar.options.db.soft_limit)', 'type' => 'info', ]); $this->infoStatements++; } $visibleStatements = count($statements) - $this->infoStatements; $data = [ 'count' => $visibleStatements, 'nb_statements' => $this->queryCount, 'nb_visible_statements' => $visibleStatements, 'nb_excluded_statements' => $this->queryCount + $this->transactionEventsCount - $visibleStatements, 'nb_failed_statements' => 0, 'accumulated_duration' => $totalTime, 'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($totalTime), 'memory_usage' => $totalMemory, 'memory_usage_str' => $totalMemory ? $this->getDataFormatter()->formatBytes($totalMemory) : null, 'statements' => $statements, ]; return $data; } /** * {@inheritDoc} */ public function getName(): string { return 'queries'; } /** * {@inheritDoc} */ public function getWidgets(): array { return [ "queries" => [ "icon" => "database", "widget" => "PhpDebugBar.Widgets.LaravelQueriesWidget", "map" => "queries", "default" => "[]", ], "queries:badge" => [ "map" => "queries.nb_statements", "default" => 0, ], ]; } protected function getSqlQueryToDisplay(array $query): string { $sql = $query['query']; $grammar = $query['connection']->getQueryGrammar(); if ($query['type'] === 'query' && $grammar instanceof Grammar) { try { $sql = $grammar->substituteBindingsIntoRawSql($sql, $query['bindings'] ?? []); return $this->getQueryFormatter()->formatSql($sql); } catch (\Throwable $e) { // Continue using the old substitute } } if ($query['type'] === 'query' && $this->renderSqlWithParams) { $pdo = null; try { $pdo = $query['connection']->getPdo(); } catch (\Throwable) { // ignore error for non-pdo laravel drivers } $sql = $this->getQueryFormatter()->formatSqlWithBindings($sql, $query['bindings'] ?? [], $pdo); } return $this->getQueryFormatter()->formatSql($sql); } public function getAssets(): array { return [ 'js' => [ 'widgets/sqlqueries/widget.js', __DIR__ . '/../../resources/queries/widget.js', ], 'css' => 'widgets/sqlqueries/widget.css', ]; } } ================================================ FILE: src/DataCollector/RequestCollector.php ================================================ getProcessingJob()) { return $this->collectJob($job); } if (app()->runningInConsole()) { return $this->collectCli(); } $this->request = request(); $result = parent::collect(); if ($this->request->hasSession()) { $sessionAttributes = $this->hideMaskedValues($this->request->session()->all()); $sessionAttributes = $this->getDataFormatter()->formatVar($sessionAttributes); $result['data']['session_attributes'] = $sessionAttributes; } $result['tooltip'] += [ 'full_url' => Str::limit($this->request->fullUrl(), 100), ]; $htmlData = []; $route = $this->request->route(); if ($route) { // @phpstan-ignore-line despite what phpdocs say, this can return null $htmlData += $this->getRouteInformation($this->request->route()); $result['tooltip'] += [ 'action_name' => $route->getName(), 'controller_action' => $route->getActionName(), ]; } if (class_exists(Telescope::class) && class_exists(IncomingEntry::class) && Telescope::isRecording()) { $entry = IncomingEntry::make([ 'requestId' => app(LaravelDebugbar::class)->getCurrentRequestId(), ])->type('debugbar'); Telescope::$entriesQueue[] = $entry; $url = route('debugbar.telescope', [$entry->uuid]); $htmlData['telescope'] = '
View in Telescope'; } unset($htmlData['as'], $htmlData['uses']); $result['data'] = $htmlData + $result['data']; return $result; } protected function collectCli(): array { $argv = new ArgvInput(); $command = $argv->getFirstArgument(); $commands = Artisan::all(); $commandClass = $commands[$command] ?? null; $data = [ 'method' => 'CLI', 'command' => $command, 'command_class' => $commandClass, 'args' => (new ArgvInput())->getRawTokens(), 'request_server' => $this->request->server->all(), ]; $data = $this->hideMaskedValues($data); foreach ($data as $key => $var) { if (!is_string($var)) { $data[$key] = $this->getDataFormatter()->formatVar($var); } } if ($commandClass) { $reflector = new \ReflectionClass($commandClass); $filename = $this->normalizeFilePath($reflector->getFileName()); if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { $data['command_class'] = [ 'value' => sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()), 'xdebug_link' => $link, ]; } } return ['data' => $data]; } protected function collectJob(Job $job): array { $jobClass = $job->resolveQueuedJobClass(); $data = [ 'method' => 'CLI', 'job' => $job->resolveName(), 'job_class' => $jobClass, 'job_id' => $job->getJobId(), 'connection' => $job->getConnectionName(), 'queue' => $job->getQueue(), 'payload' => $job->payload(), ]; $data = $this->hideMaskedValues($data); foreach ($data as $key => $var) { if (!is_string($var)) { $data[$key] = $this->getDataFormatter()->formatVar($var); } } if ($jobClass) { $reflector = new \ReflectionClass($jobClass); $filename = $this->normalizeFilePath($reflector->getFileName()); if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { $data['job_class'] = [ 'value' => sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()), 'xdebug_link' => $link, ]; } } return ['data' => $data]; } protected function getRouteInformation(mixed $route): array { 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') && class_exists(HandleComponents::class)) { try { $componentData = $this->request->request->all()['components'][0] ?? null; if (isset($componentData['snapshot'], $componentData['updates'])) { $snapshot = json_decode($componentData['snapshot'], true); if (count($componentData['updates']) > 0) { $method = $componentData['updates'][array_key_first($componentData['updates'])] ?? null; } else { $method = null; } [$component] = app(HandleComponents::class)->fromSnapshot($snapshot); $result['controller'] = ltrim($component::class, '\\'); $reflector = new \ReflectionClass($component); $controller = $component::class . '@' . $method; } } catch (\Throwable $e) { // } } if (str_contains($controller, '@')) { [$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->getDataFormatter()->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()); $result['file'] = sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()); if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { $result['file'] = [ 'value' => $result['file'], 'xdebug_link' => $link, ]; if (isset($result['controller']) && is_string($result['controller'])) { $result['controller'] = [ 'value' => $result['controller'], 'xdebug_link' => $link, ]; } } } if (isset($result['middleware']) && is_array($result['middleware'])) { $middleware = implode(', ', $result['middleware']); unset($result['middleware']); $result['middleware'] = $middleware; } return array_filter($result); } } ================================================ FILE: src/DataCollector/RouteCollector.php ================================================ current(); return $this->getRouteInformation($route); } /** * Get the route information for a given route. */ protected function getRouteInformation(mixed $route): array { 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') && class_exists(HandleComponents::class)) { try { $componentData = request('components')[0] ?? null; if (isset($componentData['snapshot'], $componentData['updates'])) { $snapshot = json_decode($componentData['snapshot'], true); if (count($componentData['updates']) > 0) { $method = $componentData['updates'][array_key_first($componentData['updates'])] ?? null; } else { $method = null; } [$component] = app(HandleComponents::class)->fromSnapshot($snapshot); $result['controller'] = ltrim($component::class, '\\'); $reflector = new \ReflectionClass($component); $controller = $component::class . '@' . $method; } } catch (\Throwable $e) { // } } if (str_contains($controller, '@')) { [$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->getDataFormatter()->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()); $result['file'] = sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()); if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { $result['file'] = [ 'value' => $result['file'], 'xdebug_link' => $link, ]; if (isset($result['controller'])) { $result['controller'] = [ 'value' => $result['controller'], 'xdebug_link' => $link, ]; } } } if ($middleware = $this->getMiddleware($route)) { $result['middleware'] = $middleware; } return array_filter($result); } /** * Get middleware */ protected function getMiddleware(mixed $route): string { return implode(', ', array_map(function ($middleware): mixed { return $middleware instanceof Closure ? 'Closure' : $middleware; }, $route->gatherMiddleware())); } /** * {@inheritDoc} */ public function getName(): string { return 'route'; } /** * {@inheritDoc} */ public function getWidgets(): array { $widget = match (true) { $this->isJsonVarDumperUsed() => "PhpDebugBar.Widgets.JsonVariableListWidget", $this->isHtmlVarDumperUsed() => "PhpDebugBar.Widgets.HtmlVariableListWidget", default => "PhpDebugBar.Widgets.VariableListWidget", }; return [ "route" => [ "icon" => "share-3", "widget" => $widget, "map" => "route", "default" => "{}", ], ]; } } ================================================ FILE: src/DataCollector/SessionCollector.php ================================================ hideMaskedValues(session()->all()); foreach ($data as $key => $value) { $data[$key] = is_string($value) ? $value : $this->getDataFormatter()->formatVar($value); } return $data; } /** * {@inheritDoc} */ public function getName(): string { return 'session'; } /** * {@inheritDoc} */ public function getWidgets(): array { $widget = match (true) { $this->isJsonVarDumperUsed() => "PhpDebugBar.Widgets.JsonVariableListWidget", $this->isHtmlVarDumperUsed() => "PhpDebugBar.Widgets.HtmlVariableListWidget", default => "PhpDebugBar.Widgets.VariableListWidget", }; return [ "session" => [ "icon" => "archive", "widget" => $widget, "map" => "session", "default" => "{}", ], ]; } } ================================================ FILE: src/DataCollector/ViewCollector.php ================================================ getName(); $type = null; $data = $view->getData(); $path = $view->getPath(); // Skip View files from strings if (Str::startsWith($name, '__components::')) { if ($source = $this->getRenderSource($name, $path)) { [$name, $type, $data, $path] = $source; } } if (is_object($path)) { $type = get_class($view); $path = null; } if ($path && $type !== 'livewire') { if (!$type) { if (substr($path, -10) === '.blade.php') { $type = 'blade'; } else { $type = pathinfo($path, PATHINFO_EXTENSION); } } $shortPath = $this->normalizeFilePath($path); foreach ($this->exclude_paths as $excludePath) { if (str_starts_with($shortPath, $excludePath)) { return; } } } $this->addTemplate($name, $data, $type, $path); } private function getRenderSource(string $name, ?string $path): ?array { $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 20); $component = null; $render = null; $view = null; foreach ($backtrace as $trace) { $function = $trace['function'] ?? null; //@phpstan-ignore-line $class = $trace['class'] ?? null; $file = $trace['file'] ?? null; $object = $trace['object'] ?? null; // Found an invokable class if ( $function === '__invoke' && $class === 'Livewire\Component' && $object && !$component ) { /** @var \Livewire\Component $component */ $component = $trace['object']; $name = get_class($component); $type = 'livewire'; $path = (new \ReflectionClass($component))->getFileName(); $component = [$name, $type, [], $path]; } if ( ( ($function === 'render' && $class === 'Illuminate\View\Compilers\BladeCompiler') || ($function === '__callStatic' && $class === 'Illuminate\Support\Facades\Facade' && ($trace['args'][0] ?? null) === 'render') ) && !str_contains($file, '/Illuminate/') && !$render ) { $render = [$name, 'render', [], $file]; } if (!$view && $class === 'Illuminate\View\View' && $object instanceof View && !str_starts_with($object->getName(), '__components::') ) { $view = [$object->getName(), null, $object->getData(), $object->getPath()]; } } if ($component) { return $component; } if ($render) { return $render; } if ($view) { return $view; } return null; } } ================================================ FILE: src/Facades/Debugbar.php ================================================ app = $app; $this->request = $request; $this->timeCollector = new TimeDataCollector($startTime); $this->messagesCollector = new MessagesCollector(); $this->exceptionsCollector = new ExceptionsCollector(); } public function setApplication(Application $app): void { $this->app = $app; } public function setRequest(Request $request): void { $this->request = $request; } public function setProcessingJob(?Job $job): void { $this->processingJob = $job; } public function getProcessingJob(): ?Job { return $this->processingJob; } public function getHttpDriver(): HttpDriverInterface { if ($this->httpDriver === null) { $this->httpDriver = new LaravelHttpDriver($this->request); } return $this->httpDriver; } public function getRequestIdGenerator(): RequestIdGeneratorInterface { if ($this->requestIdGenerator === null) { $this->requestIdGenerator = new class implements RequestIdGeneratorInterface { public function generate(): string { return (string) Str::ulid(); } }; } return $this->requestIdGenerator; } public function getTimeCollector(): TimeDataCollector { return $this->timeCollector; } public function getMessagesCollector(): MessagesCollector { return $this->messagesCollector; } public function getExceptionsCollector(): ExceptionsCollector { return $this->exceptionsCollector; } public function isCollecting(): bool { return $this->enabled && $this->booted; } /** * Enable the Debugbar and boot, if not already booted. */ public function enable(): void { $this->enabled = true; if (!$this->booted) { $this->boot(); } } /** * Boot the debugbar (add collectors, renderer and listener) */ public function boot(): void { if ($this->booted) { return; } $config = config(); $this->editorTemplate = $config->get('debugbar.editor') ?: $config->get('app.editor'); $this->remotePathReplacements = $this->getRemoteServerReplacements(); // Set custom error handler if ($config->get('debugbar.error_handler', false)) { // Get the error_level config, default to E_ALL $errorLevel = $config->get('debugbar.error_level', E_ALL); // set error handler with configured error reporting level $this->prevErrorHandler = set_error_handler([$this, 'handleError'], $errorLevel); } $this->selectStorage($this); $this->registerDataFormatter(); $this->registerCollectors(); $this->booted = true; } public function booted(): void { $startTime = defined('LARAVEL_START') ? (float) LARAVEL_START : null; if ($startTime) { $this->addMeasure('Booting', $startTime, microtime(true)); } $this->startMeasure('application', 'Application', 'time'); } protected function registerCollectors(): void { // Register default Collector Provider $this->registerCollectorProviders([ 'symfony_request' => RequestCollectorProvider::class, 'exceptions' => ExceptionsCollectorProvider::class, 'phpinfo' => PhpInfoCollectorProvider::class, 'messages' => MessagesCollectorProvider::class, 'time' => TimeCollectorProvider::class, 'memory' => MemoryCollectorProvider::class, 'laravel' => LaravelCollectorProvider::class, 'events' => EventsCollectorCollectorProvider::class, 'views' => ViewsCollectorProvider::class, 'route' => RouteCollectorProvider::class, 'log' => LogCollectorProvider::class, 'logs' => LogsCollectorProvider::class, 'db' => DatabaseCollectorProvider::class, 'models' => ModelsCollectorProvider::class, 'livewire' => LivewireCollectorProvider::class, 'inertia' => InertiaCollectorProvider::class, 'mail' => MailCollectorProvider::class, 'auth' => AuthCollectorProvider::class, 'gate' => GateCollectorProvider::class, 'cache' => CacheCollectorProvider::class, 'jobs' => JobsCollectorProvider::class, 'pennant' => PennantCollectorProvider::class, 'config' => ConfigCollectorProvider::class, 'session' => SessionCollectorProvider::class, 'http_client' => HttpClientCollectorProvider::class, ]); // Register any Custom Collectors $this->registerCustomCollectorProviders(config('debugbar.custom_collectors', [])); } /** * @param array $providers */ protected function registerCollectorProviders(array $providers): void { /** @var Repository $config */ $config = $this->app->get(Repository::class); foreach ($providers as $name => $provider) { if (!$this->shouldCollect($name)) { continue; } try { $options = $config->get('debugbar.options.' . $name, []); $this->app->call($provider, ['options' => $options]); } catch (Exception $e) { $this->addCollectorException('Error calling ' . class_basename($provider), $e); } } } /** * @param array $providers */ protected function registerCustomCollectorProviders(array $providers): void { foreach ($providers as $provider => $enabled) { if (!$enabled) { continue; } try { $provider = $this->app->make($provider); // Add collectors directly, otherwise invoke the class if (is_a($provider, DataCollectorInterface::class)) { $this->addCollector($provider); } else { $this->app->call($provider); } } catch (Exception $e) { $this->addCollectorException('Error calling ' . class_basename($provider), $e); } } } /** * Register some Casters to avoid large objects for events etc. */ protected function registerDataFormatter(): void { $formatter = new JsonDataFormatter(); $formatter->mergeClonerOptions([ 'casters' => [ \Illuminate\View\View::class => static function (\Illuminate\View\View $view, array $a, Stub $stub): array { return [ 'name' => $view->getName(), 'data' => $view->getData(), 'path' => $view->getPath(), 'engine' => get_class($view->getEngine()), 'factory' => get_class($view->getFactory()), ]; }, \Illuminate\Database\ConnectionInterface::class => static function (\Illuminate\Database\ConnectionInterface $connection, array $a, Stub $stub): array { return [ 'database' => $connection->getDatabaseName(), ]; }, ], ]); DataCollector::setDefaultDataFormatter($formatter); } public function getJavascriptRenderer(?string $baseUrl = null, ?string $basePath = null): JavascriptRenderer { if ($this->jsRenderer !== null) { return $this->jsRenderer; } $renderer = new JavascriptRenderer($this, $baseUrl, $basePath); $config = config(); $renderer->setHideEmptyTabs($config->get('debugbar.hide_empty_tabs', true)); $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)); $renderer->setUseDistFiles($config->get('debugbar.use_dist_files', true)); $renderer->setAjaxHandlerAutoShow($config->get('debugbar.ajax_handler_auto_show', true)); $renderer->setAjaxHandlerEnableTab($config->get('debugbar.ajax_handler_enable_tab', true)); $renderer->setTheme($config->get('debugbar.theme', 'auto')); $renderer->setAssetHandlerUrl(route('debugbar.assets')); $renderer->addAssets(cssFiles: ['laravel-debugbar.css', 'laravel-icons.css'], basePath: __DIR__ . '/../resources'); if ($this->getStorage()) { $renderer->setOpenHandlerUrl(route('debugbar.openhandler')); } $this->jsRenderer = $renderer; return $this->jsRenderer; } public function shouldCollect(string $name, bool $default = true): bool { return config('debugbar.collectors.' . $name, $default); } /** * Handle silenced errors */ public function handleError(int $level, string $message, string $file = '', int $line = 0, array $context = []): mixed { if ($this->hasCollector('exceptions')) { /** @var ExceptionsCollector $exceptionCollector */ $exceptionCollector = $this['exceptions']; $exceptionCollector->addWarning($level, $message, $file, $line); } if ($this->hasCollector('messages')) { /** @var MessagesCollector $messagesCollector */ $messagesCollector = $this['messages']; $file = $file ? ' on ' . $messagesCollector->normalizeFilePath($file) . ":{$line}" : ''; $messagesCollector->addMessage($message . $file, 'deprecation'); } if (! $this->prevErrorHandler) { return null; } return call_user_func($this->prevErrorHandler, $level, $message, $file, $line, $context); } /** * Starts a measure * * @param string $name Internal name, used to stop the measure * @param string|null $label Public name */ public function startMeasure(string $name, ?string $label = null, ?string $collector = null, ?string $group = null): void { $this->timeCollector->startMeasure($name, $label, $collector, $group); } /** * Stops a measure */ public function stopMeasure(string $name): void { try { $this->timeCollector->stopMeasure($name); } catch (Exception $e) { $this->addThrowable($e); } } /** * Alias for addThrowable * */ public function addException(Throwable $e): void { $this->addThrowable($e); } /** * Adds an exception to be profiled in the debug bar */ public function addThrowable(Throwable $e): void { $this->exceptionsCollector->addThrowable($e); } /** * Register collector exceptions * */ protected function addCollectorException(string $message, Exception $exception): void { $this->addThrowable( new Exception( $message . ' on Laravel Debugbar: ' . $exception->getMessage(), (int) $exception->getCode(), $exception, ), ); } /** * Modify the response and inject the debugbar (or data in headers) */ public function handleResponse(Request $request, SymfonyResponse $response): SymfonyResponse { $this->setRequest($request); if ( $this->responseIsModified || !$this->booted || !$this->isEnabled() || $this->isDebugbarRequest($request) || $this->requestIsExcluded($request) ) { return $response; } $config = $this->app->get(Repository::class); // Prevent duplicate modification $this->responseIsModified = true; // These rely on the Response, so we add them directly here $httpDriver = $this->getHttpDriver(); if ($httpDriver instanceof LaravelHttpDriver) { $httpDriver->setRequest($request); $httpDriver->setResponse($response); } elseif ($httpDriver instanceof SymfonyHttpDriver) { $httpDriver->setResponse($response); } // Show the Http Response Exception in the Debugbar, when available if ($response instanceof Response && isset($response->exception)) { $this->addThrowable($response->exception); } // Update collectors that use the request/response if ($this->hasCollector('request')) { $collector = $this->getCollector('request'); if ($collector instanceof RequestCollector) { $collector->setResponse($response); } } if ($config->get('debugbar.clockwork') && ! $this->hasCollector('clockwork')) { try { $clockworkCollector = new ClockworkCollector($request, $response); $this->addCollector($clockworkCollector); } catch (Exception $e) { $this->addCollectorException('Cannot add ClockworkCollector', $e); } $this->addClockworkHeaders($response); } if ($config->get('debugbar.add_ajax_timing', false)) { $this->addServerTimingHeaders($response); } if ($response->isRedirection()) { try { $this->stackData(); } catch (Exception $e) { $this->app['log']->error('Debugbar exception: ' . $e->getMessage(), [ 'exception' => $e, ]); } return $response; } try { // Collect + store data, only inject the ID in theheaders $this->sendDataInHeaders(true); } catch (Exception $e) { $this->app['log']->error('Debugbar exception: ' . $e->getMessage(), [ 'exception' => $e, ]); } // Check if it's safe to inject the Debugbar if ( $config->get('debugbar.inject', true) && str_contains($response->headers->get('Content-Type', 'text/html'), 'html') && !$this->isJsonRequest($request) && !$this->isJsonResponse($response) && $response->getContent() !== false && in_array($request->getRequestFormat(), [null, 'html'], true) ) { try { $this->injectDebugbar($response); } catch (Exception $e) { $this->app['log']->error('Debugbar exception: ' . $e->getMessage(), [ 'exception' => $e, ]); } } return $response; } public static function canBeEnabled(): bool { $app = app(); return $app->hasDebugModeEnabled() && !$app->environment('testing', 'production'); } /** * Check if the Debugbar is enabled */ public function isEnabled(): bool { if ($this->enabled === null) { if (!static::canBeEnabled()) { $this->enabled = false; } else { $configEnabled = value(config('debugbar.enabled')); if ($configEnabled === null) { $configEnabled = config('app.debug'); } $this->enabled = $configEnabled && !$this->app->runningInConsole(); } } return $this->enabled; } public function isStorageOpen(Request $request): bool { // Additional safeguards that may never have storage open if (!$this->isEnabled() || !config('app.debug') || app()->isProduction()) { return false; } if ($this->storageOpen === null) { $open = config('debugbar.storage.open'); if (is_callable($open)) { $this->storageOpen = ($open)($request); return $this->storageOpen; } if (is_string($open) && class_exists($open)) { $this->storageOpen = method_exists($open, 'resolve') ? $open::resolve($request) : false; return $this->storageOpen; } if (is_bool($open)) { $this->storageOpen = $open; return $this->storageOpen; } // Allow localhost request when not explicitly allowed/disallowed $this->storageOpen = IpUtils::isPrivateIp($request->getClientIp()); } return $this->storageOpen; } public function requestIsExcluded(Request $request): bool { $except = config('debugbar.except') ?: []; if (!$except) { return false; } $except = array_map(function ($item): string { return $item !== '/' ? trim($item, '/') : $item; }, $except); return $request->is($except); } /** * Check if this is a request to the Debugbar OpenHandler */ protected function isDebugbarRequest(Request $request): bool { return $request->is(config('debugbar.route_prefix') . '*'); } protected function isJsonRequest(Request $request): bool { // 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(); if (isset($acceptable[0]) && in_array($acceptable[0], ['application/json', 'application/javascript'], true)) { return true; } return false; } protected function isJsonResponse(SymfonyResponse $response): bool { if ($response instanceof JsonResponse || $response->headers->get('Content-Type') === 'application/json') { return true; } $content = $response->getContent(); if (is_string($content)) { $content = trim($content); if ($content === '') { return false; } // Quick check to see if it looks like JSON $first = $content[0]; $last = $content[strlen($content) - 1]; if ( ($first === '{' && $last === '}') || ($first === '[' && $last === ']') ) { // Must contain a colon or comma return strpbrk($content, ':,') !== false; } } return false; } /** * Collects meta data about the current request */ public function collectMetaData(): array { $meta = [ 'id' => $this->getCurrentRequestId(), 'datetime' => date('Y-m-d H:i:s'), 'utime' => microtime(true), 'method' => $this->request->getMethod(), 'uri' => $this->request->getRequestUri(), 'ip' => $this->request->getClientIp(), ]; if ($this->processingJob) { $meta['method'] = 'JOB'; $meta['uri'] = $this->processingJob->resolveName() . '@' . $this->processingJob->getConnectionName(); } elseif ($this->app->runningInConsole()) { $meta['method'] = 'CLI'; $meta['uri'] = implode(' ', (new ArgvInput())->getRawTokens()); } return $meta; } public function terminate(): void { if ($this->isCollecting() && $this->data === null && !$this->isDebugbarRequest($this->request)) { $this->collect(); } } /** * Injects the web debug toolbar into the given Response. * * Based on https://github.com/symfony/WebProfilerBundle/blob/master/EventListener/WebDebugToolbarListener.php */ public function injectDebugbar(SymfonyResponse $response): void { $content = $response->getContent(); $renderer = $this->getJavascriptRenderer(); $widget = "\n" . $renderer->renderHead() . $renderer->render(); // Try to put the widget at the end, directly before the $pos = strripos($content, ''); if (false !== $pos) { $content = substr($content, 0, $pos) . $widget . substr($content, $pos); } else { $content = $content . $widget; } $original = null; if ($response instanceof 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 ($response instanceof Response && $original) { $response->original = $original; } } /** * Disable the Debugbar */ public function disable(): void { $this->enabled = false; } public function reset(): void { parent::reset(); $this->timeCollector->reset(); $this->exceptionsCollector->reset(); $this->messagesCollector->reset(); $this->enabled = null; $this->storageOpen = null; $this->responseIsModified = false; $this->httpDriver = null; } /** * Adds a measure */ public function addMeasure(string $label, float $start, ?float $end = null, array $params = [], ?string $collector = null, ?string $group = null): void { $this->timeCollector->addMeasure($label, $start, $end, $params, $collector, $group); } /** * Utility function to measure the execution of a Closure */ public function measure(string $label, \Closure $closure, ?string $collector = null, ?string $group = null): mixed { return $this->timeCollector->measure($label, $closure, $collector, $group); } /** * Magic calls for adding messages */ public function __call(string $method, array $args): void { $messageLevels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug', 'log']; if (in_array($method, $messageLevels, true)) { foreach ($args as $arg) { $this->addMessage($arg, $method); } } } /** * Adds a message to the MessagesCollector * * A message can be anything from an object to a string */ public function addMessage(mixed $message, string $label = 'info', array $context = []): void { $this->messagesCollector->addMessage($message, $label, $context); } /** * Check the version of Laravel */ public function checkVersion(string $version, string $operator = ">="): bool { return version_compare($this->app->version(), $version, $operator); } protected function selectStorage(DebugBar $debugbar): void { /** @var Repository $config */ $config = config(); if ($config->get('debugbar.storage.enabled')) { $driver = strtolower($config->get('debugbar.storage.driver', 'file')); switch ($driver) { case 'pdo': $connection = $config->get('debugbar.storage.connection'); $table = $this->app['db']->getTablePrefix() . 'phpdebugbar'; $pdo = $this->app['db']->connection($connection)->getPdo(); $storage = new PdoStorage($pdo, $table); break; case 'redis': $connection = $config->get('debugbar.storage.connection'); $client = $this->app['redis']->connection($connection); if (is_a($client, 'Illuminate\Redis\Connections\Connection', false)) { $client = $client->client(); } $storage = new RedisStorage($client); break; case 'custom': $class = $config->get('debugbar.storage.provider'); $storage = $this->app->make($class); break; case 'socket': throw new \RuntimeException('Socket storage is not supported anymore.'); case 'file': $path = $config->get('debugbar.storage.path'); $storage = new FileStorage($path); break; case 'sqlite': $path = $config->get('debugbar.storage.path'); $storage = new SqliteStorage($path . '/debugbar.sqlite'); break; default: throw new \RuntimeException('Invalid storage selected: ' . $driver); } $debugbar->setStorage($storage); } } protected function addClockworkHeaders(SymfonyResponse $response): void { $prefix = config('debugbar.route_prefix'); $response->headers->set('X-Clockwork-Id', $this->getCurrentRequestId(), true); $response->headers->set('X-Clockwork-Version', "9", true); $response->headers->set('X-Clockwork-Path', $prefix . '/clockwork/', true); } /** * Add Server-Timing headers for the TimeData collector * * @see https://www.w3.org/TR/server-timing/ */ protected function addServerTimingHeaders(SymfonyResponse $response): void { if ($this->hasCollector('time')) { $collector = $this->timeCollector; $headers = []; foreach ($collector->collect()['measures'] as $m) { $headers[] = sprintf('app;desc="%s";dur=%F', str_replace(["\n", "\r"], ' ', str_replace('"', "'", $m['label'])), $m['duration'] * 1000); } $response->headers->set('Server-Timing', $headers, false); } } private function getRemoteServerReplacements(): array { $localPath = config('debugbar.local_sites_path') ?: base_path(); $remotePaths = array_filter(explode(',', config('debugbar.remote_sites_path') ?: '')) ?: [base_path()]; return array_fill_keys($remotePaths, $localPath); } } ================================================ FILE: src/LaravelHttpDriver.php ================================================ request = $request; } public function setResponse(?Response $response): void { $this->response = $response; } public function setHeaders(array $headers): void { if (!is_null($this->response)) { $this->response->headers->add($headers); } } public function output(string $content): void { if (!is_null($this->response)) { $existingContent = $this->response->getContent(); $content = $existingContent ? $existingContent . $content : $content; $this->response->setContent($content); } } public function isSessionStarted(): bool { return true; } public function setSessionValue(string $name, mixed $value): void { if ($value !== null) { $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } $cookie = Cookie::make($name, $value, 0); if ($this->response) { $this->response->headers->setCookie($cookie); } else { Cookie::queue($cookie); } } /** * {@inheritDoc} */ public function hasSessionValue(string $name): bool { return $this->request->hasCookie($name); } /** * {@inheritDoc} */ public function getSessionValue(string $name): mixed { $value = $this->request->cookie($name); if ($value !== null) { $value = json_decode($value, true); } return $value; } /** * {@inheritDoc} */ public function deleteSessionValue(string $name): void { $this->setSessionValue($name, null); } } ================================================ FILE: src/Middleware/DebugbarEnabled.php ================================================ debugbar->isEnabled()) { abort(404); } return $next($request); } } ================================================ FILE: src/Middleware/StopRecordingTelescope.php ================================================ ['required', 'string', 'in:js,css'], ]; } } ================================================ FILE: src/Requests/CacheDeleteRequest.php ================================================ hasValidSignature() && debugbar()->isStorageOpen($this); } public function rules(): array { return [ 'tags' => ['sometimes', 'array'], 'tags.*' => ['string'], ]; } } ================================================ FILE: src/Requests/OpenHandlerRequest.php ================================================ ['nullable', 'string'], ]; } } ================================================ FILE: src/Requests/QueriesExplainRequest.php ================================================ ['required', 'string'], 'query' => ['required', 'string'], 'bindings' => ['nullable', 'array'], 'hash' => ['required', 'string'], 'mode' => ['nullable', 'string', 'in:explain,visual,result'], 'format' => ['nullable', 'string'], ]; } } ================================================ FILE: src/ServiceProvider.php ================================================ mergeConfigFrom($configPath, 'debugbar'); $this->app->alias( DataFormatter::class, DataFormatterInterface::class, ); $this->app->singleton(LaravelDebugbar::class); $this->app->alias(LaravelDebugbar::class, 'debugbar'); $this->app->alias(LaravelDebugbar::class, DebugBar::class); Collection::macro('debug', function (): \Illuminate\Support\Collection { debug($this); return $this; }); } /** * Bootstrap the application events. * */ public function boot(Dispatcher $events): void { if ($this->app->runningInConsole()) { $configPath = __DIR__ . '/../config/debugbar.php'; $this->publishes([$configPath => $this->getConfigPath()], 'config'); $this->commands([ClearCommand::class]); } // Eearly return if debugbar can not enabled if (!LaravelDebugbar::canBeEnabled()) { return; } $this->loadRoutesFrom(__DIR__ . '/debugbar-routes.php'); // Resolve the LaravelDebugbar instance during boot to force it to be loaded in the Octane sandbox try { $debugbar = $this->app->make(LaravelDebugbar::class); } catch (\Throwable $e) { // Errors can occur when removing LaravelDebugbar with composer scripts, when php-debugbar is not installed report($e); return; } // Reset the debugbar instance on each new Octane request $events->listen(RequestReceived::class, ResetDebugbar::class); // Handle response $events->listen(RequestHandled::class, function ($event) use ($debugbar): void { $debugbar->handleResponse($event->request, $event->response); }); // Store any data collected during termination but not already stored $events->listen(Terminating::class, function ($event) use ($debugbar): void { $debugbar->terminate(); }); if (config('debugbar.collect_jobs')) { $events->listen(JobProcessing::class, function (JobProcessing $event) use ($debugbar): void { // Sync jobs in non-console jobs are just requests if ($event->connectionName === 'sync' && !$this->app->runningInConsole()) { return; } $debugbar->enable(); $debugbar->setProcessingJob($event->job); }); $events->listen(JobProcessed::class, function (JobProcessed $event) use ($debugbar): void { if ($debugbar->getProcessingJob()) { $debugbar->collect(); $debugbar->setProcessingJob(null); $debugbar->reset(); } }); } // Exclude debugbar cookies from encryption EncryptCookies::except($debugbar->getStackDataSessionNamespace()); // Attach listeners when debugbar should be enabled if ($debugbar->isEnabled() && !$debugbar->requestIsExcluded($this->app['request'])) { $debugbar->boot(); } // Register boot time, regardless of already being booted $this->booted(fn() => $debugbar->booted()); } /** * Get the config path * */ protected function getConfigPath(): string { return config_path('debugbar.php'); } } ================================================ FILE: src/Support/Clockwork/ClockworkCollector.php ================================================ * */ class ClockworkCollector extends DataCollector implements DataCollectorInterface, Renderable { protected Request $request; protected Response $response; public function __construct( Request $request, Response $response ) { $this->request = $request; $this->response = $response; } /** * {@inheritDoc} */ public function getName(): string { return 'clockwork'; } /** * {@inheritDoc} */ public function getWidgets(): array { return []; } /** * {@inheritdoc} */ public function collect(): array { $request = $this->request; $response = $this->response; $data = [ 'getData' => $request->query->all(), 'postData' => $request->request->all(), 'headers' => $request->headers->all(), 'cookies' => $request->cookies->all(), 'uri' => $request->getRequestUri(), 'method' => $request->getMethod(), 'responseStatus' => $response->getStatusCode(), ]; if ($this->request->hasSession()) { $data['sessionData'] = $this->request->getSession()->all(); } if (isset($data['headers']['authorization'][0])) { $data['headers']['authorization'][0] = substr($data['headers']['authorization'][0], 0, 12) . '******'; } return $this->hideMaskedValues($data); } } ================================================ FILE: src/Support/Clockwork/Converter.php ================================================ $meta['id'], 'method' => $meta['method'], 'uri' => $meta['uri'], 'time' => $meta['utime'], 'headers' => [], 'cookies' => [], 'emailsData' => [], 'getData' => [], 'log' => [], 'postData' => [], 'sessionData' => [], 'timelineData' => [], 'viewsData' => [], 'controller' => null, 'responseTime' => null, 'responseStatus' => null, 'responseDuration' => 0, ]; if (isset($data['clockwork'])) { $output = array_merge($output, $data['clockwork']); } if (isset($data['memory']['peak_usage'])) { $output['memoryUsage'] = $data['memory']['peak_usage']; } if (isset($data['time']['measures'])) { $time = $data['time']; $output['time'] = $time['start']; $output['responseTime'] = $time['end']; $output['responseDuration'] = $time['duration'] * 1000; foreach ($time['measures'] as $measure) { $output['timelineData'][] = [ 'data' => [], 'description' => $measure['label'], 'duration' => $measure['duration'] * 1000, 'end' => $measure['end'], 'start' => $measure['start'], 'relative_start' => $measure['start'] - $time['start'], ]; } } if (isset($data['route'])) { $route = $data['route']; $controller = null; if (isset($route['controller'])) { $controller = $route['controller']; } elseif (isset($route['uses'])) { $controller = $route['uses']; } $output['controller'] = preg_replace('/]*>(.*?)<\/a>/i', '', (string) $controller) ?: null; [$method, $uri] = explode(' ', $route['uri'], 2); $output['routes'][] = [ 'action' => $output['controller'], 'after' => $route['after'] ?? null, 'before' => $route['before'] ?? null, 'method' => $method, 'name' => $route['as'] ?? null, 'uri' => $uri, ]; } if (isset($data['messages']['messages'])) { foreach ($data['messages']['messages'] as $message) { $output['log'][] = [ 'message' => $message['message'], 'time' => $message['time'], 'level' => $message['label'], ]; } } if (isset($data['queries']['statements'])) { $queries = $data['queries']; foreach ($queries['statements'] as $statement) { if ($statement['type'] === 'explain' || $statement['type'] === 'info') { continue; } $output['databaseQueries'][] = [ 'query' => $statement['sql'], 'bindings' => $statement['params'], 'duration' => $statement['duration'] * 1000, 'time' => $statement['start'] ?? null, 'connection' => $statement['connection'], ]; } $output['databaseDuration'] = $queries['accumulated_duration'] * 1000; } if (isset($data['models']['data'])) { $output['modelsActions'] = []; $output['modelsCreated'] = []; $output['modelsUpdated'] = []; $output['modelsDeleted'] = []; $output['modelsRetrieved'] = []; foreach ($data['models']['data'] as $model => $value) { foreach ($value as $event => $count) { $eventKey = 'models' . ucfirst($event); if (isset($output[$eventKey])) { $output[$eventKey][$model] = $count; } } } } if (isset($data['views']['templates'])) { foreach ($data['views']['templates'] as $view) { $output['viewsData'][] = [ 'description' => 'Rendering a view', 'duration' => 0, 'end' => 0, 'start' => $view['start'] ?? 0, 'data' => [ 'name' => $view['name'], 'data' => $view['params'], ], ]; } } if (isset($data['event']['measures'])) { foreach ($data['event']['measures'] as $event) { $event['data'] = []; $event['listeners'] = []; foreach ($event['params'] ?? [] as $key => $param) { $event[is_numeric($key) ? 'data' : 'listeners'] = $param; } $output['events'][] = [ 'event' => ['event' => $event['label']], 'data' => $event['data'], 'time' => $event['start'], 'duration' => $event['duration'] * 1000, 'listeners' => $event['listeners'], ]; } } if (isset($data['symfonymailer_mails']['mails'])) { foreach ($data['symfonymailer_mails']['mails'] as $mail) { $output['emailsData'][] = [ 'data' => [ 'to' => implode(', ', $mail['to']), 'subject' => $mail['subject'], 'headers' => isset($mail['headers']) ? explode("\n", $mail['headers']) : null, ], ]; } } return $output; } } ================================================ FILE: src/Support/Explain.php ================================================ getDriverName(); if ($driver === 'pgsql') { return true; } if ($driver === 'mysql') { // Laravel 11 added a new MariaDB database driver but older Laravel versions handle MySQL and MariaDB with // the same driver - and even with new versions you can use the MySQL driver while connection to a MariaDB // database. This query uses a feature implemented only in MariaDB to differentiate them. try { DB::connection($connection)->select('SELECT * FROM seq_1_to_1'); return false; } catch (QueryException) { // This exception is expected when using MySQL as sequence tables are only available with MariaDB. So // the exception gets silenced as the check for MySQL has succeeded. return true; } } return false; } public function confirmVisualExplain(string $connection): ?string { return match (DB::connection($connection)->getDriverName()) { 'mysql' => 'The query and EXPLAIN output is sent to mysqlexplain.com. Do you want to continue?', 'pgsql' => 'The query and EXPLAIN output is sent to explain.dalibo.com. Do you want to continue?', default => null, }; } public function hash(string $connection, string $sql, ?array $bindings): string { $bindings = json_encode($bindings); return hash_hmac('sha256', "{$connection}::{$sql}::{$bindings}", config('app.key')); } private function verify(string $connection, string $sql, array $bindings, string $hash): void { $computedHash = $this->hash($connection, $sql, $bindings); if (!hash_equals($computedHash, $hash)) { throw new Exception('Query to execute could not be verified.'); } } public function generateSelectResult(string $connection, string $sql, array $bindings, string $hash, ?string $format): array { $this->verify($connection, $sql, $bindings, $hash); $this->validateReadOnlyQuery($sql); $result = DB::connection($connection)->select($sql, $bindings); if ($format === 'dump') { $result = DataCollector::getDefaultDataFormatter()->formatVar($result); } return ['result' => $result]; } public function generateRawExplain(string $connection, string $sql, array $bindings, string $hash): array { $this->verify($connection, $sql, $bindings, $hash); $this->validateReadOnlyQuery($sql); $connection = DB::connection($connection); return match ($driver = $connection->getDriverName()) { 'mariadb', 'mysql' => $connection->select("EXPLAIN {$sql}", $bindings), 'pgsql' => array_column($connection->select("EXPLAIN {$sql}", $bindings), 'QUERY PLAN'), default => throw new Exception("Visual explain not available for driver '{$driver}'."), }; } public function generateVisualExplain(string $connection, string $sql, array $bindings, string $hash): string { $this->verify($connection, $sql, $bindings, $hash); $this->validateReadOnlyQuery($sql); if (!$this->isVisualExplainSupported($connection)) { throw new Exception('Visual explain not available for this connection.'); } $connection = DB::connection($connection); return match ($connection->getDriverName()) { 'mysql' => $this->generateVisualExplainMysql($connection, $sql, $bindings), 'pgsql' => $this->generateVisualExplainPgsql($connection, $sql, $bindings), default => throw new Exception("Visual explain not available for driver '{$connection->getDriverName()}'."), }; } private function validateReadOnlyQuery(string $sql): void { $normalized = ltrim($sql); if (!$this->isReadOnlyQuery($normalized)) { throw new Exception('Only SELECT queries can be explained or executed.'); } } private static function redactBindings(array $bindings): array { return array_map(fn() => '?', $bindings); } private function generateVisualExplainMysql(ConnectionInterface $connection, string $query, array $bindings): string { return Http::withHeaders([ 'User-Agent' => 'fruitcake/laravel-debugbar', ])->post('https://api.mysqlexplain.com/v2/explains', [ 'query' => $query, 'bindings' => self::redactBindings($bindings), 'version' => $connection->selectOne("SELECT VERSION()")->{'VERSION()'}, 'explain_json' => $connection->selectOne("EXPLAIN FORMAT=JSON {$query}", $bindings)->EXPLAIN, 'explain_tree' => rescue(fn() => $connection->selectOne("EXPLAIN FORMAT=TREE {$query}", $bindings)->EXPLAIN, report: false), ])->throw()->json('url'); } private function generateVisualExplainPgsql(ConnectionInterface $connection, string $query, array $bindings): string { return (string) Http::asForm()->post('https://explain.dalibo.com/new', [ 'query' => $query, 'plan' => $connection->selectOne("EXPLAIN (FORMAT JSON) {$query}", $bindings)->{'QUERY PLAN'}, 'title' => '', ])->effectiveUri(); } } ================================================ FILE: src/Support/Octane/ResetDebugbar.php ================================================ sandbox->resolved(LaravelDebugbar::class)) { return; } with($event->sandbox->make(LaravelDebugbar::class), function (LaravelDebugbar $debugbar) use ($event): void { $debugbar->setApplication($event->sandbox); $debugbar->setRequest($event->request); $debugbar->reset(); if ($debugbar->isEnabled() && !$debugbar->requestIsExcluded($event->request)) { $debugbar->boot(); } if ($requestStartTime = $event->request->server->get('REQUEST_TIME_FLOAT')) { $debugbar->getTimeCollector()->setRequestStartTime((float) $requestStartTime); } $debugbar->startMeasure('application', 'Application', 'time'); }); } } ================================================ FILE: src/Twig/Extension/Debug.php ================================================ messagesCollector) { $app = app(); if ($app->bound('debugbar') && $app['debugbar']->hasCollector('messages')) { $this->messagesCollector = $app['debugbar']['messages']; } } parent::debug($env, $context); } } ================================================ FILE: src/Twig/Extension/Dump.php ================================================ bound('debugbar')) { $this->debugbar = $app['debugbar']; } parent::__construct(null, 'stopwatch'); } public function getDebugbar() { return $this->debugbar; } public function getTokenParsers() { return [ /* * {% measure foo %} * Some stuff which will be recorded on the timeline * {% endmeasure %} */ new MeasureTwigTokenParser(!is_null($this->debugbar), $this->tagName, $this->getName()), ]; } public function startMeasure(...$arg) { if (!$this->debugbar || !$this->debugbar->hasCollector('time')) { return; } $this->debugbar->getCollector('time')->startMeasure(...$arg); } public function stopMeasure(...$arg) { if (!$this->debugbar || !$this->debugbar->hasCollector('time')) { return; } $this->debugbar->getCollector('time')->stopMeasure(...$arg); } } ================================================ FILE: src/debugbar-routes.php ================================================ app('config')->get('debugbar.route_prefix'), 'domain' => app('config')->get('debugbar.route_domain'), 'middleware' => array_merge( app('config')->get('debugbar.route_middleware', []), [DebugbarEnabled::class, StopRecordingTelescope::class] ), ]; app('router')->group($routeConfig, function ($router): void { $router->get('open', [OpenHandlerController::class, 'handle'])->name('debugbar.openhandler'); $router->delete('cache/{key}', [CacheController::class, 'delete'])->where('key', '.*')->name('debugbar.cache.delete'); $router->post('queries/explain', [QueriesController::class, 'explain'])->name('debugbar.queries.explain'); $router->get('clockwork/{id}', [OpenHandlerController::class, 'clockwork'])->name('debugbar.clockwork'); $router->get('assets', [AssetController::class, 'getAssets'])->name('debugbar.assets'); if (class_exists(\Laravel\Telescope\Telescope::class)) { $router->get('telescope/{id}', [TelescopeController::class, 'show'])->name('debugbar.telescope'); } }); ================================================ FILE: src/helpers.php ================================================ hasCollector($collector) ? $debugbar->getCollector($collector) : null; } return $debugbar; } } if (!function_exists('debug')) { /** * Adds one or more messages to the MessagesCollector * */ function debug(mixed ...$value): void { $debugbar = debugbar(); foreach ($value as $message) { $debugbar->addMessage($message, 'debug'); } } } ================================================ FILE: tests/BrowserTestCase.php ================================================ Debugbar::class]; } } ================================================ FILE: tests/Controllers/AssetControllerTest.php ================================================ getJson('/_debugbar/assets'); $response->assertUnprocessable(); } public function testAssetRouteRejectsInvalidType(): void { $response = $this->getJson('/_debugbar/assets?type=invalid'); $response->assertUnprocessable(); } public function testAssetRouteAcceptsCssType(): void { $response = $this->get('/_debugbar/assets?type=css'); $response->assertOk(); } public function testAssetRouteAcceptsJsType(): void { $response = $this->get('/_debugbar/assets?type=js'); $response->assertOk(); } } ================================================ FILE: tests/Controllers/CacheControllerTest.php ================================================ signedRoute('debugbar.cache.delete', ['key' => urlencode($key)]); $this->delete($url)->assertOk()->assertJson(['success' => true]); static::assertFalse(Cache::has($key)); } public function testItRejectsRequestWithInvalidSignature(): void { $key = 'test-key'; Cache::put($key, 'test-value'); $this->delete('/_debugbar/cache/' . $key)->assertForbidden(); static::assertTrue(Cache::has($key)); } public function testItRejectsRequestWhenStorageIsNotOpen(): void { $this->app['config']->set('debugbar.storage.open', false); $this->resetStorageOpen(); $key = 'test-key'; Cache::put($key, 'test-value'); $url = url()->signedRoute('debugbar.cache.delete', ['key' => urlencode($key)]); $this->delete($url)->assertForbidden(); static::assertTrue(Cache::has($key)); } public function testItRejectsInvalidTagsParameter(): void { $key = 'test-key'; Cache::put($key, 'test-value'); $url = url()->signedRoute('debugbar.cache.delete', ['key' => urlencode($key), 'tags' => 'not-an-array']); $this->deleteJson($url)->assertUnprocessable(); } /** @return array> */ public static function cacheKeyProvider(): array { return [ 'simple key' => ['test-delete-key'], 'key with route parameter syntax' => ['pattern::category,resources/{resource}'], 'key with colons and slashes' => ['key:with:colons/and/slashes'], ]; } } ================================================ FILE: tests/Controllers/DebugbarEnabledMiddlewareTest.php ================================================ get('/_debugbar/open')->assertNotFound(); $this->get('/_debugbar/assets?type=js')->assertNotFound(); $this->postJson('/_debugbar/queries/explain')->assertNotFound(); $this->delete('/_debugbar/cache/test-key')->assertNotFound(); } public function testRoutesAreAccessibleWhenDebugbarIsEnabled(): void { $this->enableDebugbar(); // Assets route should work (or return validation error, not 404) $this->get('/_debugbar/assets?type=js')->assertOk(); // OpenHandler should be accessible (not 404) $response = $this->get('/_debugbar/open'); static::assertNotEquals(404, $response->getStatusCode()); } protected function enableDebugbar(): void { $debugbar = app(LaravelDebugbar::class); (new ReflectionObject($debugbar)) ->getProperty('enabled') ->setValue($debugbar, true); } } ================================================ FILE: tests/Controllers/OpenHandlerControllerTest.php ================================================ app['config']->set('debugbar.storage.open', false); $this->resetStorageOpen(); $response = $this->get('/_debugbar/open?op=find'); $response->assertOk(); $response->assertJsonFragment(['method' => 'ERROR']); } public function testOpenHandlerAllowsGetOpWithoutStorageOpen(): void { $this->app['config']->set('debugbar.storage.open', false); $this->resetStorageOpen(); // op=get is always allowed even without storage open $response = $this->get('/_debugbar/open'); $response->assertOk(); } public function testOpenHandlerWorksWhenStorageIsOpen(): void { $this->app['config']->set('debugbar.storage.open', true); $this->resetStorageOpen(); $this->ensureStorageDirectory(); $response = $this->get('/_debugbar/open?op=find'); $response->assertOk(); } public function testOpenHandlerStorageOpenCallbackReceivesRequest(): void { $receivedRequest = null; $this->app['config']->set('debugbar.storage.open', function ($request) use (&$receivedRequest) { $receivedRequest = $request; return $request->header('X-Debugbar-Token') === 'valid-token'; }); $this->resetStorageOpen(); // Without the header, callback returns false — storage is closed $response = $this->get('/_debugbar/open?op=find'); $response->assertOk(); $response->assertJsonFragment(['method' => 'ERROR']); static::assertNotNull($receivedRequest); // With the header, callback returns true — storage is open $this->resetStorageOpen(); $this->ensureStorageDirectory(); $response = $this->get('/_debugbar/open?op=find', ['X-Debugbar-Token' => 'valid-token']); $response->assertOk(); $data = $response->json(); if (is_array($data) && isset($data[0]['method'])) { static::assertNotEquals('ERROR', $data[0]['method']); } } private function ensureStorageDirectory(): void { $path = config('debugbar.storage.path', storage_path('debugbar')); if (!is_dir($path)) { mkdir($path, 0o755, true); } } } ================================================ FILE: tests/Controllers/QueriesControllerTest.php ================================================ app['config']->set('debugbar.storage.open', false); $this->app['config']->set('debugbar.options.db.explain.enabled', true); $this->resetStorageOpen(); $response = $this->postJson('/_debugbar/queries/explain', [ 'connection' => 'sqlite', 'query' => 'SELECT 1', 'bindings' => [], 'hash' => 'abc123', ]); $response->assertStatus(400); $response->assertJson(['success' => false]); } public function testExplainReturnsErrorWhenExplainIsDisabled(): void { $this->app['config']->set('debugbar.storage.open', true); $this->app['config']->set('debugbar.options.db.explain.enabled', false); $response = $this->postJson('/_debugbar/queries/explain', [ 'connection' => 'sqlite', 'query' => 'SELECT 1', 'bindings' => [], 'hash' => 'abc123', ]); $response->assertStatus(400); $response->assertJson([ 'success' => false, 'message' => 'EXPLAIN is currently disabled in the Debugbar.', ]); } public function testExplainValidatesRequiredFields(): void { $this->app['config']->set('debugbar.storage.open', true); $this->app['config']->set('debugbar.options.db.explain.enabled', true); $response = $this->postJson('/_debugbar/queries/explain', []); $response->assertUnprocessable(); $response->assertJsonValidationErrors(['connection', 'query', 'hash']); } public function testExplainValidatesModeValues(): void { $this->app['config']->set('debugbar.storage.open', true); $this->app['config']->set('debugbar.options.db.explain.enabled', true); $response = $this->postJson('/_debugbar/queries/explain', [ 'connection' => 'sqlite', 'query' => 'SELECT 1', 'bindings' => [], 'hash' => 'abc123', 'mode' => 'invalid', ]); $response->assertUnprocessable(); $response->assertJsonValidationErrors(['mode']); } public function testExplainAcceptsValidModeValues(): void { $this->app['config']->set('debugbar.storage.open', true); $this->app['config']->set('debugbar.options.db.explain.enabled', true); foreach (['visual', 'result'] as $mode) { $response = $this->postJson('/_debugbar/queries/explain', [ 'connection' => 'sqlite', 'query' => 'SELECT 1', 'bindings' => [], 'hash' => 'abc123', 'mode' => $mode, ]); static::assertNotEquals(422, $response->getStatusCode(), "Mode '{$mode}' should be accepted"); } } } ================================================ FILE: tests/DataCollector/CacheCollectorTest.php ================================================ boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new KeyWritten('array', 'test-key', 'test-value', 60, [])); $collector->onCacheEvent(new CacheHit('array', 'test-key', 'test-value', [])); $data = $collector->collect(); static::assertEquals(2, $data['nb_measures']); } #[DataProvider('cacheKeyProvider')] public function testItGeneratesDeleteUrlForCacheHit(string $key): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new CacheHit('array', $key, 'value', [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); static::assertArrayHasKey('delete_url', $lastMeasure); static::assertStringContainsString('_debugbar/cache/', $lastMeasure['delete_url']); static::assertStringContainsString(urlencode($key), $lastMeasure['delete_url']); } #[DataProvider('cacheKeyProvider')] public function testItGeneratesDeleteUrlForKeyWritten(string $key): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new KeyWritten('array', $key, 'value', 60, [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); static::assertArrayHasKey('delete_url', $lastMeasure); static::assertStringContainsString(urlencode($key), $lastMeasure['delete_url']); } #[DataProvider('sizeDataProvider')] public function testItCalculatesMemoryUsageForKeyWritten(mixed $value, int $expectedSize): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new KeyWritten('array', 'size-key', $value, 60, [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); static::assertEquals($expectedSize, $lastMeasure['memory']); } #[DataProvider('sizeDataProvider')] public function testItCalculatesMemoryUsageForCacheHit(mixed $value, int $expectedSize): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new CacheHit('array', 'size-key', $value, [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); static::assertEquals($expectedSize, $lastMeasure['memory']); } public function testItHandlesClosureValueGracefully(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new KeyWritten('array', 'closure-key', function () { return 'hello'; }, 60, [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); // Closures can't be serialized, so memory should be 0 static::assertEquals(0, $lastMeasure['memory']); static::assertEquals(1, $data['nb_measures']); } public function testItHandlesClosureValueInCacheHitGracefully(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new CacheHit('array', 'closure-key', function () { return 'hello'; }, [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); // Closures can't be serialized, so memory should be 0 static::assertEquals(0, $lastMeasure['memory']); static::assertEquals(1, $data['nb_measures']); } public function testCacheMissHasNoMemoryUsage(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new CacheMissed('array', 'miss-key', [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); // CacheMissed has no value, so memory should be 0 static::assertEquals(0, $lastMeasure['memory']); } public function testItCollectsRememberMissPattern(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); // Simulate a remember() call: first a RetrievingKey, then a miss, then a write $collector->onStartCacheEvent(new RetrievingKey('array', 'remember-key', [])); $collector->onCacheEvent(new CacheMissed('array', 'remember-key', [])); $collector->onStartCacheEvent(new WritingKey('array', 'remember-key', 'computed-value', 300, [])); $collector->onCacheEvent(new KeyWritten('array', 'remember-key', 'computed-value', 300, [])); $data = $collector->collect(); static::assertEquals(2, $data['nb_measures']); $measures = array_values($data['measures']); static::assertStringContainsString('missed', $measures[0]['label']); static::assertStringContainsString('written', $measures[1]['label']); static::assertGreaterThan(0, $measures[1]['memory']); } public function testItCollectsRememberHitPattern(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); // Simulate a remember() call that hits cache $collector->onStartCacheEvent(new RetrievingKey('array', 'remember-key', [])); $collector->onCacheEvent(new CacheHit('array', 'remember-key', 'cached-value', [])); $data = $collector->collect(); static::assertEquals(1, $data['nb_measures']); $lastMeasure = end($data['measures']); static::assertStringContainsString('hit', $lastMeasure['label']); static::assertGreaterThan(0, $lastMeasure['memory']); } public function testItCollectsForgottenEvent(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); $collector->onCacheEvent(new KeyForgotten('array', 'forget-key', [])); $data = $collector->collect(); static::assertEquals(1, $data['nb_measures']); $lastMeasure = end($data['measures']); static::assertStringContainsString('forgotten', $lastMeasure['label']); } public function testStartEventTimingIsUsed(): void { debugbar()->boot(); /** @var CacheCollector $collector */ $collector = debugbar()->getCollector('cache'); // Use CacheMissed (no value property) so the event hash matches between start and end $collector->onStartCacheEvent(new RetrievingKey('array', 'timed-key', [])); usleep(10000); $collector->onCacheEvent(new CacheMissed('array', 'timed-key', [])); $data = $collector->collect(); $lastMeasure = end($data['measures']); static::assertGreaterThan(0.001, $lastMeasure['duration']); } /** @return array */ public static function sizeDataProvider(): array { return [ 'string value' => ['hello world', strlen(serialize('hello world')) * 8], 'integer value' => [42, strlen(serialize(42)) * 8], 'float value' => [3.14, strlen(serialize(3.14)) * 8], 'boolean value' => [true, strlen(serialize(true)) * 8], 'null value' => [null, 0], // null fails isset() check, so no memoryUsage is calculated 'array value' => [['a', 'b', 'c'], strlen(serialize(['a', 'b', 'c'])) * 8], 'nested array' => [['key' => ['nested' => 'value']], strlen(serialize(['key' => ['nested' => 'value']])) * 8], 'empty string' => ['', strlen(serialize('')) * 8], 'large string' => [str_repeat('x', 1000), strlen(serialize(str_repeat('x', 1000))) * 8], 'stdClass object' => [(object) ['foo' => 'bar'], strlen(serialize((object) ['foo' => 'bar'])) * 8], ]; } /** @return array> */ public static function cacheKeyProvider(): array { return [ 'simple key' => ['simple-key'], 'key with route parameter syntax' => ['pattern::category,resources/{resource}'], 'key with colons and slashes' => ['key:with:colons/and/slashes'], ]; } } ================================================ FILE: tests/DataCollector/GateCollectorTest.php ================================================ boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\GateCollector $collector */ $collector = debugbar()->getCollector('gate'); $collector->setDataFormatter(new DataFormatter()); $user = new User([ 'id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => 'password', ]); $user->can('view', $user); Gate::before(function ($user, $ability, $result, $arguments = []) { return true; }); $user->can('view', $user); $collect = $collector->collect(); static::assertEquals(2, $collect['count']); $gateError = $collect['messages'][0]; static::assertEquals('error', $gateError['label']); static::assertEquals( 'view Fruitcake\LaravelDebugbar\Tests\Models\User(id=1)', $gateError['message'], ); static::assertEquals( [ 'ability' => '"view"', 'target' => '"Fruitcake\LaravelDebugbar\Tests\Models\User(id=1)"', 'result' => 'null', 'user' => '1', 'arguments' => 'array:1 [ 0 => "Fruitcake\LaravelDebugbar\Tests\Models\User(id=1)" ]', ], $gateError['context'] ); $gateSuccess = $collect['messages'][1]; static::assertEquals('success', $gateSuccess['label']); static::assertEquals( 'view Fruitcake\LaravelDebugbar\Tests\Models\User(id=1)', $gateSuccess['message'], ); static::assertEquals( $gateSuccess['context'], [ 'ability' => '"view"', 'target' => '"Fruitcake\LaravelDebugbar\Tests\Models\User(id=1)"', 'result' => 'true', 'user' => '1', 'arguments' => 'array:1 [ 0 => "Fruitcake\LaravelDebugbar\Tests\Models\User(id=1)" ]', ], ); } } ================================================ FILE: tests/DataCollector/HttpClientCollectorTest.php ================================================ 'application/json'], '{"success":true}'); $response = new Response($psrResponse); $event = new ResponseReceived($request, $response); $collector->addEvent($event); $data = $collector->collect(); static::assertEquals(1, $data['nb_requests']); static::assertCount(1, $data['requests']); $requestData = $data['requests'][0]; static::assertEquals('GET', $requestData['method']); static::assertEquals('https://example.com/api/test', $requestData['url']); static::assertEquals(200, $requestData['status']); static::assertArrayHasKey('details', $requestData); } public function testItCollectsConnectionFailedEvents() { $collector = new HttpClientCollector(); $request = new Request(new \GuzzleHttp\Psr7\Request('POST', 'https://example.com/api/fail')); $exception = new ConnectionException('Connection failed'); $event = new ConnectionFailed($request, $exception); $collector->addEvent($event); $data = $collector->collect(); static::assertEquals(1, $data['nb_requests']); static::assertCount(1, $data['requests']); $requestData = $data['requests'][0]; static::assertEquals('POST', $requestData['method']); static::assertEquals('https://example.com/api/fail', $requestData['url']); static::assertNull($requestData['status']); static::assertArrayHasKey('details', $requestData); // Exception is not available in Laravel 10 if (isset($event->exception)) { static::assertArrayHasKey('exception', $requestData['details']); } } public function testItMasksAuthorizationHeader() { HttpClientCollector::setDefaultDataFormatter(new HtmlDataFormatter()); $collector = new HttpClientCollector(); $request = new Request( new \GuzzleHttp\Psr7\Request( 'GET', 'https://example.com/api/test', ['Authorization' => 'Bearer secret-token'] ) ); $psrResponse = new Psr7Response(200, [], ''); $response = new Response($psrResponse); $event = new ResponseReceived($request, $response); $collector->addEvent($event); $data = $collector->collect(); $requestData = $data['requests'][0]; static::assertArrayHasKey('request_headers', $requestData['details']); static::assertStringNotContainsString('secret-token', $requestData['details']['request_headers']); } public function testItCollectsMultipleEvents() { $collector = new HttpClientCollector(); $request1 = new Request(new \GuzzleHttp\Psr7\Request('GET', 'https://example.com/api/1')); $psrResponse1 = new Psr7Response(200, [], '{"id":1}'); $response1 = new Response($psrResponse1); $event1 = new ResponseReceived($request1, $response1); $request2 = new Request(new \GuzzleHttp\Psr7\Request('POST', 'https://example.com/api/2')); $psrResponse2 = new Psr7Response(201, [], '{"id":2}'); $response2 = new Response($psrResponse2); $event2 = new ResponseReceived($request2, $response2); $collector->addEvent($event1); $collector->addEvent($event2); $data = $collector->collect(); static::assertEquals(2, $data['nb_requests']); static::assertCount(2, $data['requests']); static::assertEquals('GET', $data['requests'][0]['method']); static::assertEquals('POST', $data['requests'][1]['method']); } } ================================================ FILE: tests/DataCollector/JobsCollectorTest.php ================================================ set('debugbar.collectors.jobs', true); // The `sync` and `null` driver don't dispatch events // `database` or `redis` driver work great $app['config']->set('queue.default', 'database'); parent::getEnvironmentSetUp($app); } public function testItCollectsDispatchedJobs() { $this->loadLaravelMigrations(); $this->createJobsTable(); debugbar()->boot(); /** @var \DebugBar\DataCollector\ObjectCountCollector $collector */ $collector = debugbar()->getCollector('jobs'); $collector->setXdebugLinkTemplate(''); $collector->setKeyMap([]); $data = []; static::assertEquals( [ 'data' => $data, 'count' => 0, 'is_counter' => true, 'key_map' => [], ], $collector->collect(), ); OrderShipped::dispatch(1); $data[OrderShipped::class] = ['value' => 1]; static::assertEquals( [ 'data' => $data, 'count' => 1, 'is_counter' => true, 'key_map' => [], ], $collector->collect(), ); dispatch(new SendNotification()); dispatch(new SendNotification()); dispatch(new SendNotification()); $data[SendNotification::class] = ['value' => 3]; static::assertEquals( [ 'data' => $data, 'count' => 4, 'is_counter' => true, 'key_map' => [], ], $collector->collect(), ); } protected function createJobsTable() { (new class extends Migration { public function up() { if (Schema::hasTable('jobs')) { return; } Schema::create('jobs', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('queue')->index(); $table->longText('payload'); $table->unsignedTinyInteger('attempts'); $table->unsignedInteger('reserved_at')->nullable(); $table->unsignedInteger('available_at'); $table->unsignedInteger('created_at'); }); } })->up(); } } ================================================ FILE: tests/DataCollector/Livewire/DummyComponent.php ================================================ counter++; } public function render() { return <<<'blade'
Hello. You are #{{ $counter }}!
Increase
blade; } } ================================================ FILE: tests/DataCollector/LivewireCollectorTest.php ================================================ boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\GateCollector $collector */ $collector = debugbar()->getCollector('livewire'); static::assertInstanceOf(LivewireCollector::class, $collector); if (version_compare(InstalledVersions::getVersion('livewire/livewire'), '3.0', '<')) { $component = new DummyComponent('123'); $view = view('dashboard', ['_instance' => $component]); $collector->addLivewire2View($view, request()); } else { $component = new DummyComponent(); $component->setId('123'); $component->setName('fruitcake.laravel-debugbar.tests.data-collector.livewire.dummy-component'); $collector->addLivewireComponent($component, request()); } $data = $collector->collect(); static::assertEquals('Fruitcake\LaravelDebugbar\Tests\DataCollector\Livewire\DummyComponent fruitcake.laravel-debugbar.tests.data-collector.livewire.dummy-component #123', $data['templates'][0]['name']); static::assertStringContainsString('MyComponent', $data['templates'][0]['params']['title']); } public function testItCollectsAnonymousLivewireComponents() { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\GateCollector $collector */ $collector = debugbar()->getCollector('livewire'); static::assertInstanceOf(LivewireCollector::class, $collector); $component = new class extends Component { public $title = 'MyComponent'; }; if (version_compare(InstalledVersions::getVersion('livewire/livewire'), '3.0', '<')) { $component->id = '123'; $view = view('dashboard', ['_instance' => $component]); $collector->addLivewire2View($view, request()); } else { $component->setId('123'); $component->setName('fruitcake.laravel-debugbar.tests.data-collector.livewire.dummy-component'); $collector->addLivewireComponent($component, request()); } $data = $collector->collect(); if (version_compare(InstalledVersions::getVersion('livewire/livewire'), '3.0', '<')) { static::assertStringContainsString('livewire.component@anonymous.', $data['templates'][0]['name']); static::assertStringContainsString('tests.data-collector.livewire-collector-test.php:', $data['templates'][0]['name']); static::assertStringContainsString(' #123', $data['templates'][0]['name']); } else { static::assertEquals('fruitcake.laravel-debugbar.tests.data-collector.livewire.dummy-component #123', $data['templates'][0]['name']); } static::assertStringContainsString('MyComponent', $data['templates'][0]['params']['title']); } } ================================================ FILE: tests/DataCollector/MailCollectorTest.php ================================================ boot(); /** @var SymfonyMailCollector $collector */ $collector = debugbar()->getCollector('symfonymailer_mails'); Mail::raw('Test body content', function ($message) { $message->to('recipient@example.com') ->subject('Test Subject'); }); $data = $collector->collect(); static::assertEquals(1, $data['count']); static::assertCount(1, $data['mails']); static::assertEquals('Test Subject', $data['mails'][0]['subject']); static::assertContains('recipient@example.com', $data['mails'][0]['to']); } public function testItCollectsMultipleMails(): void { debugbar()->boot(); /** @var SymfonyMailCollector $collector */ $collector = debugbar()->getCollector('symfonymailer_mails'); Mail::raw('First mail', function ($message) { $message->to('first@example.com') ->subject('First Subject'); }); Mail::raw('Second mail', function ($message) { $message->to('second@example.com') ->subject('Second Subject'); }); $data = $collector->collect(); static::assertEquals(2, $data['count']); static::assertCount(2, $data['mails']); static::assertEquals('First Subject', $data['mails'][0]['subject']); static::assertEquals('Second Subject', $data['mails'][1]['subject']); } public function testItAddsMailsToTimelineCollector(): void { debugbar()->boot(); /** @var TimeDataCollector $timeCollector */ $timeCollector = debugbar()->getTimeCollector(); Mail::raw('Timeline test body', function ($message) { $message->to('timeline@example.com') ->subject('Timeline Test'); }); $data = $timeCollector->collect(); $mailMeasures = array_filter($data['measures'], function ($measure) { return str_starts_with($measure['label'], 'Mail: '); }); static::assertNotEmpty($mailMeasures, 'Expected a mail measure in the timeline'); $mailMeasure = reset($mailMeasures); static::assertEquals('Mail: Timeline Test', $mailMeasure['label']); static::assertGreaterThan(0, $mailMeasure['duration']); } public function testItDoesNotAddToTimelineWhenDisabled(): void { $this->app['config']->set('debugbar.options.mail.timeline', false); debugbar()->boot(); /** @var TimeDataCollector $timeCollector */ $timeCollector = debugbar()->getTimeCollector(); Mail::raw('No timeline body', function ($message) { $message->to('notimeline@example.com') ->subject('No Timeline Test'); }); $data = $timeCollector->collect(); $mailMeasures = array_filter($data['measures'], function ($measure) { return str_starts_with($measure['label'], 'Mail: '); }); static::assertEmpty($mailMeasures, 'Expected no mail measures in the timeline'); } public function testItCollectsMailBody(): void { debugbar()->boot(); /** @var SymfonyMailCollector $collector */ $collector = debugbar()->getCollector('symfonymailer_mails'); Mail::raw('This is the plain text body', function ($message) { $message->to('body@example.com') ->subject('Body Test'); }); $data = $collector->collect(); static::assertEquals(1, $data['count']); static::assertStringContainsString('This is the plain text body', $data['mails'][0]['body']); } public function testItHidesMailBodyWhenDisabled(): void { $this->app['config']->set('debugbar.options.mail.show_body', false); debugbar()->boot(); /** @var SymfonyMailCollector $collector */ $collector = debugbar()->getCollector('symfonymailer_mails'); Mail::raw('Hidden body content', function ($message) { $message->to('nobody@example.com') ->subject('Hidden Body Test'); }); $data = $collector->collect(); static::assertEquals(1, $data['count']); static::assertNull($data['mails'][0]['body']); static::assertNull($data['mails'][0]['html']); } } ================================================ FILE: tests/DataCollector/ModelsCollectorTest.php ================================================ loadLaravelMigrations(); debugbar()->boot(); /** @var \DebugBar\DataCollector\ObjectCountCollector $collector */ $collector = debugbar()->getCollector('models'); $collector->setXdebugLinkTemplate(''); $collector->collectCountSummary(false); $collector->setKeyMap([]); $data = []; static::assertEquals( ['data' => $data, 'key_map' => [], 'count' => 0, 'is_counter' => true], $collector->collect(), ); User::create([ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => Hash::make('password'), ]); User::create([ 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'password' => Hash::make('password'), ]); $data[User::class] = ['created' => 2]; static::assertEquals( [ 'data' => $data, 'count' => 2, 'is_counter' => true, 'key_map' => [ ], ], $collector->collect(), ); $user = User::first(); $data[User::class]['retrieved'] = 1; static::assertEquals( ['data' => $data, 'key_map' => [], 'count' => 3, 'is_counter' => true], $collector->collect(), ); $user->update(['name' => 'Jane Doe']); $data[User::class]['updated'] = 1; static::assertEquals( [ 'data' => $data, 'count' => 4, 'is_counter' => true, 'key_map' => [], ], $collector->collect(), ); Person::all(); $data[Person::class] = ['retrieved' => 2]; static::assertEquals( ['data' => $data, 'key_map' => [], 'count' => 6, 'is_counter' => true], $collector->collect(), ); $user->delete(); $data[User::class]['deleted'] = 1; static::assertEquals( [ 'data' => $data, 'count' => 7, 'is_counter' => true, 'key_map' => [ ], ], $collector->collect(), ); } } ================================================ FILE: tests/DataCollector/PennantCollectorTest.php ================================================ loadMigrationsFrom( dirname($reflection->getFileName()) . '/../database/migrations' ); } public function testItCollectsPennantValues() { debugbar()->boot(); Feature::define('new-api', true); Feature::define('old-api', fn() => false); Feature::define('api-version', fn() => '3.x'); /** @var \Fruitcake\LaravelDebugbar\DataCollector\GateCollector $collector */ $collector = debugbar()->getCollector('pennant'); static::assertInstanceOf(PennantCollector::class, $collector); $data = $collector->collect(); static::assertCount(3, $data); static::assertTrue($data['new-api']); static::assertFalse($data['old-api']); static::assertEquals('3.x', $data['api-version']); } } ================================================ FILE: tests/DataCollector/QueryCollectorRuntimeDatabaseTest.php ================================================ set('database.default', null); $app['config']->set('database.connections', []); $app['config']->set('debugbar.options.db.explain.enabled', false); } public function testCollectsQueriesFromRuntimeConnections() { if (version_compare($this->app->version(), '10', '<')) { static::markTestSkipped('This test is not compatible with Laravel 9.x and below'); } debugbar()->enable(); /** @var Connection $connection */ $connection = $this->app['db']->connectUsing( 'runtime-connection', [ 'driver' => 'sqlite', 'database' => ':memory:', ], ); $connection->statement('SELECT 1'); /** @var \Debugbar\DataCollector\ExceptionsCollector $collector */ $exceptions = debugbar()->getCollector('exceptions'); static::assertEmpty($exceptions->getExceptions()); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); tap($collector->collect(), function (array $collection) { $this->assertEquals(1, $collection['nb_statements']); self::assertSame('SELECT 1', $collection['statements'][1]['sql']); }); } public function testCollectsQueriesFromRuntimeConnectionsWithoutConnectUsing() { debugbar()->enable(); $this->app['config']->set('database.connections.dynamic-connection', [ 'driver' => 'sqlite', 'database' => ':memory:', ]); $this->app['config']->set('database.default', 'dynamic-connection'); /** @var Connection $connection */ $connection = $this->app['db']->connection('dynamic-connection'); $connection->statement('SELECT 1'); /** @var \Debugbar\DataCollector\ExceptionsCollector $collector */ $exceptions = debugbar()->getCollector('exceptions'); static::assertEmpty($exceptions->getExceptions()); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); tap($collector->collect(), function (array $collection) { $this->assertEquals(1, $collection['nb_statements']); self::assertSame('SELECT 1', $collection['statements'][1]['sql']); }); } } ================================================ FILE: tests/DataCollector/QueryCollectorTest.php ================================================ loadLaravelMigrations(); debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->addQuery(new QueryExecuted( "SELECT ('[1, 2, 3]'::jsonb ?? ?) as a, ('[4, 5, 6]'::jsonb ??| ?) as b, 'hello world ? example ??' as c", [3, '{4}'], 0, $this->app['db']->connection(), )); tap($collector->collect(), function (array $collection) { $this->assertEquals(1, $collection['nb_statements']); tap(Arr::first($collection['statements']), function (array $statement) { $this->assertEquals([3, '{4}'], $statement['params']); $this->assertEquals(<<boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->addQuery(new QueryExecuted( "SELECT a FROM b WHERE c = ? AND d = ? AND e = ?", ['$10', '$2y$10_DUMMY_BCRYPT_HASH', '$_$$_$$$_$2_$3'], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertEquals( "SELECT a FROM b WHERE c = '$10' AND d = '$2y$10_DUMMY_BCRYPT_HASH' AND e = '\$_$\$_$$\$_$2_$3'", $statement['sql'], ); }); } public function testResultModeForSelectQuery(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setShowQueryResult(true); $collector->addQuery(new QueryExecuted( 'SELECT * FROM users WHERE id = ?', [1], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNotNull($statement['explain']); $this->assertContains('result', $statement['explain']['modes']); }); } public function testResultModeForWithQuery(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setShowQueryResult(true); $collector->addQuery(new QueryExecuted( 'WITH cte AS (SELECT 1) SELECT * FROM cte', [], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNotNull($statement['explain']); $this->assertContains('result', $statement['explain']['modes']); }); } public function testResultModeExcludedForNonSelectQuery(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setShowQueryResult(true); $collector->addQuery(new QueryExecuted( 'INSERT INTO users (name) VALUES (?)', ['test'], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNull($statement['explain']); }); } public function testResultModeExcludedWhenDisabled(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setShowQueryResult(false); $collector->addQuery(new QueryExecuted( 'SELECT * FROM users', [], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNull($statement['explain']); }); } public function testExplainModeExcludedForSqlite(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setExplainQuery(true); $collector->addQuery(new QueryExecuted( 'SELECT * FROM users', [], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNull($statement['explain']); }); } public function testExplainModeExcludedWhenBindingsNull(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setExplainQuery(true); $collector->setLimits(0, null); $collector->addQuery(new QueryExecuted( 'SELECT * FROM users WHERE id = ?', [1], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNull($statement['explain']); }); } public function testExplainModeExcludedForNonSelectQuery(): void { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); $collector->setExplainQuery(true); $collector->addQuery(new QueryExecuted( 'UPDATE users SET name = ?', ['test'], 0, $this->app['db']->connection(), )); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertNull($statement['explain']); }); } public function testFindingCorrectPathForView() { debugbar()->boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\QueryCollector $collector */ $collector = debugbar()->getCollector('queries'); view('query') ->with('db', $this->app['db']->connection()) ->with('collector', $collector) ->render(); tap(Arr::first($collector->collect()['statements']), function (array $statement) { $this->assertEquals( "SELECT a FROM b WHERE c = '$10' AND d = '$2y$10_DUMMY_BCRYPT_HASH' AND e = '\$_$\$_$$\$_$2_$3'", $statement['sql'], ); $this->assertTrue(@file_exists($statement['backtrace'][1]->file)); $this->assertEquals( realpath(__DIR__ . '/../resources/views/query.blade.php'), realpath($statement['backtrace'][1]->file), ); }); } } ================================================ FILE: tests/DataCollector/RouteCollectorTest.php ================================================ boot(); $this->routeCollector = debugbar()->getCollector('route'); } protected function getEnvironmentSetUp($app) { $app['config']->set('debugbar.collectors.route', true); parent::getEnvironmentSetUp($app); } public function testItCollectsRouteUri() { $this->get('web/html'); static::assertSame('GET web/html', $this->routeCollector->collect()['uri']); $this->call('POST', 'web/mw'); static::assertSame('POST web/mw', $this->routeCollector->collect()['uri']); } /** * @dataProvider controllerData */ public function testItCollectsWithControllerHandler($controller, $file, $url) { $this->get('web/show'); $collected = $this->routeCollector->collect(); static::assertNotEmpty($collected); static::assertArrayHasKey('file', $collected); static::assertArrayHasKey('controller', $collected); static::assertStringContainsString($file, $collected['file']['value']); static::assertStringContainsString($url, $collected['file']['xdebug_link']['url']); static::assertStringContainsString($controller, $collected['controller']['value']); static::assertStringContainsString($url, $collected['controller']['xdebug_link']['url']); } /** * @dataProvider viewComponentData */ public function testItCollectsWithViewComponentHandler($controller, $file, $url) { $this->get('web/view'); $collected = $this->routeCollector->collect(); static::assertStringContainsString($file, $collected['file']['value']); static::assertStringContainsString($url, $collected['file']['xdebug_link']['url']); static::assertStringContainsString($controller, $collected['controller']['value']); static::assertStringContainsString($url, $collected['controller']['xdebug_link']['url']); } /** * @dataProvider closureData */ public function testItCollectsWithClosureHandler($file) { RouteCollector::setDefaultDataFormatter(new HtmlDataFormatter()); $this->get('web/html'); $collected = $this->routeCollector->collect(); static::assertNotEmpty($collected); static::assertArrayHasKey('uses', $collected); static::assertArrayHasKey('file', $collected); static::assertStringContainsString('Closure', $collected['uses']); static::assertStringContainsString($file, $collected['file']['value']); } public function testItCollectsMiddleware() { $this->call('POST', 'web/mw'); $collected = $this->routeCollector->collect(); static::assertNotEmpty($collected); static::assertArrayHasKey('middleware', $collected); static::assertStringContainsString('MockMiddleware', $collected['middleware']); } public static function controllerData() { $filePath = urlencode(str_replace('\\', '/', realpath(__DIR__ . '/../Mocks/MockController.php'))); return [['MockController@show', 'MockController.php', sprintf('phpstorm://open?file=%s', $filePath), ]]; } public static function viewComponentData() { $filePath = urlencode(str_replace('\\', '/', realpath(__DIR__ . '/../Mocks/MockViewComponent.php'))); return [['MockViewComponent@render', 'MockViewComponent.php', sprintf('phpstorm://open?file=%s', $filePath), ]]; } public static function closureData() { return [['TestCase.php']]; } } ================================================ FILE: tests/DataCollector/SessionCollectorTest.php ================================================ setDataFormatter(new DataFormatter()); static::assertEmpty($collector->collect()); $this->withSession(['testVariable' => "1", 'secret' => 'testSecret'])->get('/'); $collected = $collector->collect(); static::assertNotEmpty($collected); static::assertArrayHasKey('secret', $collected); static::assertArrayHasKey('testVariable', $collected); static::assertEquals('te***et', $collected['secret']); static::assertEquals("1", $collected['testVariable']); $this->flushSession(); static::assertCount(0, $collector->collect()); } } ================================================ FILE: tests/DataCollector/ViewCollectorTest.php ================================================ boot(); /** @var \Fruitcake\LaravelDebugbar\DataCollector\ViewCollector $collector */ $collector = debugbar()->getCollector('views'); $collector->addView( view('dashboard'), ); tap(Arr::first($collector->collect()['templates']), function (array $template) { $this->assertEquals( 'phpstorm://open?file=' . urlencode(str_replace('\\', '/', realpath(__DIR__ . '/../resources/views/dashboard.blade.php'))) . '&line=1', $template['xdebug_link']['url'], ); }); } } ================================================ FILE: tests/DataFormatter/QueryFormatterTest.php ================================================ checkBindings($bindings); static::assertSame($output, ["some string", "[string,Another ' string,[nested,array]]"]); } public function testItFormatsObjectBindings() { $object = new \StdClass(); $object->attribute1 = 'test'; $bindings = [ 'some string', $object, ]; $queryFormatter = new QueryFormatter(); $output = $queryFormatter->checkBindings($bindings); static::assertSame($output, ['some string', '{"attribute1":"test"}']); } } ================================================ FILE: tests/DebugbarBrowserTest.php ================================================ set('app.debug', true); $app['config']->set('debugbar.hide_empty_tabs', false); /** @var Router $router */ $router = $app['router']; $this->addWebRoutes($router); $this->addApiRoutes($router); $this->addViewPaths(); $kernel = app(\Illuminate\Contracts\Http\Kernel::class); $kernel->pushMiddleware(\Illuminate\Session\Middleware\StartSession::class); $kernel->pushMiddleware(\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class); \Orchestra\Testbench\Dusk\Options::withoutUI(); } protected function addWebRoutes(Router $router) { $router->get('web/redirect', [ 'uses' => function () { return redirect($this->applicationBaseUrl() . '/web/plain'); }, ]); $router->get('web/plain', [ 'uses' => function () { return 'PONG'; }, ]); $router->get('web/html', [ 'uses' => function () { return 'HTMLPONG'; }, ]); $router->get('web/ajax', [ 'uses' => function () { return view('ajax'); }, ]); $router->get('web/custom-prototype', [ 'uses' => function () { /** @var Connection $connection */ $connection = $this->app['db']->connectUsing( 'runtime-connection', [ 'driver' => 'sqlite', 'database' => ':memory:', ], ); event(new QueryExecuted('SELECT * FROM users WHERE username = ?', ['debuguser'], 0, $connection)); return view('custom-prototype'); }, ]); $router->get('web/query/{num?}', [ 'uses' => function ($num = 1) { debugbar()->boot(); /** @var Connection $connection */ $connection = $this->app['db']->connectUsing( 'runtime-connection', [ 'driver' => 'sqlite', 'database' => ':memory:', ], ); foreach (range(1, $num) as $i) { $executedQuery = new QueryExecuted('SELECT * FROM users WHERE username = ?', ['debuguser' . $i], 0, $connection); event($executedQuery); } return 'PONG'; }, ]); } protected function addApiRoutes(Router $router) { $router->get('api/ping', [ 'uses' => function () { return response()->json(['status' => 'pong']); }, ]); } protected function addViewPaths() { config(['view.paths' => array_merge(config('view.paths'), [__DIR__ . '/resources/views'])]); } public function testItStacksOnRedirect() { $this->browse(function (Browser $browser) { $browser->visit('web/redirect') ->assertSee('PONG') ->waitFor('.phpdebugbar') ->assertSee('GET /web/plain') ->click('.phpdebugbar-widgets-datasets-switcher-widget') ->waitForTextIn('.phpdebugbar-widgets-datasets-list', 'web/redirect') ->assertSee('(stacked)') ->assertSee('web/redirect'); }); } public function testItInjectsOnPlainText() { $this->browse(function ($browser) { $browser->visit('web/plain') ->assertSee('PONG') ->waitFor('.phpdebugbar') ->assertSee('GET /web/plain'); }); } public function testItInjectsOnHtml() { $this->browse(function ($browser) { $browser->visit('web/html') ->assertSee('HTMLPONG') ->waitFor('.phpdebugbar') ->assertSee('GET /web/html'); }); } public function testItDoesntInjectOnJson() { $this->browse(function ($browser) { $browser->visit('api/ping') ->assertSee('pong') ->assertSourceMissing('debugbar') ->assertDontSee('GET /api/ping'); }); } public function testItCapturesAjaxRequests() { $this->browse(function (Browser $browser) { $browser->visit('web/ajax') ->waitFor('.phpdebugbar') ->assertSee('GET /web/ajax') ->click('#ajax-link') ->waitForTextIn('#result', 'pong') ->assertSee('GET /api/ping'); }); } public function testDatabaseTabIsClickable() { $this->browse(function (Browser $browser) { $browser->visit('web/plain') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->assertDontSee('0 statements were executed') ->click('.phpdebugbar-tab[data-collector="queries"]') ->assertSee('0 statements were executed'); }); } public function testDatabaseCollectsQueries() { $this->browse(function (Browser $browser) { $browser->visit('web/query') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->waitForTextIn('.phpdebugbar-tab[data-collector="queries"] .phpdebugbar-badge', 1) ->click('.phpdebugbar-tab[data-collector="queries"]') ->screenshotElement('.phpdebugbar', 'queries-tab') ->waitForText('executed') ->waitForText('1 statements were executed') ->with('.phpdebugbar-widgets-sqlqueries', function ($queriesPane) { $queriesPane->assertSee('SELECT * FROM users') ->click('.phpdebugbar-widgets-list-item:nth-child(2)') ->assertSee('Params') ->assertSee('debuguser') ->assertSee('Backtrace') ->assertSee('DatabaseCollectorProvider.php:'); }) ->screenshotElement('.phpdebugbar', 'queries-expanded'); }); } public function testDatabaseCollectsQueriesWithCustomPrototype() { if (version_compare($this->app->version(), '10', '<')) { static::markTestSkipped('This test is not compatible with Laravel 9.x and below'); } $this->browse(function (Browser $browser) { $browser->visit('web/custom-prototype') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->waitForTextIn('.phpdebugbar-tab[data-collector="queries"] .phpdebugbar-badge', 1) ->click('.phpdebugbar-tab[data-collector="queries"]') ->screenshotElement('.phpdebugbar', 'queries-tab') ->waitForText('executed') ->assertSee('1 statements were executed') ->with('.phpdebugbar-widgets-sqlqueries', function ($queriesPane) { $queriesPane->assertSee('SELECT * FROM users') ->click('.phpdebugbar-widgets-list-item:nth-child(2)') ->assertSee('Params') ->assertSee('debuguser') ->assertSee('Backtrace') ->assertSee('DatabaseCollectorProvider.php:'); }) ->screenshotElement('.phpdebugbar', 'queries-expanded'); }); } public function testDatabaseCollectsQueriesWithSoftLimit() { $this->browse(function (Browser $browser) { $browser->visit('web/query/200') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->waitForTextIn('.phpdebugbar-tab[data-collector="queries"] .phpdebugbar-badge', 200, 30) ->click('.phpdebugbar-tab[data-collector="queries"]') ->screenshotElement('.phpdebugbar', 'queries-tab') ->waitForText('executed') ->waitForText('200 statements were executed, 100 of which were duplicates, 100 unique.') ->waitForText('Query soft limit for Debugbar is reached after 100 queries, additional 100 queries only show the query.') ->screenshotElement('.phpdebugbar', 'queries-expanded'); }); } public function testDatabaseCollectsQueriesWithHardLimit() { $this->browse(function (Browser $browser) { $browser->visit('web/query/600') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->waitForTextIn('.phpdebugbar-tab[data-collector="queries"] .phpdebugbar-badge', 600) ->click('.phpdebugbar-tab[data-collector="queries"]') ->screenshotElement('.phpdebugbar', 'queries-tab') ->waitForText('executed') ->waitForText('600 statements were executed, 100 have been excluded, 400 of which were duplicates, 200 unique.') ->waitForText('Query soft and hard limit for Debugbar are reached. Only the first 100 queries show details. Queries after the first 500 are ignored. ') ->screenshotElement('.phpdebugbar', 'queries-expanded'); }); } } ================================================ FILE: tests/DebugbarDocsTest.php ================================================ resolving(LaravelDebugbar::class, function ($debugbar) { $refObject = new \ReflectionObject($debugbar); $refProperty = $refObject->getProperty('enabled'); $refProperty->setValue($debugbar, true); }); } public function testItInjectsOnDocs() { /** @var Router $router */ $router = $this->app['router']; $this->app['config']->set('debugbar.hide_empty_tabs', true); $this->loadLaravelMigrations(); Http::fake([ 'packagist.org/*' => Http::response([ 'downloads' => [ 'total' => 117241469, 'monthly' => 2006302, 'daily' => 109736, ], ], 200, []), ]); $router->get('docs', function () { debugbar()->addMessage('Hello Artisans!'); debugbar()->warning('Watch out for ..'); debugbar()->error('Bugs!'); User::create(['email' => 'demo@example.com', 'name' => 'Barry', 'password' => bcrypt('secret')]); User::count(); User::where('name', 'Barry')->first(); User::where('id', 1)->get(); User::where('id', 1)->get(); User::where('id', 1)->get(); view('dashboard')->render(); Http::get('https://packagist.org/packages/barryvdh/laravel-debugbar/stats.json')->json(); debugbar()->addException(new \RuntimeException('Whoops! This is just a demo')); return ''; }); $crawler = $this->call('GET', 'docs'); static::assertTrue(Str::contains($crawler->content(), 'debugbar')); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); static::assertEquals(200, $crawler->getStatusCode()); @mkdir(__DIR__ . '/../build/docs/assets', 0o777, true); // Store output for test file_put_contents(__DIR__ . '/../build/docs/render.html', $crawler->getContent()); $css = $this->call('GET', '/_debugbar/assets?type=css'); static::assertEquals(200, $css->getStatusCode()); static::assertNotEmpty($css->getContent()); file_put_contents(__DIR__ . '/../build/docs/assets/debugbar.css', $css->getContent()); $js = $this->call('GET', '/_debugbar/assets?type=js'); static::assertEquals(200, $js->getStatusCode()); static::assertNotEmpty($js->getContent()); file_put_contents(__DIR__ . '/../build/docs/assets/debugbar.js', $this->call('GET', '/_debugbar/assets?type=js')->getContent()); } } ================================================ FILE: tests/DebugbarTest.php ================================================ resolving(LaravelDebugbar::class, function ($debugbar) { $refObject = new \ReflectionObject($debugbar); $refProperty = $refObject->getProperty('enabled'); $refProperty->setValue($debugbar, true); }); } public function testItInjectsOnPlainText() { $crawler = $this->call('GET', 'web/plain'); static::assertTrue(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItInjectsOnEmptyResponse() { $crawler = $this->call('GET', 'web/empty'); static::assertTrue(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItInjectsOnNullyResponse() { $crawler = $this->call('GET', 'web/null'); static::assertTrue(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItInjectsOnHtml() { $crawler = $this->call('GET', 'web/html'); static::assertTrue(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItDoesntInjectOnJson() { $crawler = $this->call('GET', 'api/ping'); static::assertFalse(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItDoesntInjectOnJsonLookingString() { $crawler = $this->call('GET', 'web/fakejson'); static::assertFalse(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItDoesntInjectsOnHxRequestWithHxTarget() { $crawler = $this->get('web/html', [ 'Hx-Request' => 'true', 'Hx-Target' => 'main', ]); static::assertFalse(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } public function testItInjectsOnHxRequestWithoutHxTarget() { $crawler = $this->get('web/html', [ 'Hx-Request' => 'true', ]); static::assertTrue(Str::contains($crawler->content(), 'debugbar')); static::assertEquals(200, $crawler->getStatusCode()); static::assertNotEmpty($crawler->headers->get('phpdebugbar-id')); } } ================================================ FILE: tests/ErrorHandlerTest.php ================================================ resolving(LaravelDebugbar::class, function ($debugbar) { $refObject = new ReflectionObject($debugbar); $refProperty = $refObject->getProperty('enabled'); $refProperty->setValue($debugbar, true); }); // Enable collectors needed for error handling $app['config']->set('debugbar.collectors.messages', true); $app['config']->set('debugbar.collectors.exceptions', true); $app['config']->set('debugbar.error_handler', true); // Exclude deprecation warnings $app['config']->set('debugbar.error_level', E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED); } public function testErrorHandlerRespectsCustomErrorLevel() { $app = $this->app; $debugbar = $app->make(LaravelDebugbar::class); $debugbar->boot(); // Get initial message count $initialCount = 0; if ($debugbar->hasCollector('messages')) { $initialCount = count($debugbar->getCollector('messages')->collect()['messages']); } // Trigger a deprecation warning - should NOT be captured @trigger_error('Test deprecation warning', E_USER_DEPRECATED); // Check that error was NOT captured if ($debugbar->hasCollector('messages')) { $messages = $debugbar->getCollector('messages')->collect(); $newCount = count($messages['messages']); static::assertEquals($initialCount, $newCount, 'Deprecation warning should not be captured when excluded from error_level'); } // Trigger a warning (not a deprecation) - should be captured @trigger_error('Test warning', E_USER_WARNING); // Check that warning WAS captured if ($debugbar->hasCollector('messages')) { $messages = $debugbar->getCollector('messages')->collect(); $finalCount = count($messages['messages']); static::assertGreaterThan($initialCount, $finalCount, 'Non-deprecation errors should still be captured'); } } } ================================================ FILE: tests/Jobs/OrderShipped.php ================================================ orderId = $orderId; } public function handle() { // Do Nothing } } ================================================ FILE: tests/Jobs/SendNotification.php ================================================ set('app.debug', true); $app['config']->set('debugbar.hide_empty_tabs', false); config(['view.paths' => array_merge(config('view.paths'), [__DIR__ . '/resources/views'])]); // Set app layout config([ 'livewire.layout' => 'layouts.app', // Livewire 3 'livewire.component_layout' => 'layouts.app', // Livewire 4 ]); /** @var Router $router */ $router = $app['router']; // Register Component Livewire::component('dummy-component', DummyComponent::class); $router->get('web/livewire-component', [ 'uses' => DummyComponent::class, ]); $router->get('web/livewire-view', function () { return view('livewire-component'); }); $kernel = app(\Illuminate\Contracts\Http\Kernel::class); $kernel->pushMiddleware(\Illuminate\Session\Middleware\StartSession::class); $kernel->pushMiddleware(\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class); \Orchestra\Testbench\Dusk\Options::withoutUI(); } protected function getPackageProviders($app) { return [ServiceProvider::class, LivewireServiceProvider::class]; } public function testLivewireCollectsComponents() { $this->browse(function (Browser $browser) { $browser->visit('web/livewire-component') ->waitFor('[wire\\:id]') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->waitForTextIn('.phpdebugbar-tab[data-collector="livewire"] .phpdebugbar-badge', 1) ->click('.phpdebugbar-tab[data-collector="livewire"]') ->assertSee('1 Livewire component') ->assertSee('You are #1') ->with('.phpdebugbar-widgets-list-item', function ($queriesPane) { $queriesPane->assertSee('DummyComponent') ->click('.phpdebugbar-widgets-name') ->assertSee('Params') ->assertSee('title') ->assertSee('MyComponent'); }) ->click('.phpdebugbar-tab[data-collector="request"]') ->waitForText('Tests\DataCollector\Livewire\DummyComponent', 3) ->clickLink('Increase') ->waitForText('You are #2', 30); }); } public function testLivewireCollectsView() { $this->browse(function (Browser $browser) { $browser->visit('web/livewire-view') ->waitFor('.phpdebugbar') ->click('.phpdebugbar-tab-settings') ->waitForTextIn('.phpdebugbar-tab[data-collector="livewire"] .phpdebugbar-badge', 1) ->click('.phpdebugbar-tab[data-collector="livewire"]') ->assertSee('1 Livewire component') ->assertSee('You are #1') ->with('.phpdebugbar-widgets-list-item', function ($queriesPane) { $queriesPane->assertSee('DummyComponent') ->click('.phpdebugbar-widgets-name') ->assertSee('Params') ->assertSee('title') ->assertSee('MyComponent'); }) ->click('.phpdebugbar-tab[data-collector="request"]') ->clickLink('Increase') ->waitForText('You are #2', 3) ->assertSee('Tests\DataCollector\Livewire\DummyComponent'); }); } } ================================================ FILE: tests/Mocks/MockController.php ================================================ explain = new Explain(); } public function testSelectQueryIsReadOnly(): void { static::assertTrue($this->explain->isReadOnlyQuery('SELECT * FROM users')); } public function testSelectQueryWithLeadingWhitespaceIsReadOnly(): void { static::assertTrue($this->explain->isReadOnlyQuery(' SELECT * FROM users')); } public function testSelectQueryCaseInsensitiveIsReadOnly(): void { static::assertTrue($this->explain->isReadOnlyQuery('select * FROM users')); } public function testWithQueryIsReadOnly(): void { static::assertTrue($this->explain->isReadOnlyQuery('WITH cte AS (SELECT 1) SELECT * FROM cte')); } public function testWithQueryCaseInsensitiveIsReadOnly(): void { static::assertTrue($this->explain->isReadOnlyQuery('with cte AS (SELECT 1) SELECT * FROM cte')); } public function testInsertQueryIsNotReadOnly(): void { static::assertFalse($this->explain->isReadOnlyQuery('INSERT INTO users (name) VALUES (?)')); } public function testUpdateQueryIsNotReadOnly(): void { static::assertFalse($this->explain->isReadOnlyQuery('UPDATE users SET name = ? WHERE id = ?')); } public function testDeleteQueryIsNotReadOnly(): void { static::assertFalse($this->explain->isReadOnlyQuery('DELETE FROM users WHERE id = ?')); } public function testDropQueryIsNotReadOnly(): void { static::assertFalse($this->explain->isReadOnlyQuery('DROP TABLE users')); } public function testSelectAsSubstringIsNotReadOnly(): void { static::assertFalse($this->explain->isReadOnlyQuery('SELECTFOO')); } public function testWithAsSubstringIsNotReadOnly(): void { static::assertFalse($this->explain->isReadOnlyQuery('WITHFOO')); } public function testRawExplainSupportedForMysql(): void { static::assertTrue($this->explain->isRawExplainSupported('mysql', [])); } public function testRawExplainSupportedForMariadb(): void { static::assertTrue($this->explain->isRawExplainSupported('mariadb', [])); } public function testRawExplainSupportedForPgsql(): void { static::assertTrue($this->explain->isRawExplainSupported('pgsql', [])); } public function testRawExplainNotSupportedForSqlite(): void { static::assertFalse($this->explain->isRawExplainSupported('sqlite', [])); } public function testRawExplainNotSupportedForSqlsrv(): void { static::assertFalse($this->explain->isRawExplainSupported('sqlsrv', [])); } public function testRawExplainNotSupportedWhenBindingsNull(): void { static::assertFalse($this->explain->isRawExplainSupported('mysql', null)); } public function testRawExplainSupportedWithEmptyBindings(): void { static::assertTrue($this->explain->isRawExplainSupported('mysql', [])); } } ================================================ FILE: tests/TestCase.php ================================================ Debugbar::class]; } /** * Define environment setup. * * @param \Illuminate\Foundation\Application $app * * @return void */ protected function getEnvironmentSetUp($app) { /** @var Router $router */ $router = $app['router']; $app['config']->set('debugbar.enabled', false); $app['config']->set('debugbar.hide_empty_tabs', false); $app['config']->set('app.debug', true); $app['env'] = 'local'; $this->addWebRoutes($router); $this->addApiRoutes($router); $this->addViewPaths(); } protected function addWebRoutes(Router $router) { $router->get('web/plain', function () { return 'PONG'; }); $router->get('web/empty', function () { return ''; }); $router->get('web/null', function () { return null; }); $router->get('web/html', function () { return 'Pong'; }); $router->get('web/fakejson', function () { return '{"foo":"bar"}'; }); $router->get('web/show', [MockController::class, 'show']); $router->get('web/view', MockViewComponent::class); $router->post('web/mw')->middleware(MockMiddleware::class); } protected function addApiRoutes(Router $router) { $router->get('api/ping', [ 'uses' => function () { return response()->json(['status' => 'pong']); }, ]); } protected function addViewPaths() { config(['view.paths' => array_merge(config('view.paths'), [__DIR__ . '/resources/views'])]); } protected function resetStorageOpen(): void { $debugbar = app(LaravelDebugbar::class); (new ReflectionObject($debugbar)) ->getProperty('storageOpen') ->setValue($debugbar, null); } } ================================================ FILE: tests/resources/views/ajax.blade.php ================================================ Click me
Waiting..
================================================ FILE: tests/resources/views/custom-prototype.blade.php ================================================ ================================================ FILE: tests/resources/views/dashboard.blade.php ================================================

Basic view

================================================ FILE: tests/resources/views/layouts/app.blade.php ================================================ @livewireStyles {{ $slot }} @livewireScripts ================================================ FILE: tests/resources/views/livewire-component.blade.php ================================================ @livewireStyles

Livewire Test

@livewireScripts ================================================ FILE: tests/resources/views/query.blade.php ================================================ @php $collector->addQuery(new \Illuminate\Database\Events\QueryExecuted( "SELECT a FROM b WHERE c = ? AND d = ? AND e = ?", ['$10', '$2y$10_DUMMY_BCRYPT_HASH', '$_$$_$$$_$2_$3'], 0, $db )); @endphp