├── CHANGELOG.md ├── LICENSE ├── SECURITY.md ├── composer.json ├── config └── debugbar.php ├── database └── migrations │ └── 2014_12_01_120000_create_phpdebugbar_storage_table.php ├── phpcs.xml ├── readme.md └── src ├── Console └── ClearCommand.php ├── Controllers ├── AssetController.php ├── BaseController.php ├── CacheController.php ├── OpenHandlerController.php ├── QueriesController.php └── TelescopeController.php ├── DataCollector ├── CacheCollector.php ├── EventCollector.php ├── FilesCollector.php ├── GateCollector.php ├── JobsCollector.php ├── LaravelCollector.php ├── LivewireCollector.php ├── LogsCollector.php ├── ModelsCollector.php ├── MultiAuthCollector.php ├── PennantCollector.php ├── QueryCollector.php ├── RequestCollector.php ├── RouteCollector.php ├── SessionCollector.php └── ViewCollector.php ├── DataFormatter ├── QueryFormatter.php └── SimpleFormatter.php ├── DebugbarViewEngine.php ├── Facade.php ├── Facades └── Debugbar.php ├── JavascriptRenderer.php ├── LaravelDebugbar.php ├── LumenServiceProvider.php ├── Middleware ├── DebugbarEnabled.php └── InjectDebugbar.php ├── Resources ├── cache │ └── widget.js ├── laravel-debugbar.css └── queries │ └── widget.js ├── ServiceProvider.php ├── Storage ├── FilesystemStorage.php └── SocketStorage.php ├── Support ├── Clockwork │ ├── ClockworkCollector.php │ └── Converter.php ├── Explain.php └── RequestIdGenerator.php ├── SymfonyHttpDriver.php ├── Twig └── Extension │ ├── Debug.php │ ├── Dump.php │ └── Stopwatch.php ├── debugbar-routes.php └── helpers.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.15.4 - 2025-04-16 4 | 5 | ### What's Changed 6 | 7 | * Remove html `` tag from route on clockwork by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1777 8 | * Fix default for capturing dd/dump by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1783 9 | 10 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.3...v3.15.4 11 | 12 | ## v3.15.3 - 2025-04-08 13 | 14 | ### What's Changed 15 | 16 | * Add condition for implemented query grammar by @rikwillems in https://github.com/barryvdh/laravel-debugbar/pull/1757 17 | * Collect dumps on message collector by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1759 18 | * Fix `capture_dumps` option on laravel `dd();` by @parallels999 in https://github.com/barryvdh/laravel-debugbar/pull/1762 19 | * Preserve laravel error handler by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1760 20 | * Fix `Trying to access array offset on false on LogsCollector.php` by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1763 21 | * Update css theme for views widget by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1768 22 | * Fix laravel-debugbar.css on query widget by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1765 23 | * Use htmlvardumper if available on CacheCollector by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1766 24 | * Update QueryCollector.php fix issue #1775 by @Mathias-DS in https://github.com/barryvdh/laravel-debugbar/pull/1776 25 | * Better grouping the events count by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1774 26 | 27 | ### New Contributors 28 | 29 | * @rikwillems made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1757 30 | * @Mathias-DS made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1776 31 | 32 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.2...v3.15.3 33 | 34 | ## v3.15.2 - 2025-02-25 35 | 36 | ### What's Changed 37 | 38 | * Fix empty tabs on clockwork by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1750 39 | * fix: Ignore info query statements in Clockwork converter by @boserup in https://github.com/barryvdh/laravel-debugbar/pull/1749 40 | * Check if request controller is string by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1751 41 | 42 | ### New Contributors 43 | 44 | * @boserup made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1749 45 | 46 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.1...v3.15.2 47 | 48 | ## v3.15.1 - 2025-02-24 49 | 50 | ### What's Changed 51 | 52 | * Hide more empty tabs by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1742 53 | * Always show application by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1745 54 | * Add conflict with old debugbar by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1746 55 | 56 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.15.0...v3.15.1 57 | 58 | ## v3.15.0 - 2025-02-21 59 | 60 | ### What's Changed 61 | 62 | * Add middleware to web to save session by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1710 63 | * Check web middleware by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1712 64 | * Add special `dev` to composer keywords by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1713 65 | * Removed extra sentence by @cheack in https://github.com/barryvdh/laravel-debugbar/pull/1714 66 | * Hide empty tabs by default by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1711 67 | * Combine route info with Request by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1720 68 | * fix: The log is not processed correctly when it consists of multiple lines. by @uniho in https://github.com/barryvdh/laravel-debugbar/pull/1721 69 | * [WIP] Use php-debugbar dark theme, move to variables by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1717 70 | * Remove openhandler overrides by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1723 71 | * Drop Lumen And Laravel 9 by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1725 72 | * Use tooltip for Laravel collector by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1724 73 | * Add more data to timeline by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1726 74 | * Laravel version preview as repo branch name by @angeljqv in https://github.com/barryvdh/laravel-debugbar/pull/1727 75 | * Laravel 12 support by @jonnott in https://github.com/barryvdh/laravel-debugbar/pull/1730 76 | * Preview action_name on request tooltip by @angeljqv in https://github.com/barryvdh/laravel-debugbar/pull/1728 77 | * Map tooltips by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1732 78 | * Add back L9 by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1734 79 | * Fix tooltip url by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1735 80 | * Show request status as badge by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1736 81 | * Fix request badge by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1737 82 | * Use Laravel ULID for key by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1738 83 | * defer datasets by config option by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1739 84 | * Reorder request tab by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1740 85 | * Defer config by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1741 86 | 87 | ### New Contributors 88 | 89 | * @cheack made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1714 90 | * @angeljqv made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1727 91 | * @jonnott made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1730 92 | 93 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.10...v3.15.0 94 | 95 | ## v3.14.10 - 2024-12-23 96 | 97 | ### What's Changed 98 | 99 | * Fix Debugbar spelling inconsistencies by @ralphjsmit in https://github.com/barryvdh/laravel-debugbar/pull/1626 100 | * Fix Visual Explain confirm message by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1709 101 | 102 | ### New Contributors 103 | 104 | * @ralphjsmit made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1626 105 | 106 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.9...v3.14.10 107 | 108 | ## v3.14.9 - 2024-11-25 109 | 110 | ### What's Changed 111 | 112 | * Fix custom prototype array by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1706 113 | 114 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.8...v3.14.9 115 | 116 | ## v3.14.8 - 2024-11-25 117 | 118 | ### What's Changed 119 | 120 | * Add fix + failing test for custom array prototype by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1705 121 | 122 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.7...v3.14.8 123 | 124 | ## v3.14.7 - 2024-11-14 125 | 126 | ### What's Changed 127 | 128 | * Make better use of query tab space by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1694 129 | * Do not open query details on text selecting by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1693 130 | * Add (initial) support for PHP 8.4 by @jnoordsij in https://github.com/barryvdh/laravel-debugbar/pull/1631 131 | * More warnings by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1696 132 | * Fix sql-duplicate highlight by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1699 133 | * ci: Use GitHub Actions V4 by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1700 134 | * Fix "Uncaught TypeError: is not iterable" by @erikn69 in https://github.com/barryvdh/laravel-debugbar/pull/1701 135 | * Fix Exception when QueryCollector softLimit exceeded by @johnkary in https://github.com/barryvdh/laravel-debugbar/pull/1702 136 | * Test soft/hard limit queries by @barryvdh in https://github.com/barryvdh/laravel-debugbar/pull/1703 137 | 138 | ### New Contributors 139 | 140 | * @johnkary made their first contribution in https://github.com/barryvdh/laravel-debugbar/pull/1702 141 | 142 | **Full Changelog**: https://github.com/barryvdh/laravel-debugbar/compare/v3.14.6...v3.14.7 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-present Barry vd. Heuvel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report security issues to `barryvdh@gmail.com` 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barryvdh/laravel-debugbar", 3 | "description": "PHP Debugbar integration for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "debugbar", 7 | "profiler", 8 | "debug", 9 | "webprofiler", 10 | "dev" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Barry vd. Heuvel", 16 | "email": "barryvdh@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "php-debugbar/php-debugbar": "~2.2.0", 22 | "illuminate/routing": "^9|^10|^11|^12", 23 | "illuminate/session": "^9|^10|^11|^12", 24 | "illuminate/support": "^9|^10|^11|^12", 25 | "symfony/finder": "^6|^7" 26 | }, 27 | "require-dev": { 28 | "mockery/mockery": "^1.3.3", 29 | "orchestra/testbench-dusk": "^7|^8|^9|^10", 30 | "phpunit/phpunit": "^9.5.10|^10|^11", 31 | "squizlabs/php_codesniffer": "^3.5" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Barryvdh\\Debugbar\\": "src/" 36 | }, 37 | "files": [ 38 | "src/helpers.php" 39 | ] 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Barryvdh\\Debugbar\\Tests\\": "tests" 44 | } 45 | }, 46 | "minimum-stability": "dev", 47 | "prefer-stable": true, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "3.16-dev" 51 | }, 52 | "laravel": { 53 | "providers": [ 54 | "Barryvdh\\Debugbar\\ServiceProvider" 55 | ], 56 | "aliases": { 57 | "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" 58 | } 59 | } 60 | }, 61 | "scripts": { 62 | "check-style": "phpcs", 63 | "fix-style": "phpcbf", 64 | "test": "phpunit" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/debugbar.php: -------------------------------------------------------------------------------- 1 | env('DEBUGBAR_ENABLED', null), 18 | 'hide_empty_tabs' => env('DEBUGBAR_HIDE_EMPTY_TABS', true), // Hide tabs until they have content 19 | 'except' => [ 20 | 'telescope*', 21 | 'horizon*', 22 | ], 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Storage settings 27 | |-------------------------------------------------------------------------- 28 | | 29 | | Debugbar stores data for session/ajax requests. 30 | | You can disable this, so the debugbar stores data in headers/session, 31 | | but this can cause problems with large data collectors. 32 | | By default, file storage (in the storage folder) is used. Redis and PDO 33 | | can also be used. For PDO, run the package migrations first. 34 | | 35 | | Warning: Enabling storage.open will allow everyone to access previous 36 | | request, do not enable open storage in publicly available environments! 37 | | Specify a callback if you want to limit based on IP or authentication. 38 | | Leaving it to null will allow localhost only. 39 | */ 40 | 'storage' => [ 41 | 'enabled' => env('DEBUGBAR_STORAGE_ENABLED', true), 42 | 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. 43 | 'driver' => env('DEBUGBAR_STORAGE_DRIVER', 'file'), // redis, file, pdo, socket, custom 44 | 'path' => env('DEBUGBAR_STORAGE_PATH', storage_path('debugbar')), // For file driver 45 | 'connection' => env('DEBUGBAR_STORAGE_CONNECTION', null), // Leave null for default connection (Redis/PDO) 46 | 'provider' => env('DEBUGBAR_STORAGE_PROVIDER', ''), // Instance of StorageInterface for custom driver 47 | 'hostname' => env('DEBUGBAR_STORAGE_HOSTNAME', '127.0.0.1'), // Hostname to use with the "socket" driver 48 | 'port' => env('DEBUGBAR_STORAGE_PORT', 2304), // Port to use with the "socket" driver 49 | ], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Editor 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Choose your preferred editor to use when clicking file name. 57 | | 58 | | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", 59 | | "vscode-insiders-remote", "vscodium", "textmate", "emacs", 60 | | "sublime", "atom", "nova", "macvim", "idea", "netbeans", 61 | | "xdebug", "espresso" 62 | | 63 | */ 64 | 65 | 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Remote Path Mapping 70 | |-------------------------------------------------------------------------- 71 | | 72 | | If you are using a remote dev server, like Laravel Homestead, Docker, or 73 | | even a remote VPS, it will be necessary to specify your path mapping. 74 | | 75 | | Leaving one, or both of these, empty or null will not trigger the remote 76 | | URL changes and Debugbar will treat your editor links as local files. 77 | | 78 | | "remote_sites_path" is an absolute base path for your sites or projects 79 | | in Homestead, Vagrant, Docker, or another remote development server. 80 | | 81 | | Example value: "/home/vagrant/Code" 82 | | 83 | | "local_sites_path" is an absolute base path for your sites or projects 84 | | on your local computer where your IDE or code editor is running on. 85 | | 86 | | Example values: "/Users//Code", "C:\Users\\Documents\Code" 87 | | 88 | */ 89 | 90 | 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'), 91 | 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')), 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Vendors 96 | |-------------------------------------------------------------------------- 97 | | 98 | | Vendor files are included by default, but can be set to false. 99 | | This can also be set to 'js' or 'css', to only include javascript or css vendor files. 100 | | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) 101 | | and for js: jquery and highlight.js 102 | | So if you want syntax highlighting, set it to true. 103 | | jQuery is set to not conflict with existing jQuery scripts. 104 | | 105 | */ 106 | 107 | 'include_vendors' => env('DEBUGBAR_INCLUDE_VENDORS', true), 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Capture Ajax Requests 112 | |-------------------------------------------------------------------------- 113 | | 114 | | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), 115 | | you can use this option to disable sending the data through the headers. 116 | | 117 | | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. 118 | | 119 | | Note for your request to be identified as ajax requests they must either send the header 120 | | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. 121 | | 122 | | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. 123 | | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. 124 | | 125 | | You can defer loading the dataset, so it will be loaded with ajax after the request is done. (Experimental) 126 | */ 127 | 128 | 'capture_ajax' => env('DEBUGBAR_CAPTURE_AJAX', true), 129 | 'add_ajax_timing' => env('DEBUGBAR_ADD_AJAX_TIMING', false), 130 | 'ajax_handler_auto_show' => env('DEBUGBAR_AJAX_HANDLER_AUTO_SHOW', true), 131 | 'ajax_handler_enable_tab' => env('DEBUGBAR_AJAX_HANDLER_ENABLE_TAB', true), 132 | 'defer_datasets' => env('DEBUGBAR_DEFER_DATASETS', false), 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Custom Error Handler for Deprecated warnings 136 | |-------------------------------------------------------------------------- 137 | | 138 | | When enabled, the Debugbar shows deprecated warnings for Symfony components 139 | | in the Messages tab. 140 | | 141 | */ 142 | 'error_handler' => env('DEBUGBAR_ERROR_HANDLER', false), 143 | 144 | /* 145 | |-------------------------------------------------------------------------- 146 | | Clockwork integration 147 | |-------------------------------------------------------------------------- 148 | | 149 | | The Debugbar can emulate the Clockwork headers, so you can use the Chrome 150 | | Extension, without the server-side code. It uses Debugbar collectors instead. 151 | | 152 | */ 153 | 'clockwork' => env('DEBUGBAR_CLOCKWORK', false), 154 | 155 | /* 156 | |-------------------------------------------------------------------------- 157 | | DataCollectors 158 | |-------------------------------------------------------------------------- 159 | | 160 | | Enable/disable DataCollectors 161 | | 162 | */ 163 | 164 | 'collectors' => [ 165 | 'phpinfo' => env('DEBUGBAR_COLLECTORS_PHPINFO', false), // Php version 166 | 'messages' => env('DEBUGBAR_COLLECTORS_MESSAGES', true), // Messages 167 | 'time' => env('DEBUGBAR_COLLECTORS_TIME', true), // Time Datalogger 168 | 'memory' => env('DEBUGBAR_COLLECTORS_MEMORY', true), // Memory usage 169 | 'exceptions' => env('DEBUGBAR_COLLECTORS_EXCEPTIONS', true), // Exception displayer 170 | 'log' => env('DEBUGBAR_COLLECTORS_LOG', true), // Logs from Monolog (merged in messages if enabled) 171 | 'db' => env('DEBUGBAR_COLLECTORS_DB', true), // Show database (PDO) queries and bindings 172 | 'views' => env('DEBUGBAR_COLLECTORS_VIEWS', true), // Views with their data 173 | 'route' => env('DEBUGBAR_COLLECTORS_ROUTE', false), // Current route information 174 | 'auth' => env('DEBUGBAR_COLLECTORS_AUTH', false), // Display Laravel authentication status 175 | 'gate' => env('DEBUGBAR_COLLECTORS_GATE', true), // Display Laravel Gate checks 176 | 'session' => env('DEBUGBAR_COLLECTORS_SESSION', false), // Display session data 177 | 'symfony_request' => env('DEBUGBAR_COLLECTORS_SYMFONY_REQUEST', true), // Only one can be enabled.. 178 | 'mail' => env('DEBUGBAR_COLLECTORS_MAIL', true), // Catch mail messages 179 | 'laravel' => env('DEBUGBAR_COLLECTORS_LARAVEL', true), // Laravel version and environment 180 | 'events' => env('DEBUGBAR_COLLECTORS_EVENTS', false), // All events fired 181 | 'default_request' => env('DEBUGBAR_COLLECTORS_DEFAULT_REQUEST', false), // Regular or special Symfony request logger 182 | 'logs' => env('DEBUGBAR_COLLECTORS_LOGS', false), // Add the latest log messages 183 | 'files' => env('DEBUGBAR_COLLECTORS_FILES', false), // Show the included files 184 | 'config' => env('DEBUGBAR_COLLECTORS_CONFIG', false), // Display config settings 185 | 'cache' => env('DEBUGBAR_COLLECTORS_CACHE', false), // Display cache events 186 | 'models' => env('DEBUGBAR_COLLECTORS_MODELS', true), // Display models 187 | 'livewire' => env('DEBUGBAR_COLLECTORS_LIVEWIRE', true), // Display Livewire (when available) 188 | 'jobs' => env('DEBUGBAR_COLLECTORS_JOBS', false), // Display dispatched jobs 189 | 'pennant' => env('DEBUGBAR_COLLECTORS_PENNANT', false), // Display Pennant feature flags 190 | ], 191 | 192 | /* 193 | |-------------------------------------------------------------------------- 194 | | Extra options 195 | |-------------------------------------------------------------------------- 196 | | 197 | | Configure some DataCollectors 198 | | 199 | */ 200 | 201 | 'options' => [ 202 | 'time' => [ 203 | 'memory_usage' => env('DEBUGBAR_OPTIONS_TIME_MEMORY_USAGE', false), // Calculated by subtracting memory start and end, it may be inaccurate 204 | ], 205 | 'messages' => [ 206 | 'trace' => env('DEBUGBAR_OPTIONS_MESSAGES_TRACE', true), // Trace the origin of the debug message 207 | 'capture_dumps' => env('DEBUGBAR_OPTIONS_MESSAGES_CAPTURE_DUMPS', false), // Capture laravel `dump();` as message 208 | ], 209 | 'memory' => [ 210 | 'reset_peak' => env('DEBUGBAR_OPTIONS_MEMORY_RESET_PEAK', false), // run memory_reset_peak_usage before collecting 211 | 'with_baseline' => env('DEBUGBAR_OPTIONS_MEMORY_WITH_BASELINE', false), // Set boot memory usage as memory peak baseline 212 | 'precision' => (int) env('DEBUGBAR_OPTIONS_MEMORY_PRECISION', 0), // Memory rounding precision 213 | ], 214 | 'auth' => [ 215 | 'show_name' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_NAME', true), // Also show the users name/email in the debugbar 216 | 'show_guards' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_GUARDS', true), // Show the guards that are used 217 | ], 218 | 'gate' => [ 219 | 'trace' => false, // Trace the origin of the Gate checks 220 | ], 221 | 'db' => [ 222 | 'with_params' => env('DEBUGBAR_OPTIONS_WITH_PARAMS', true), // Render SQL with the parameters substituted 223 | 'exclude_paths' => [ // Paths to exclude entirely from the collector 224 | //'vendor/laravel/framework/src/Illuminate/Session', // Exclude sessions queries 225 | ], 226 | 'backtrace' => env('DEBUGBAR_OPTIONS_DB_BACKTRACE', true), // Use a backtrace to find the origin of the query in your files. 227 | 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) 228 | 'timeline' => env('DEBUGBAR_OPTIONS_DB_TIMELINE', false), // Add the queries to the timeline 229 | 'duration_background' => env('DEBUGBAR_OPTIONS_DB_DURATION_BACKGROUND', true), // Show shaded background on each query relative to how long it took to execute. 230 | 'explain' => [ // Show EXPLAIN output on queries 231 | 'enabled' => env('DEBUGBAR_OPTIONS_DB_EXPLAIN_ENABLED', false), 232 | ], 233 | 'hints' => env('DEBUGBAR_OPTIONS_DB_HINTS', false), // Show hints for common mistakes 234 | 'show_copy' => env('DEBUGBAR_OPTIONS_DB_SHOW_COPY', true), // Show copy button next to the query, 235 | 'slow_threshold' => env('DEBUGBAR_OPTIONS_DB_SLOW_THRESHOLD', false), // Only track queries that last longer than this time in ms 236 | 'memory_usage' => env('DEBUGBAR_OPTIONS_DB_MEMORY_USAGE', false), // Show queries memory usage 237 | 'soft_limit' => (int) env('DEBUGBAR_OPTIONS_DB_SOFT_LIMIT', 100), // After the soft limit, no parameters/backtrace are captured 238 | 'hard_limit' => (int) env('DEBUGBAR_OPTIONS_DB_HARD_LIMIT', 500), // After the hard limit, queries are ignored 239 | ], 240 | 'mail' => [ 241 | 'timeline' => env('DEBUGBAR_OPTIONS_MAIL_TIMELINE', true), // Add mails to the timeline 242 | 'show_body' => env('DEBUGBAR_OPTIONS_MAIL_SHOW_BODY', true), 243 | ], 244 | 'views' => [ 245 | 'timeline' => env('DEBUGBAR_OPTIONS_VIEWS_TIMELINE', true), // Add the views to the timeline 246 | 'data' => env('DEBUGBAR_OPTIONS_VIEWS_DATA', false), // True for all data, 'keys' for only names, false for no parameters. 247 | 'group' => (int) env('DEBUGBAR_OPTIONS_VIEWS_GROUP', 50), // Group duplicate views. Pass value to auto-group, or true/false to force 248 | 'inertia_pages' => env('DEBUGBAR_OPTIONS_VIEWS_INERTIA_PAGES', 'js/Pages'), // Path for Inertia views 249 | 'exclude_paths' => [ // Add the paths which you don't want to appear in the views 250 | 'vendor/filament' // Exclude Filament components by default 251 | ], 252 | ], 253 | 'route' => [ 254 | 'label' => env('DEBUGBAR_OPTIONS_ROUTE_LABEL', true), // Show complete route on bar 255 | ], 256 | 'session' => [ 257 | 'hiddens' => [], // Hides sensitive values using array paths 258 | ], 259 | 'symfony_request' => [ 260 | 'label' => env('DEBUGBAR_OPTIONS_SYMFONY_REQUEST_LABEL', true), // Show route on bar 261 | 'hiddens' => [], // Hides sensitive values using array paths, example: request_request.password 262 | ], 263 | 'events' => [ 264 | 'data' => env('DEBUGBAR_OPTIONS_EVENTS_DATA', false), // Collect events data, listeners 265 | 'excluded' => [], // Example: ['eloquent.*', 'composing', Illuminate\Cache\Events\CacheHit::class] 266 | ], 267 | 'logs' => [ 268 | 'file' => env('DEBUGBAR_OPTIONS_LOGS_FILE', null), 269 | ], 270 | 'cache' => [ 271 | 'values' => env('DEBUGBAR_OPTIONS_CACHE_VALUES', true), // Collect cache values 272 | ], 273 | ], 274 | 275 | /* 276 | |-------------------------------------------------------------------------- 277 | | Inject Debugbar in Response 278 | |-------------------------------------------------------------------------- 279 | | 280 | | Usually, the debugbar is added just before , by listening to the 281 | | Response after the App is done. If you disable this, you have to add them 282 | | in your template yourself. See http://phpdebugbar.com/docs/rendering.html 283 | | 284 | */ 285 | 286 | 'inject' => env('DEBUGBAR_INJECT', true), 287 | 288 | /* 289 | |-------------------------------------------------------------------------- 290 | | Debugbar route prefix 291 | |-------------------------------------------------------------------------- 292 | | 293 | | Sometimes you want to set route prefix to be used by Debugbar to load 294 | | its resources from. Usually the need comes from misconfigured web server or 295 | | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 296 | | 297 | */ 298 | 'route_prefix' => env('DEBUGBAR_ROUTE_PREFIX', '_debugbar'), 299 | 300 | /* 301 | |-------------------------------------------------------------------------- 302 | | Debugbar route middleware 303 | |-------------------------------------------------------------------------- 304 | | 305 | | Additional middleware to run on the Debugbar routes 306 | */ 307 | 'route_middleware' => [], 308 | 309 | /* 310 | |-------------------------------------------------------------------------- 311 | | Debugbar route domain 312 | |-------------------------------------------------------------------------- 313 | | 314 | | By default Debugbar route served from the same domain that request served. 315 | | To override default domain, specify it as a non-empty value. 316 | */ 317 | 'route_domain' => env('DEBUGBAR_ROUTE_DOMAIN', null), 318 | 319 | /* 320 | |-------------------------------------------------------------------------- 321 | | Debugbar theme 322 | |-------------------------------------------------------------------------- 323 | | 324 | | Switches between light and dark theme. If set to auto it will respect system preferences 325 | | Possible values: auto, light, dark 326 | */ 327 | 'theme' => env('DEBUGBAR_THEME', 'auto'), 328 | 329 | /* 330 | |-------------------------------------------------------------------------- 331 | | Backtrace stack limit 332 | |-------------------------------------------------------------------------- 333 | | 334 | | By default, the Debugbar limits the number of frames returned by the 'debug_backtrace()' function. 335 | | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. 336 | */ 337 | 'debug_backtrace_limit' => (int) env('DEBUGBAR_DEBUG_BACKTRACE_LIMIT', 50), 338 | ]; 339 | -------------------------------------------------------------------------------- /database/migrations/2014_12_01_120000_create_phpdebugbar_storage_table.php: -------------------------------------------------------------------------------- 1 | string('id'); 16 | $table->longText('data'); 17 | $table->string('meta_utime'); 18 | $table->dateTime('meta_datetime'); 19 | $table->string('meta_uri'); 20 | $table->string('meta_ip'); 21 | $table->string('meta_method'); 22 | 23 | $table->primary('id'); 24 | $table->index('meta_utime'); 25 | $table->index('meta_datetime'); 26 | $table->index('meta_uri'); 27 | $table->index('meta_ip'); 28 | $table->index('meta_method'); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | */ 35 | public function down() 36 | { 37 | Schema::drop('phpdebugbar'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | config 7 | src 8 | tests 9 | 10 | src/Resources/* 11 | *.blade.php 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Debugbar for Laravel 2 | ![Unit Tests](https://github.com/barryvdh/laravel-debugbar/workflows/Unit%20Tests/badge.svg) 3 | [![Packagist License](https://img.shields.io/badge/Licence-MIT-blue)](http://choosealicense.com/licenses/mit/) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/barryvdh/laravel-debugbar?label=Stable)](https://packagist.org/packages/barryvdh/laravel-debugbar) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/barryvdh/laravel-debugbar?label=Downloads)](https://packagist.org/packages/barryvdh/laravel-debugbar) 6 | [![Fruitcake](https://img.shields.io/badge/Powered%20By-Fruitcake-b2bc35.svg)](https://fruitcake.nl/) 7 | 8 | This is a package to integrate [PHP Debug Bar](https://github.com/php-debugbar/php-debugbar) with Laravel. 9 | It includes a ServiceProvider to register the debugbar and attach it to the output. You can publish assets and configure it through Laravel. 10 | It bootstraps some Collectors to work with Laravel and implements a couple custom DataCollectors, specific for Laravel. 11 | It is configured to display Redirects and Ajax/Livewire Requests. (Shown in a dropdown) 12 | Read [the documentation](http://phpdebugbar.com/docs/) for more configuration options. 13 | 14 | ![Debugbar Dark Mode screenshot](https://github.com/barryvdh/laravel-debugbar/assets/973269/6600837a-8b2d-4acb-ab0c-158c9ca5439c) 15 | 16 | > [!CAUTION] 17 | > Use the DebugBar only in development. Do not use Debugbar on publicly accessible websites, as it will leak information from stored requests (by design). 18 | 19 | > [!WARNING] 20 | > 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. 21 | 22 | This package includes some custom collectors: 23 | - QueryCollector: Show all queries, including binding + timing 24 | - RouteCollector: Show information about the current Route. 25 | - ViewCollector: Show the currently loaded views. (Optionally: display the shared data) 26 | - EventsCollector: Show all events 27 | - LaravelCollector: Show the Laravel version and Environment. (disabled by default) 28 | - SymfonyRequestCollector: replaces the RequestCollector with more information about the request/response 29 | - LogsCollector: Show the latest log entries from the storage logs. (disabled by default) 30 | - FilesCollector: Show the files that are included/required by PHP. (disabled by default) 31 | - ConfigCollector: Display the values from the config files. (disabled by default) 32 | - CacheCollector: Display all cache events. (disabled by default) 33 | 34 | Bootstraps the following collectors for Laravel: 35 | - LogCollector: Show all Log messages 36 | - SymfonyMailCollector for Mail 37 | 38 | And the default collectors: 39 | - PhpInfoCollector 40 | - MessagesCollector 41 | - TimeDataCollector (With Booting and Application timing) 42 | - MemoryCollector 43 | - ExceptionsCollector 44 | 45 | It also provides a facade interface (`Debugbar`) for easy logging Messages, Exceptions and Time 46 | 47 | ## Installation 48 | 49 | Require this package with composer. It is recommended to only require the package for development. 50 | 51 | ```shell 52 | composer require barryvdh/laravel-debugbar --dev 53 | ``` 54 | 55 | Laravel uses Package Auto-Discovery, so doesn't require you to manually add the ServiceProvider. 56 | 57 | The Debugbar will be enabled when `APP_DEBUG` is `true`. 58 | 59 | > If you use a catch-all/fallback route, make sure you load the Debugbar ServiceProvider before your own App ServiceProviders. 60 | 61 | ### Laravel without auto-discovery: 62 | 63 | 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. 64 | 65 | ```php 66 | Barryvdh\Debugbar\ServiceProvider::class, 67 | ``` 68 | 69 | If you want to use the facade to log messages, add this within the `register` method of `app/Providers/AppServiceProvider.php` class: 70 | 71 | ```php 72 | public function register(): void 73 | { 74 | $loader = \Illuminate\Foundation\AliasLoader::getInstance(); 75 | $loader->alias('Debugbar', \Barryvdh\Debugbar\Facades\Debugbar::class); 76 | } 77 | ``` 78 | 79 | The profiler is enabled by default, if you have APP_DEBUG=true. You can override that in the config (`debugbar.enabled`) or by setting `DEBUGBAR_ENABLED` in your `.env`. See more options in `config/debugbar.php` 80 | 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. 81 | 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) 82 | 83 | #### Copy the package config to your local config with the publish command: 84 | 85 | ```shell 86 | php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider" 87 | ``` 88 | 89 | ### Laravel with Octane: 90 | 91 | Make sure to add LaravelDebugbar to your flush list in `config/octane.php`. 92 | 93 | ```php 94 | 'flush' => [ 95 | \Barryvdh\Debugbar\LaravelDebugbar::class, 96 | ], 97 | ``` 98 | 99 | ### Lumen: 100 | 101 | For Lumen, register a different Provider in `bootstrap/app.php`: 102 | 103 | ```php 104 | if (env('APP_DEBUG')) { 105 | $app->register(Barryvdh\Debugbar\LumenServiceProvider::class); 106 | } 107 | ``` 108 | 109 | To change the configuration, copy the file to your config folder and enable it: 110 | 111 | ```php 112 | $app->configure('debugbar'); 113 | ``` 114 | 115 | ## Usage 116 | 117 | You can now add messages using the Facade (when added), using the PSR-3 levels (debug, info, notice, warning, error, critical, alert, emergency): 118 | 119 | ```php 120 | Debugbar::info($object); 121 | Debugbar::error('Error!'); 122 | Debugbar::warning('Watch out…'); 123 | Debugbar::addMessage('Another message', 'mylabel'); 124 | ``` 125 | 126 | And start/stop timing: 127 | 128 | ```php 129 | Debugbar::startMeasure('render','Time for rendering'); 130 | Debugbar::stopMeasure('render'); 131 | Debugbar::addMeasure('now', LARAVEL_START, microtime(true)); 132 | Debugbar::measure('My long operation', function() { 133 | // Do something… 134 | }); 135 | ``` 136 | 137 | Or log exceptions: 138 | 139 | ```php 140 | try { 141 | throw new Exception('foobar'); 142 | } catch (Exception $e) { 143 | Debugbar::addThrowable($e); 144 | } 145 | ``` 146 | 147 | There are also helper functions available for the most common calls: 148 | 149 | ```php 150 | // All arguments will be dumped as a debug message 151 | debug($var1, $someString, $intValue, $object); 152 | 153 | // `$collection->debug()` will return the collection and dump it as a debug message. Like `$collection->dump()` 154 | collect([$var1, $someString])->debug(); 155 | 156 | start_measure('render','Time for rendering'); 157 | stop_measure('render'); 158 | add_measure('now', LARAVEL_START, microtime(true)); 159 | measure('My long operation', function() { 160 | // Do something… 161 | }); 162 | ``` 163 | 164 | If you want you can add your own DataCollectors, through the Container or the Facade: 165 | 166 | ```php 167 | Debugbar::addCollector(new DebugBar\DataCollector\MessagesCollector('my_messages')); 168 | //Or via the App container: 169 | $debugbar = App::make('debugbar'); 170 | $debugbar->addCollector(new DebugBar\DataCollector\MessagesCollector('my_messages')); 171 | ``` 172 | 173 | By default, the Debugbar is injected just before ``. If you want to inject the Debugbar yourself, 174 | set the config option 'inject' to false and use the renderer yourself and follow http://phpdebugbar.com/docs/rendering.html 175 | 176 | ```php 177 | $renderer = Debugbar::getJavascriptRenderer(); 178 | ``` 179 | 180 | Note: Not using the auto-inject, will disable the Request information, because that is added After the response. 181 | You can add the default_request datacollector in the config as alternative. 182 | 183 | ## Enabling/Disabling on run time 184 | You can enable or disable the debugbar during run time. 185 | 186 | ```php 187 | \Debugbar::enable(); 188 | \Debugbar::disable(); 189 | ``` 190 | 191 | 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. 192 | 193 | ## Storage 194 | 195 | 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. 196 | Make sure you only do this on local development, because otherwise other people will be able to view previous requests. 197 | In general, Debugbar should only be used locally or at least restricted by IP. 198 | It's possible to pass a callback, which will receive the Request object, so you can determine access to the OpenHandler storage. 199 | 200 | ## Twig Integration 201 | 202 | Laravel Debugbar comes with two Twig Extensions. These are tested with [rcrowe/TwigBridge](https://github.com/rcrowe/TwigBridge) 0.6.x 203 | 204 | Add the following extensions to your TwigBridge config/extensions.php (or register the extensions manually) 205 | 206 | ```php 207 | 'Barryvdh\Debugbar\Twig\Extension\Debug', 208 | 'Barryvdh\Debugbar\Twig\Extension\Dump', 209 | 'Barryvdh\Debugbar\Twig\Extension\Stopwatch', 210 | ``` 211 | 212 | 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, 213 | instead of showing it directly in the template. It dumps the arguments, or when empty; all context variables. 214 | 215 | ```twig 216 | {{ debug() }} 217 | {{ debug(user, categories) }} 218 | ``` 219 | 220 | 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. 221 | 222 | ```twig 223 | {% stopwatch "foo" %} 224 | …some things that gets timed 225 | {% endstopwatch %} 226 | ``` 227 | -------------------------------------------------------------------------------- /src/Console/ClearCommand.php: -------------------------------------------------------------------------------- 1 | debugbar = $debugbar; 17 | 18 | parent::__construct(); 19 | } 20 | 21 | public function handle() 22 | { 23 | $this->debugbar->boot(); 24 | 25 | if ($storage = $this->debugbar->getStorage()) { 26 | try { 27 | $storage->clear(); 28 | } catch (\InvalidArgumentException $e) { 29 | // hide InvalidArgumentException if storage location does not exist 30 | if (strpos($e->getMessage(), 'does not exist') === false) { 31 | throw $e; 32 | } 33 | } 34 | $this->info('Debugbar Storage cleared!'); 35 | } else { 36 | $this->error('No Debugbar Storage found..'); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controllers/AssetController.php: -------------------------------------------------------------------------------- 1 | debugbar->getJavascriptRenderer(); 17 | 18 | $content = $renderer->dumpAssetsToString('js'); 19 | 20 | $response = new Response( 21 | $content, 22 | 200, 23 | [ 24 | 'Content-Type' => 'text/javascript', 25 | ] 26 | ); 27 | 28 | return $this->cacheResponse($response); 29 | } 30 | 31 | /** 32 | * Return the stylesheets for the Debugbar 33 | * 34 | * @return \Symfony\Component\HttpFoundation\Response 35 | */ 36 | public function css() 37 | { 38 | $renderer = $this->debugbar->getJavascriptRenderer(); 39 | 40 | $content = $renderer->dumpAssetsToString('css'); 41 | 42 | $response = new Response( 43 | $content, 44 | 200, 45 | [ 46 | 'Content-Type' => 'text/css', 47 | ] 48 | ); 49 | 50 | return $this->cacheResponse($response); 51 | } 52 | 53 | /** 54 | * Cache the response 1 year (31536000 sec) 55 | */ 56 | protected function cacheResponse(Response $response) 57 | { 58 | $response->setSharedMaxAge(31536000); 59 | $response->setMaxAge(31536000); 60 | $response->setExpires(new \DateTime('+1 year')); 61 | 62 | return $response; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Controllers/BaseController.php: -------------------------------------------------------------------------------- 1 | debugbar = $debugbar; 20 | 21 | if ($request->hasSession()) { 22 | $request->session()->reflash(); 23 | } 24 | 25 | $this->middleware(function ($request, $next) { 26 | if (class_exists(Telescope::class)) { 27 | Telescope::stopRecording(); 28 | } 29 | return $next($request); 30 | }); 31 | } 32 | } 33 | 34 | } else { 35 | 36 | class BaseController 37 | { 38 | protected $debugbar; 39 | 40 | public function __construct(Request $request, LaravelDebugbar $debugbar) 41 | { 42 | $this->debugbar = $debugbar; 43 | 44 | if ($request->hasSession()) { 45 | $request->session()->reflash(); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Controllers/CacheController.php: -------------------------------------------------------------------------------- 1 | tags($tags); 20 | } else { 21 | unset($tags); 22 | } 23 | 24 | $success = $cache->forget($key); 25 | 26 | return response()->json(compact('success')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Controllers/OpenHandlerController.php: -------------------------------------------------------------------------------- 1 | ip(), ['127.0.0.1', '::1'], true)) { 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | public function handle(Request $request) 44 | { 45 | if ($request->input('op') === 'get' || $this->isStorageOpen($request)) { 46 | $openHandler = new OpenHandler($this->debugbar); 47 | $data = $openHandler->handle($request->input(), false, false); 48 | } else { 49 | $data = [ 50 | [ 51 | 'datetime' => date("Y-m-d H:i:s"), 52 | 'id' => null, 53 | 'ip' => $request->getClientIp(), 54 | 'method' => 'ERROR', 55 | '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. !!', 56 | 'utime' => microtime(true), 57 | ] 58 | ]; 59 | } 60 | 61 | return new Response( 62 | $data, 63 | 200, 64 | [ 65 | 'Content-Type' => 'application/json' 66 | ] 67 | ); 68 | } 69 | 70 | /** 71 | * Return Clockwork output 72 | * 73 | * @param $id 74 | * @return mixed 75 | * @throws \DebugBar\DebugBarException 76 | */ 77 | public function clockwork(Request $request, $id) 78 | { 79 | $request = [ 80 | 'op' => 'get', 81 | 'id' => $id, 82 | ]; 83 | 84 | $openHandler = new OpenHandler($this->debugbar); 85 | $data = $openHandler->handle($request, false, false); 86 | 87 | // Convert to Clockwork 88 | $converter = new Converter(); 89 | $output = $converter->convert(json_decode($data, true)); 90 | 91 | return response()->json($output); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Controllers/QueriesController.php: -------------------------------------------------------------------------------- 1 | json([ 18 | 'success' => false, 19 | 'message' => 'EXPLAIN is currently disabled in the Debugbar.', 20 | ], 400); 21 | } 22 | 23 | try { 24 | $explain = new Explain(); 25 | 26 | if ($request->json('mode') === 'visual') { 27 | return response()->json([ 28 | 'success' => true, 29 | 'data' => $explain->generateVisualExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')), 30 | ]); 31 | } 32 | 33 | return response()->json([ 34 | 'success' => true, 35 | 'data' => $explain->generateRawExplain($request->json('connection'), $request->json('query'), $request->json('bindings'), $request->json('hash')), 36 | 'visual' => $explain->isVisualExplainSupported($request->json('connection')) ? [ 37 | 'confirm' => $explain->confirmVisualExplain($request->json('connection')), 38 | ] : null, 39 | ]); 40 | } catch (Exception $e) { 41 | return response()->json([ 42 | 'success' => false, 43 | 'message' => $e->getMessage(), 44 | ], 400); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Controllers/TelescopeController.php: -------------------------------------------------------------------------------- 1 | find($uuid); 19 | $result = $storage->get('request', (new EntryQueryOptions())->batchId($entry->batchId))->first(); 20 | 21 | return redirect(config('telescope.domain') . '/' . config('telescope.path') . '/requests/' . $result->id); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DataCollector/CacheCollector.php: -------------------------------------------------------------------------------- 1 | ['hit', RetrievingKey::class], 36 | CacheMissed::class => ['missed', RetrievingKey::class], 37 | CacheFlushed::class => ['flushed', CacheFlushing::class], 38 | CacheFlushFailed::class => ['flush_failed', CacheFlushing::class], 39 | KeyWritten::class => ['written', WritingKey::class], 40 | KeyWriteFailed::class => ['write_failed', WritingKey::class], 41 | KeyForgotten::class => ['forgotten', ForgettingKey::class], 42 | KeyForgetFailed::class => ['forget_failed', ForgettingKey::class], 43 | ]; 44 | 45 | public function __construct($requestStartTime, $collectValues) 46 | { 47 | parent::__construct($requestStartTime); 48 | 49 | $this->collectValues = $collectValues; 50 | } 51 | 52 | public function onCacheEvent($event) 53 | { 54 | $class = get_class($event); 55 | $params = get_object_vars($event); 56 | $label = $this->classMap[$class][0]; 57 | 58 | if (isset($params['value'])) { 59 | if ($this->collectValues) { 60 | if ($this->isHtmlVarDumperUsed()) { 61 | $params['value'] = $this->getVarDumper()->renderVar($params['value']); 62 | } else { 63 | $params['value'] = htmlspecialchars($this->getDataFormatter()->formatVar($params['value'])); 64 | } 65 | } else { 66 | unset($params['value']); 67 | } 68 | } 69 | 70 | if (!empty($params['key'] ?? null) && in_array($label, ['hit', 'written'])) { 71 | $params['delete'] = route('debugbar.cache.delete', [ 72 | 'key' => urlencode($params['key']), 73 | 'tags' => !empty($params['tags']) ? json_encode($params['tags']) : '', 74 | ]); 75 | } 76 | 77 | $time = microtime(true); 78 | $startHashKey = $this->getEventHash($this->classMap[$class][1] ?? '', $params); 79 | $startTime = $this->eventStarts[$startHashKey] ?? $time; 80 | $this->addMeasure($label . "\t" . ($params['key'] ?? ''), $startTime, $time, $params); 81 | } 82 | 83 | public function onStartCacheEvent($event) 84 | { 85 | $startHashKey = $this->getEventHash(get_class($event), get_object_vars($event)); 86 | $this->eventStarts[$startHashKey] = microtime(true); 87 | } 88 | 89 | private function getEventHash(string $class, array $params): string 90 | { 91 | unset($params['value']); 92 | 93 | return $class . ':' . substr(hash('sha256', json_encode($params)), 0, 12); 94 | } 95 | 96 | public function subscribe(Dispatcher $dispatcher) 97 | { 98 | foreach (array_keys($this->classMap) as $eventClass) { 99 | $dispatcher->listen($eventClass, [$this, 'onCacheEvent']); 100 | } 101 | 102 | $startEvents = array_unique(array_filter(array_map( 103 | fn ($values) => $values[1] ?? null, 104 | array_values($this->classMap) 105 | ))); 106 | 107 | foreach ($startEvents as $eventClass) { 108 | $dispatcher->listen($eventClass, [$this, 'onStartCacheEvent']); 109 | } 110 | } 111 | 112 | public function collect() 113 | { 114 | $data = parent::collect(); 115 | $data['nb_measures'] = $data['count'] = count($data['measures']); 116 | 117 | return $data; 118 | } 119 | 120 | public function getName() 121 | { 122 | return 'cache'; 123 | } 124 | 125 | public function getWidgets() 126 | { 127 | return [ 128 | 'cache' => [ 129 | 'icon' => 'clipboard', 130 | 'widget' => 'PhpDebugBar.Widgets.LaravelCacheWidget', 131 | 'map' => 'cache', 132 | 'default' => '{}', 133 | ], 134 | 'cache:badge' => [ 135 | 'map' => 'cache.nb_measures', 136 | 'default' => 'null', 137 | ], 138 | ]; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/DataCollector/EventCollector.php: -------------------------------------------------------------------------------- 1 | collectValues = $collectValues; 29 | $this->excludedEvents = $excludedEvents; 30 | $this->setDataFormatter(new SimpleFormatter()); 31 | } 32 | 33 | public function onWildcardEvent($name = null, $data = []) 34 | { 35 | $currentTime = microtime(true); 36 | $eventClass = explode(':', $name)[0]; 37 | 38 | foreach ($this->excludedEvents as $excludedEvent) { 39 | if (Str::is($excludedEvent, $eventClass)) { 40 | return; 41 | } 42 | } 43 | 44 | if (! $this->collectValues) { 45 | $this->addMeasure($name, $currentTime, $currentTime, [], null, $eventClass); 46 | 47 | return; 48 | } 49 | 50 | $params = $this->prepareParams($data); 51 | 52 | // Find all listeners for the current event 53 | foreach ($this->events->getListeners($name) as $i => $listener) { 54 | // Check if it's an object + method name 55 | if (is_array($listener) && count($listener) > 1 && is_object($listener[0])) { 56 | list($class, $method) = $listener; 57 | 58 | // Skip this class itself 59 | if ($class instanceof static) { 60 | continue; 61 | } 62 | 63 | // Format the listener to readable format 64 | $listener = get_class($class) . '@' . $method; 65 | 66 | // Handle closures 67 | } elseif ($listener instanceof \Closure) { 68 | $reflector = new \ReflectionFunction($listener); 69 | 70 | // Skip our own listeners 71 | if ($reflector->getNamespaceName() == 'Barryvdh\Debugbar') { 72 | continue; 73 | } 74 | 75 | // Format the closure to a readable format 76 | $filename = ltrim(str_replace(base_path(), '', $reflector->getFileName()), '/'); 77 | $lines = $reflector->getStartLine() . '-' . $reflector->getEndLine(); 78 | $listener = $reflector->getName() . ' (' . $filename . ':' . $lines . ')'; 79 | } else { 80 | // Not sure if this is possible, but to prevent edge cases 81 | $listener = $this->getDataFormatter()->formatVar($listener); 82 | } 83 | 84 | $params['listeners.' . $i] = $listener; 85 | } 86 | $this->addMeasure($name, $currentTime, $currentTime, $params, null, $eventClass); 87 | } 88 | 89 | public function subscribe(Dispatcher $events) 90 | { 91 | $this->events = $events; 92 | $events->listen('*', [$this, 'onWildcardEvent']); 93 | } 94 | 95 | protected function prepareParams($params) 96 | { 97 | $data = []; 98 | foreach ($params as $key => $value) { 99 | if (is_object($value) && Str::is('Illuminate\*\Events\*', get_class($value))) { 100 | $value = $this->prepareParams(get_object_vars($value)); 101 | } 102 | $data[$key] = htmlentities($this->getDataFormatter()->formatVar($value), ENT_QUOTES, 'UTF-8', false); 103 | } 104 | 105 | return $data; 106 | } 107 | 108 | public function collect() 109 | { 110 | $data = parent::collect(); 111 | $data['nb_measures'] = $data['count'] = count($data['measures']); 112 | 113 | return $data; 114 | } 115 | 116 | public function getName() 117 | { 118 | return 'event'; 119 | } 120 | 121 | public function getWidgets() 122 | { 123 | return [ 124 | "events" => [ 125 | "icon" => "tasks", 126 | "widget" => "PhpDebugBar.Widgets.TimelineWidget", 127 | "map" => "event", 128 | "default" => "{}", 129 | ], 130 | 'events:badge' => [ 131 | 'map' => 'event.nb_measures', 132 | 'default' => 0, 133 | ], 134 | ]; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/DataCollector/FilesCollector.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | $this->basePath = base_path(); 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function collect() 28 | { 29 | $files = $this->getIncludedFiles(); 30 | $compiled = $this->getCompiledFiles(); 31 | 32 | $included = []; 33 | $alreadyCompiled = []; 34 | foreach ($files as $file) { 35 | // Skip the files from Debugbar, they are only loaded for Debugging and confuse the output. 36 | // Of course some files are stil always loaded (ServiceProvider, Facade etc) 37 | if ( 38 | strpos($file, 'vendor/maximebf/debugbar/src') !== false || strpos( 39 | $file, 40 | 'vendor/barryvdh/laravel-debugbar/src' 41 | ) !== false 42 | ) { 43 | continue; 44 | } elseif (!in_array($file, $compiled)) { 45 | $included[] = [ 46 | 'message' => "'" . $this->stripBasePath($file) . "',", 47 | // Use PHP syntax so we can copy-paste to compile config file. 48 | 'is_string' => true, 49 | ]; 50 | } else { 51 | $alreadyCompiled[] = [ 52 | 'message' => "* '" . $this->stripBasePath($file) . "',", 53 | // Mark with *, so know they are compiled anyway. 54 | 'is_string' => true, 55 | ]; 56 | } 57 | } 58 | 59 | // First the included files, then those that are going to be compiled. 60 | $messages = array_merge($included, $alreadyCompiled); 61 | 62 | return [ 63 | 'messages' => $messages, 64 | 'count' => count($included), 65 | ]; 66 | } 67 | 68 | /** 69 | * Get the files included on load. 70 | * 71 | * @return array 72 | */ 73 | protected function getIncludedFiles() 74 | { 75 | return get_included_files(); 76 | } 77 | 78 | /** 79 | * Get the files that are going to be compiled, so they aren't as important. 80 | * 81 | * @return array 82 | */ 83 | protected function getCompiledFiles() 84 | { 85 | if ($this->app && class_exists('Illuminate\Foundation\Console\OptimizeCommand')) { 86 | $reflector = new \ReflectionClass('Illuminate\Foundation\Console\OptimizeCommand'); 87 | $path = dirname($reflector->getFileName()) . '/Optimize/config.php'; 88 | 89 | if (file_exists($path)) { 90 | $app = $this->app; 91 | $core = require $path; 92 | return array_merge($core, $app['config']['compile']); 93 | } 94 | } 95 | return []; 96 | } 97 | 98 | /** 99 | * Remove the basePath from the paths, so they are relative to the base 100 | * 101 | * @param $path 102 | * @return string 103 | */ 104 | protected function stripBasePath($path) 105 | { 106 | return ltrim(str_replace($this->basePath, '', $path), '/'); 107 | } 108 | 109 | /** 110 | * {@inheritDoc} 111 | */ 112 | public function getWidgets() 113 | { 114 | $name = $this->getName(); 115 | return [ 116 | "$name" => [ 117 | "icon" => "files-o", 118 | "widget" => "PhpDebugBar.Widgets.MessagesWidget", 119 | "map" => "$name.messages", 120 | "default" => "{}" 121 | ], 122 | "$name:badge" => [ 123 | "map" => "$name.count", 124 | "default" => "null" 125 | ] 126 | ]; 127 | } 128 | 129 | /** 130 | * {@inheritDoc} 131 | */ 132 | public function getName() 133 | { 134 | return 'files'; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/DataCollector/GateCollector.php: -------------------------------------------------------------------------------- 1 | router = $router; 36 | $this->setDataFormatter(new SimpleFormatter()); 37 | $gate->after(function ($user, $ability, $result, $arguments = []) { 38 | $this->addCheck($user, $ability, $result, $arguments); 39 | }); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | protected function customizeMessageHtml($messageHtml, $message) 46 | { 47 | $pos = strpos((string) $messageHtml, 'array:5'); 48 | if ($pos !== false) { 49 | 50 | $name = $message['ability'] .' ' . $message['target'] ?? ''; 51 | 52 | $messageHtml = substr_replace($messageHtml, $name, $pos, 7); 53 | } 54 | 55 | return parent::customizeMessageHtml($messageHtml, $message); 56 | } 57 | 58 | public function addCheck($user, $ability, $result, $arguments = []) 59 | { 60 | $userKey = 'user'; 61 | $userId = null; 62 | 63 | if ($user) { 64 | $userKey = Str::snake(class_basename($user)); 65 | $userId = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user->getKey(); 66 | } 67 | 68 | $label = $result ? 'success' : 'error'; 69 | 70 | if ($result instanceof Response) { 71 | $label = $result->allowed() ? 'success' : 'error'; 72 | } 73 | 74 | $target = null; 75 | if (isset($arguments[0])) { 76 | if ($arguments[0] instanceof Model) { 77 | $model = $arguments[0]; 78 | if ($model->getKeyName() && isset($model[$model->getKeyName()])) { 79 | $target = get_class($model) . '(' . $model->getKeyName() . '=' . $model->getKey() . ')'; 80 | } else { 81 | $target = get_class($model); 82 | } 83 | } else if (is_string($arguments[0])) { 84 | $target = $arguments[0]; 85 | } 86 | } 87 | 88 | $this->addMessage([ 89 | 'ability' => $ability, 90 | 'target' => $target, 91 | 'result' => $result, 92 | $userKey => $userId, 93 | 'arguments' => $this->getDataFormatter()->formatVar($arguments), 94 | ], $label, false); 95 | } 96 | 97 | /** 98 | * @param array $stacktrace 99 | * 100 | * @return array 101 | */ 102 | protected function getStackTraceItem($stacktrace) 103 | { 104 | foreach ($stacktrace as $i => $trace) { 105 | if (!isset($trace['file'])) { 106 | continue; 107 | } 108 | 109 | if (str_ends_with($trace['file'], 'Illuminate/Routing/ControllerDispatcher.php')) { 110 | $trace = $this->findControllerFromDispatcher($trace); 111 | } elseif (str_starts_with($trace['file'], storage_path())) { 112 | $hash = pathinfo($trace['file'], PATHINFO_FILENAME); 113 | 114 | if ($file = $this->findViewFromHash($hash)) { 115 | $trace['file'] = $file; 116 | } 117 | } 118 | 119 | if ($this->fileIsInExcludedPath($trace['file'])) { 120 | continue; 121 | } 122 | 123 | return $trace; 124 | } 125 | 126 | return $stacktrace[0]; 127 | } 128 | 129 | /** 130 | * Find the route action file 131 | * 132 | * @param array $trace 133 | * @return array 134 | */ 135 | protected function findControllerFromDispatcher($trace) 136 | { 137 | /** @var \Closure|string|array $action */ 138 | $action = $this->router->current()->getAction('uses'); 139 | 140 | if (is_string($action)) { 141 | [$controller, $method] = explode('@', $action); 142 | 143 | $reflection = new \ReflectionMethod($controller, $method); 144 | $trace['file'] = $reflection->getFileName(); 145 | $trace['line'] = $reflection->getStartLine(); 146 | } elseif ($action instanceof \Closure) { 147 | $reflection = new \ReflectionFunction($action); 148 | $trace['file'] = $reflection->getFileName(); 149 | $trace['line'] = $reflection->getStartLine(); 150 | } 151 | 152 | return $trace; 153 | } 154 | 155 | /** 156 | * Find the template name from the hash. 157 | * 158 | * @param string $hash 159 | * @return null|array 160 | */ 161 | protected function findViewFromHash($hash) 162 | { 163 | $finder = app('view')->getFinder(); 164 | 165 | if (isset($this->reflection['viewfinderViews'])) { 166 | $property = $this->reflection['viewfinderViews']; 167 | } else { 168 | $reflection = new \ReflectionClass($finder); 169 | $property = $reflection->getProperty('views'); 170 | $property->setAccessible(true); 171 | $this->reflection['viewfinderViews'] = $property; 172 | } 173 | 174 | $xxh128Exists = in_array('xxh128', hash_algos()); 175 | 176 | foreach ($property->getValue($finder) as $name => $path) { 177 | if (($xxh128Exists && hash('xxh128', 'v2' . $path) == $hash) || sha1('v2' . $path) == $hash) { 178 | return $path; 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/DataCollector/JobsCollector.php: -------------------------------------------------------------------------------- 1 | listen(\Illuminate\Queue\Events\JobQueued::class, function ($event) { 24 | $class = get_class($event->job); 25 | $this->jobs[$class] = ($this->jobs[$class] ?? 0) + 1; 26 | $this->count++; 27 | }); 28 | } 29 | 30 | public function collect() 31 | { 32 | ksort($this->jobs, SORT_NUMERIC); 33 | 34 | return ['data' => array_reverse($this->jobs), 'count' => $this->count]; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function getName() 41 | { 42 | return 'jobs'; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function getWidgets() 49 | { 50 | return [ 51 | "jobs" => [ 52 | "icon" => "briefcase", 53 | "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", 54 | "map" => "jobs.data", 55 | "default" => "{}" 56 | ], 57 | 'jobs:badge' => [ 58 | 'map' => 'jobs.count', 59 | 'default' => 0 60 | ] 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/DataCollector/LaravelCollector.php: -------------------------------------------------------------------------------- 1 | Str::of($this->laravel->version())->explode('.')->first() . '.x', 27 | 'tooltip' => [ 28 | 'Laravel Version' => $this->laravel->version(), 29 | 'PHP Version' => phpversion(), 30 | 'Environment' => $this->laravel->environment(), 31 | 'Debug Mode' => config('app.debug') ? 'Enabled' : 'Disabled', 32 | 'URL' => Str::of(config('app.url'))->replace(['http://', 'https://'], ''), 33 | 'Timezone' => config('app.timezone'), 34 | 'Locale' => config('app.locale'), 35 | ] 36 | ]; 37 | } 38 | 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function getName() 44 | { 45 | return 'laravel'; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getWidgets() 52 | { 53 | return [ 54 | "version" => [ 55 | "icon" => "laravel phpdebugbar-fab", 56 | "map" => "laravel.version", 57 | "default" => "" 58 | ], 59 | "version:tooltip" => [ 60 | "map" => "laravel.tooltip", 61 | "default" => "{}" 62 | ], 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DataCollector/LivewireCollector.php: -------------------------------------------------------------------------------- 1 | getData()['_instance']; 30 | 31 | // Create a unique name for each component 32 | $key = $component->getName() . ' #' . $component->id; 33 | 34 | $data = [ 35 | 'data' => $component->getPublicPropertiesDefinedBySubClass(), 36 | ]; 37 | 38 | if ($request->request->get('id') == $component->id) { 39 | $data['oldData'] = $request->request->get('data'); 40 | $data['actionQueue'] = $request->request->get('actionQueue'); 41 | } 42 | 43 | $data['name'] = $component->getName(); 44 | $data['view'] = $view->name(); 45 | $data['component'] = get_class($component); 46 | $data['id'] = $component->id; 47 | 48 | $this->data[$key] = $this->formatVar($data); 49 | }); 50 | 51 | Livewire::listen('render', function (Component $component) use ($request) { 52 | // Create an unique name for each compoent 53 | $key = $component->getName() . ' #' . $component->getId(); 54 | 55 | $data = [ 56 | 'data' => $component->all(), 57 | ]; 58 | 59 | if ($request->request->get('id') == $component->getId()) { 60 | $data['oldData'] = $request->request->get('data'); 61 | $data['actionQueue'] = $request->request->get('actionQueue'); 62 | } 63 | 64 | $data['name'] = $component->getName(); 65 | $data['component'] = get_class($component); 66 | $data['id'] = $component->getId(); 67 | 68 | $this->data[$key] = $this->formatVar($data); 69 | }); 70 | } 71 | 72 | public function collect() 73 | { 74 | return ['data' => $this->data, 'count' => count($this->data)]; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | public function getName() 81 | { 82 | return 'livewire'; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | public function getWidgets() 89 | { 90 | return [ 91 | "livewire" => [ 92 | "icon" => "bolt", 93 | "widget" => "PhpDebugBar.Widgets.VariableListWidget", 94 | "map" => "livewire.data", 95 | "default" => "{}" 96 | ], 97 | 'livewire:badge' => [ 98 | 'map' => 'livewire.count', 99 | 'default' => 0 100 | ] 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/DataCollector/LogsCollector.php: -------------------------------------------------------------------------------- 1 | getStorageLogs($path); 25 | } 26 | } 27 | 28 | /** 29 | * get logs apache in app/storage/logs 30 | * only 24 last of current day 31 | * 32 | * @param string $path 33 | * 34 | * @return array 35 | */ 36 | public function getStorageLogs($path) 37 | { 38 | if (!file_exists($path)) { 39 | return; 40 | } 41 | 42 | //Load the latest lines, guessing about 15x the number of log entries (for stack traces etc) 43 | $file = implode("", $this->tailFile($path, $this->lines)); 44 | $basename = basename($path); 45 | 46 | foreach ($this->getLogs($file) as $log) { 47 | $this->messages[] = [ 48 | 'message' => $log['header'] . $log['stack'], 49 | 'label' => $log['level'], 50 | 'time' => substr($log['header'], 1, 19), 51 | 'collector' => $basename, 52 | 'is_string' => false, 53 | ]; 54 | } 55 | } 56 | 57 | /** 58 | * By Ain Tohvri (ain) 59 | * http://tekkie.flashbit.net/php/tail-functionality-in-php 60 | * @param string $file 61 | * @param int $lines 62 | * @return array 63 | */ 64 | protected function tailFile($file, $lines) 65 | { 66 | $handle = fopen($file, "r"); 67 | $linecounter = $lines; 68 | $pos = -2; 69 | $beginning = false; 70 | $text = []; 71 | while ($linecounter > 0) { 72 | $t = " "; 73 | while ($t != "\n") { 74 | if (fseek($handle, $pos, SEEK_END) == -1) { 75 | $beginning = true; 76 | break; 77 | } 78 | $t = fgetc($handle); 79 | $pos--; 80 | } 81 | $linecounter--; 82 | if ($beginning) { 83 | rewind($handle); 84 | } 85 | $text[$lines - $linecounter - 1] = fgets($handle); 86 | if ($beginning) { 87 | break; 88 | } 89 | } 90 | fclose($handle); 91 | return array_reverse($text); 92 | } 93 | 94 | /** 95 | * Search a string for log entries 96 | * Based on https://github.com/mikemand/logviewer/blob/master/src/Kmd/Logviewer/Logviewer.php by mikemand 97 | * 98 | * @param $file 99 | * @return array 100 | */ 101 | public function getLogs($file) 102 | { 103 | $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])*/"; 104 | 105 | $log_levels = $this->getLevels(); 106 | 107 | // There has GOT to be a better way of doing this... 108 | preg_match_all($pattern, $file, $headings); 109 | $log_data = preg_split($pattern, $file) ?: []; 110 | 111 | $log = []; 112 | foreach ($headings as $h) { 113 | for ($i = 0, $j = count($h); $i < $j; $i++) { 114 | foreach ($log_levels as $ll) { 115 | if (strpos(strtolower($h[$i]), strtolower('.' . $ll))) { 116 | $log[] = ['level' => $ll, 'header' => $h[$i], 'stack' => $log_data[$i] ?? '']; 117 | } 118 | } 119 | } 120 | } 121 | 122 | return $log; 123 | } 124 | 125 | /** 126 | * @return array 127 | */ 128 | public function getMessages() 129 | { 130 | return array_reverse(parent::getMessages()); 131 | } 132 | 133 | /** 134 | * Get the log levels from psr/log. 135 | * Based on https://github.com/mikemand/logviewer/blob/master/src/Kmd/Logviewer/Logviewer.php by mikemand 136 | * 137 | * @access public 138 | * @return array 139 | */ 140 | public function getLevels() 141 | { 142 | $class = new ReflectionClass(new LogLevel()); 143 | return $class->getConstants(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/DataCollector/ModelsCollector.php: -------------------------------------------------------------------------------- 1 | listen('eloquent.retrieved:*', function ($event, $models) { 25 | foreach (array_filter($models) as $model) { 26 | $class = get_class($model); 27 | $this->models[$class] = ($this->models[$class] ?? 0) + 1; 28 | $this->count++; 29 | } 30 | }); 31 | } 32 | 33 | public function collect() 34 | { 35 | ksort($this->models, SORT_NUMERIC); 36 | 37 | return ['data' => array_reverse($this->models), 'count' => $this->count]; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function getName() 44 | { 45 | return 'models'; 46 | } 47 | 48 | /** 49 | * {@inheritDoc} 50 | */ 51 | public function getWidgets() 52 | { 53 | return [ 54 | "models" => [ 55 | "icon" => "cubes", 56 | "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", 57 | "map" => "models.data", 58 | "default" => "{}" 59 | ], 60 | 'models:badge' => [ 61 | 'map' => 'models.count', 62 | 'default' => 0 63 | ] 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/DataCollector/MultiAuthCollector.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 38 | $this->guards = $guards; 39 | } 40 | 41 | /** 42 | * Set to show the users name/email 43 | * @param bool $showName 44 | */ 45 | public function setShowName($showName) 46 | { 47 | $this->showName = (bool) $showName; 48 | } 49 | 50 | /** 51 | * Set to hide the guards tab, and show only name 52 | * @param bool $showGuardsData 53 | */ 54 | public function setShowGuardsData($showGuardsData) 55 | { 56 | $this->showGuardsData = (bool) $showGuardsData; 57 | } 58 | 59 | /** 60 | * @{inheritDoc} 61 | */ 62 | public function collect() 63 | { 64 | $data = [ 65 | 'guards' => [], 66 | ]; 67 | $names = ''; 68 | 69 | foreach ($this->guards as $guardName => $config) { 70 | try { 71 | $guard = $this->auth->guard($guardName); 72 | if ($this->hasUser($guard)) { 73 | $user = $guard->user(); 74 | 75 | if (!is_null($user)) { 76 | $data['guards'][$guardName] = $this->getUserInformation($user); 77 | $names .= $guardName . ": " . $data['guards'][$guardName]['name'] . ', '; 78 | } 79 | } else { 80 | $data['guards'][$guardName] = null; 81 | } 82 | } catch (\Exception $e) { 83 | continue; 84 | } 85 | } 86 | 87 | foreach ($data['guards'] as $key => $var) { 88 | if (!is_string($data['guards'][$key])) { 89 | $data['guards'][$key] = $this->formatVar($var); 90 | } 91 | } 92 | 93 | $data['names'] = rtrim($names, ', '); 94 | if (!$this->showGuardsData) { 95 | unset($data['guards']); 96 | } 97 | 98 | return $data; 99 | } 100 | 101 | private function hasUser(Guard $guard) 102 | { 103 | if (method_exists($guard, 'hasUser')) { 104 | return $guard->hasUser(); 105 | } 106 | 107 | return false; 108 | } 109 | 110 | /** 111 | * Get displayed user information 112 | * @param \Illuminate\Auth\UserInterface $user 113 | * @return array 114 | */ 115 | protected function getUserInformation($user = null) 116 | { 117 | // Defaults 118 | if (is_null($user)) { 119 | return [ 120 | 'name' => 'Guest', 121 | 'user' => ['guest' => true], 122 | ]; 123 | } 124 | 125 | // The default auth identifer is the ID number, which isn't all that 126 | // useful. Try username, email and name. 127 | $identifier = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user->getKey(); 128 | if (is_numeric($identifier) || Str::isUuid($identifier) || Str::isUlid($identifier)) { 129 | try { 130 | if (isset($user->username)) { 131 | $identifier = $user->username; 132 | } elseif (isset($user->email)) { 133 | $identifier = $user->email; 134 | } elseif (isset($user->name)) { 135 | $identifier = Str::limit($user->name, 24); 136 | } 137 | } catch (\Throwable $e) { 138 | } 139 | } 140 | 141 | return [ 142 | 'name' => $identifier, 143 | 'user' => $user instanceof Arrayable ? $user->toArray() : $user, 144 | ]; 145 | } 146 | 147 | /** 148 | * @{inheritDoc} 149 | */ 150 | public function getName() 151 | { 152 | return 'auth'; 153 | } 154 | 155 | /** 156 | * @{inheritDoc} 157 | */ 158 | public function getWidgets() 159 | { 160 | $widgets = []; 161 | 162 | if ($this->showGuardsData) { 163 | $widgets["auth"] = [ 164 | "icon" => "lock", 165 | "widget" => "PhpDebugBar.Widgets.VariableListWidget", 166 | "map" => "auth.guards", 167 | "default" => "{}", 168 | ]; 169 | } 170 | 171 | if ($this->showName) { 172 | $widgets['auth.name'] = [ 173 | 'icon' => 'user', 174 | 'tooltip' => 'Auth status', 175 | 'map' => 'auth.names', 176 | 'default' => '', 177 | ]; 178 | } 179 | 180 | return $widgets; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/DataCollector/PennantCollector.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function collect() 29 | { 30 | $store = $this->manager->store(Config::get('pennant.default')); 31 | 32 | return $store->values($store->stored()); 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public function getName() 39 | { 40 | return 'pennant'; 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function getWidgets() 47 | { 48 | return [ 49 | "pennant" => [ 50 | "icon" => "flag", 51 | "widget" => "PhpDebugBar.Widgets.VariableListWidget", 52 | "map" => "pennant", 53 | "default" => "{}" 54 | ] 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DataCollector/QueryCollector.php: -------------------------------------------------------------------------------- 1 | timeCollector = $timeCollector; 49 | } 50 | 51 | /** 52 | * @param int|null $softLimit After the soft limit, no parameters/backtrace are captured 53 | * @param int|null $hardLimit After the hard limit, queries are ignored 54 | * @return void 55 | */ 56 | public function setLimits(?int $softLimit, ?int $hardLimit): void 57 | { 58 | $this->softLimit = $softLimit; 59 | $this->hardLimit = $hardLimit; 60 | } 61 | 62 | /** 63 | * Renders the SQL of traced statements with params embedded 64 | * 65 | * @param boolean $enabled 66 | * @param string $quotationChar NOT USED 67 | */ 68 | public function setRenderSqlWithParams($enabled = true, $quotationChar = "'") 69 | { 70 | $this->renderSqlWithParams = $enabled; 71 | } 72 | 73 | /** 74 | * Show or hide the hints in the parameters 75 | * 76 | * @param boolean $enabled 77 | */ 78 | public function setShowHints($enabled = true) 79 | { 80 | $this->showHints = $enabled; 81 | } 82 | 83 | /** 84 | * Show or hide copy button next to the queries 85 | * 86 | * @param boolean $enabled 87 | */ 88 | public function setShowCopyButton($enabled = true) 89 | { 90 | $this->showCopyButton = $enabled; 91 | } 92 | 93 | /** 94 | * Enable/disable finding the source 95 | * 96 | * @param bool|int $value 97 | * @param array $middleware 98 | */ 99 | public function setFindSource($value, array $middleware) 100 | { 101 | $this->findSource = $value; 102 | $this->middleware = $middleware; 103 | } 104 | 105 | public function mergeExcludePaths(array $excludePaths) 106 | { 107 | $this->excludePaths = array_merge($this->excludePaths, $excludePaths); 108 | } 109 | 110 | /** 111 | * Set additional paths to exclude from the backtrace 112 | * 113 | * @param array $excludePaths Array of file paths to exclude from backtrace 114 | */ 115 | public function mergeBacktraceExcludePaths(array $excludePaths) 116 | { 117 | $this->backtraceExcludePaths = array_merge($this->backtraceExcludePaths, $excludePaths); 118 | } 119 | 120 | /** 121 | * Enable/disable the shaded duration background on queries 122 | * 123 | * @param bool $enabled 124 | */ 125 | public function setDurationBackground($enabled = true) 126 | { 127 | $this->durationBackground = $enabled; 128 | } 129 | 130 | /** 131 | * Enable/disable the EXPLAIN queries 132 | * 133 | * @param bool $enabled 134 | * @param array|null $types Array of types to explain queries (select/insert/update/delete) 135 | */ 136 | public function setExplainSource($enabled, $types) 137 | { 138 | $this->explainQuery = $enabled; 139 | } 140 | 141 | public function startMemoryUsage() 142 | { 143 | $this->lastMemoryUsage = memory_get_usage(false); 144 | } 145 | 146 | /** 147 | * 148 | * @param \Illuminate\Database\Events\QueryExecuted $query 149 | */ 150 | public function addQuery($query) 151 | { 152 | $this->queryCount++; 153 | 154 | if ($this->hardLimit && $this->queryCount > $this->hardLimit) { 155 | return; 156 | } 157 | 158 | $limited = $this->softLimit && $this->queryCount > $this->softLimit; 159 | 160 | $sql = (string) $query->sql; 161 | $time = $query->time / 1000; 162 | $endTime = microtime(true); 163 | $startTime = $endTime - $time; 164 | $hints = $this->performQueryAnalysis($sql); 165 | 166 | $pdo = null; 167 | try { 168 | $pdo = $query->connection->getPdo(); 169 | 170 | if(! ($pdo instanceof \PDO)) { 171 | $pdo = null; 172 | } 173 | } catch (\Throwable $e) { 174 | // ignore error for non-pdo laravel drivers 175 | } 176 | 177 | $source = []; 178 | 179 | if (!$limited && $this->findSource) { 180 | try { 181 | $source = $this->findSource(); 182 | } catch (\Exception $e) { 183 | } 184 | } 185 | 186 | $bindings = match (true) { 187 | $limited && filled($query->bindings) => [], 188 | default => $query->connection->prepareBindings($query->bindings), 189 | }; 190 | 191 | $this->queries[] = [ 192 | 'query' => $sql, 193 | 'type' => 'query', 194 | 'bindings' => $bindings, 195 | 'start' => $startTime, 196 | 'time' => $time, 197 | 'memory' => $this->lastMemoryUsage ? memory_get_usage(false) - $this->lastMemoryUsage : 0, 198 | 'source' => $source, 199 | 'connection' => $query->connection, 200 | 'driver' => $query->connection->getConfig('driver'), 201 | 'hints' => ($this->showHints && !$limited) ? $hints : null, 202 | 'show_copy' => $this->showCopyButton, 203 | ]; 204 | 205 | if ($this->timeCollector !== null) { 206 | $this->timeCollector->addMeasure(Str::limit($sql, 100), $startTime, $endTime, [], 'db', 'Database Query'); 207 | } 208 | } 209 | 210 | /** 211 | * Mimic mysql_real_escape_string 212 | * 213 | * @param string $value 214 | * @return string 215 | */ 216 | protected function emulateQuote($value) 217 | { 218 | $search = ["\\", "\x00", "\n", "\r", "'", '"', "\x1a"]; 219 | $replace = ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"]; 220 | 221 | return "'" . str_replace($search, $replace, (string) $value) . "'"; 222 | } 223 | 224 | /** 225 | * Explainer::performQueryAnalysis() 226 | * 227 | * Perform simple regex analysis on the code 228 | * 229 | * @package xplain (https://github.com/rap2hpoutre/mysql-xplain-xplain) 230 | * @author e-doceo 231 | * @copyright 2014 232 | * @version $Id$ 233 | * @access public 234 | * @param string $query 235 | * @return string[] 236 | */ 237 | protected function performQueryAnalysis($query) 238 | { 239 | // @codingStandardsIgnoreStart 240 | $hints = []; 241 | if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $query)) { 242 | $hints[] = 'Use SELECT * only if you need all columns from table'; 243 | } 244 | if (preg_match('/ORDER BY RAND()/i', $query)) { 245 | $hints[] = 'ORDER BY RAND() is slow, try to avoid if you can. 246 | You can read this 247 | or this'; 248 | } 249 | if (strpos($query, '!=') !== false) { 250 | $hints[] = 'The != operator is not standard. Use the <> operator to test for inequality instead.'; 251 | } 252 | if (stripos($query, 'WHERE') === false && preg_match('/^(SELECT) /i', $query)) { 253 | $hints[] = 'The SELECT statement has no WHERE clause and could examine many more rows than intended'; 254 | } 255 | if (preg_match('/LIMIT\\s/i', $query) && stripos($query, 'ORDER BY') === false) { 256 | $hints[] = 'LIMIT without ORDER BY causes non-deterministic results, depending on the query execution plan'; 257 | } 258 | if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $query, $matches)) { 259 | $hints[] = 'An argument has a leading wildcard character: ' . $matches[1] . '. 260 | The predicate with this argument is not sargable and cannot use an index if one exists.'; 261 | } 262 | return $hints; 263 | 264 | // @codingStandardsIgnoreEnd 265 | } 266 | 267 | /** 268 | * Use a backtrace to search for the origins of the query. 269 | * 270 | * @return array 271 | */ 272 | protected function findSource() 273 | { 274 | $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, app('config')->get('debugbar.debug_backtrace_limit', 50)); 275 | 276 | $sources = []; 277 | 278 | foreach ($stack as $index => $trace) { 279 | $sources[] = $this->parseTrace($index, $trace); 280 | } 281 | 282 | return array_slice(array_filter($sources), 0, is_int($this->findSource) ? $this->findSource : 5); 283 | } 284 | 285 | /** 286 | * Parse a trace element from the backtrace stack. 287 | * 288 | * @param int $index 289 | * @param array $trace 290 | * @return object|bool 291 | */ 292 | protected function parseTrace($index, array $trace) 293 | { 294 | $frame = (object) [ 295 | 'index' => $index, 296 | 'namespace' => null, 297 | 'name' => null, 298 | 'file' => null, 299 | 'line' => $trace['line'] ?? '1', 300 | ]; 301 | 302 | if (isset($trace['function']) && $trace['function'] == 'substituteBindings') { 303 | $frame->name = 'Route binding'; 304 | 305 | return $frame; 306 | } 307 | 308 | if ( 309 | isset($trace['class']) && 310 | isset($trace['file']) && 311 | !$this->fileIsInExcludedPath($trace['file']) 312 | ) { 313 | $frame->file = $trace['file']; 314 | 315 | if (isset($trace['object']) && is_a($trace['object'], '\Twig\Template')) { 316 | list($frame->file, $frame->line) = $this->getTwigInfo($trace); 317 | } elseif (strpos($frame->file, storage_path()) !== false) { 318 | $hash = pathinfo($frame->file, PATHINFO_FILENAME); 319 | 320 | if ($frame->name = $this->findViewFromHash($hash)) { 321 | $frame->file = $frame->name[1]; 322 | $frame->name = $frame->name[0]; 323 | } else { 324 | $frame->name = $hash; 325 | } 326 | 327 | $frame->namespace = 'view'; 328 | 329 | return $frame; 330 | } elseif (strpos($frame->file, 'Middleware') !== false) { 331 | $frame->name = $this->findMiddlewareFromFile($frame->file); 332 | 333 | if ($frame->name) { 334 | $frame->namespace = 'middleware'; 335 | } else { 336 | $frame->name = $this->normalizeFilePath($frame->file); 337 | } 338 | 339 | return $frame; 340 | } 341 | 342 | $frame->name = $this->normalizeFilePath($frame->file); 343 | 344 | return $frame; 345 | } 346 | 347 | 348 | return false; 349 | } 350 | 351 | /** 352 | * Check if the given file is to be excluded from analysis 353 | * 354 | * @param string $file 355 | * @return bool 356 | */ 357 | protected function fileIsInExcludedPath($file) 358 | { 359 | $normalizedPath = str_replace('\\', '/', $file); 360 | 361 | foreach ($this->backtraceExcludePaths as $excludedPath) { 362 | if (strpos($normalizedPath, $excludedPath) !== false) { 363 | return true; 364 | } 365 | } 366 | 367 | return false; 368 | } 369 | 370 | /** 371 | * Find the middleware alias from the file. 372 | * 373 | * @param string $file 374 | * @return string|null 375 | */ 376 | protected function findMiddlewareFromFile($file) 377 | { 378 | $filename = pathinfo($file, PATHINFO_FILENAME); 379 | 380 | foreach ($this->middleware as $alias => $class) { 381 | if (!is_null($class) && !is_null($filename) && strpos($class, $filename) !== false) { 382 | return $alias; 383 | } 384 | } 385 | } 386 | 387 | /** 388 | * Find the template name from the hash. 389 | * 390 | * @param string $hash 391 | * @return null|array 392 | */ 393 | protected function findViewFromHash($hash) 394 | { 395 | $finder = app('view')->getFinder(); 396 | 397 | if (isset($this->reflection['viewfinderViews'])) { 398 | $property = $this->reflection['viewfinderViews']; 399 | } else { 400 | $reflection = new \ReflectionClass($finder); 401 | $property = $reflection->getProperty('views'); 402 | $property->setAccessible(true); 403 | $this->reflection['viewfinderViews'] = $property; 404 | } 405 | 406 | $xxh128Exists = in_array('xxh128', hash_algos()); 407 | 408 | foreach ($property->getValue($finder) as $name => $path) { 409 | if (($xxh128Exists && hash('xxh128', 'v2' . $path) == $hash) || sha1('v2' . $path) == $hash) { 410 | return [$name, $path]; 411 | } 412 | } 413 | } 414 | 415 | /** 416 | * Get the filename/line from a Twig template trace 417 | * 418 | * @param array $trace 419 | * @return array The file and line 420 | */ 421 | protected function getTwigInfo($trace) 422 | { 423 | $file = $trace['object']->getTemplateName(); 424 | 425 | if (isset($trace['line'])) { 426 | foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) { 427 | if ($codeLine <= $trace['line']) { 428 | return [$file, $templateLine]; 429 | } 430 | } 431 | } 432 | 433 | return [$file, -1]; 434 | } 435 | 436 | /** 437 | * Collect a database transaction event. 438 | * @param string $event 439 | * @param \Illuminate\Database\Connection $connection 440 | * @return array 441 | */ 442 | public function collectTransactionEvent($event, $connection) 443 | { 444 | $this->transactionEventsCount++; 445 | $source = []; 446 | 447 | if ($this->findSource) { 448 | try { 449 | $source = $this->findSource(); 450 | } catch (\Exception $e) { 451 | } 452 | } 453 | 454 | $this->queries[] = [ 455 | 'query' => $event, 456 | 'type' => 'transaction', 457 | 'bindings' => [], 458 | 'start' => microtime(true), 459 | 'time' => 0, 460 | 'memory' => 0, 461 | 'source' => $source, 462 | 'connection' => $connection, 463 | 'driver' => $connection->getConfig('driver'), 464 | 'hints' => null, 465 | 'show_copy' => false, 466 | ]; 467 | } 468 | 469 | /** 470 | * Reset the queries. 471 | */ 472 | public function reset() 473 | { 474 | $this->queries = []; 475 | $this->queryCount = 0; 476 | $this->infoStatements = 0 ; 477 | } 478 | 479 | /** 480 | * {@inheritDoc} 481 | */ 482 | public function collect() 483 | { 484 | $totalTime = 0; 485 | $totalMemory = 0; 486 | $queries = $this->queries; 487 | 488 | $statements = []; 489 | foreach ($queries as $query) { 490 | $source = reset($query['source']); 491 | $normalizedPath = is_object($source) ? $this->normalizeFilePath($source->file ?: '') : ''; 492 | if ($query['type'] != 'transaction' && Str::startsWith($normalizedPath, $this->excludePaths)) { 493 | continue; 494 | } 495 | 496 | $totalTime += $query['time']; 497 | $totalMemory += $query['memory']; 498 | 499 | $connectionName = $query['connection']->getDatabaseName(); 500 | if (str_ends_with($connectionName, '.sqlite')) { 501 | $connectionName = $this->normalizeFilePath($connectionName); 502 | } 503 | 504 | $canExplainQuery = match (true) { 505 | in_array($query['driver'], ['mariadb', 'mysql', 'pgsql']) => $query['bindings'] !== null && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $query['query']), 506 | default => false, 507 | }; 508 | 509 | $statements[] = [ 510 | 'sql' => $this->getSqlQueryToDisplay($query), 511 | 'type' => $query['type'], 512 | 'params' => [], 513 | 'bindings' => $query['bindings'] ?? [], 514 | 'hints' => $query['hints'], 515 | 'show_copy' => $query['show_copy'], 516 | 'backtrace' => array_values($query['source']), 517 | 'start' => $query['start'] ?? null, 518 | 'duration' => $query['time'], 519 | 'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']), 520 | 'memory' => $query['memory'], 521 | 'memory_str' => $query['memory'] ? $this->getDataFormatter()->formatBytes($query['memory']) : null, 522 | 'filename' => $this->getDataFormatter()->formatSource($source, true), 523 | 'source' => $source, 524 | 'xdebug_link' => is_object($source) ? $this->getXdebugLink($source->file ?: '', $source->line) : null, 525 | 'connection' => $connectionName, 526 | 'explain' => $this->explainQuery && $canExplainQuery ? [ 527 | 'url' => route('debugbar.queries.explain'), 528 | 'driver' => $query['driver'], 529 | 'connection' => $query['connection']->getName(), 530 | 'query' => $query['query'], 531 | 'hash' => (new Explain())->hash($query['connection']->getName(), $query['query'], $query['bindings']), 532 | ] : null, 533 | ]; 534 | } 535 | 536 | if ($this->durationBackground) { 537 | if ($totalTime > 0) { 538 | // For showing background measure on Queries tab 539 | $start_percent = 0; 540 | 541 | foreach ($statements as $i => $statement) { 542 | if (!isset($statement['duration'])) { 543 | continue; 544 | } 545 | 546 | $width_percent = $statement['duration'] / $totalTime * 100; 547 | 548 | $statements[$i] = array_merge($statement, [ 549 | 'start_percent' => round($start_percent, 3), 550 | 'width_percent' => round($width_percent, 3), 551 | ]); 552 | 553 | $start_percent += $width_percent; 554 | } 555 | } 556 | } 557 | 558 | if ($this->softLimit && $this->hardLimit && ($this->queryCount > $this->softLimit && $this->queryCount > $this->hardLimit)) { 559 | array_unshift($statements, [ 560 | '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).', 561 | 'type' => 'info', 562 | ]); 563 | $statements[] = [ 564 | '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)', 565 | 'type' => 'info', 566 | ]; 567 | $this->infoStatements+= 2; 568 | } elseif ($this->hardLimit && $this->queryCount > $this->hardLimit) { 569 | array_unshift($statements, [ 570 | '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)', 571 | 'type' => 'info', 572 | ]); 573 | $statements[] = [ 574 | '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)', 575 | 'type' => 'info', 576 | ]; 577 | $this->infoStatements+= 2; 578 | } elseif ($this->softLimit && $this->queryCount > $this->softLimit) { 579 | array_unshift($statements, [ 580 | '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)', 581 | 'type' => 'info', 582 | ]); 583 | $this->infoStatements++; 584 | } 585 | 586 | $visibleStatements = count($statements) - $this->infoStatements; 587 | 588 | $data = [ 589 | 'count' => $visibleStatements, 590 | 'nb_statements' => $this->queryCount, 591 | 'nb_visible_statements' => $visibleStatements, 592 | 'nb_excluded_statements' => $this->queryCount + $this->transactionEventsCount - $visibleStatements, 593 | 'nb_failed_statements' => 0, 594 | 'accumulated_duration' => $totalTime, 595 | 'accumulated_duration_str' => $this->formatDuration($totalTime), 596 | 'memory_usage' => $totalMemory, 597 | 'memory_usage_str' => $totalMemory ? $this->getDataFormatter()->formatBytes($totalMemory) : null, 598 | 'statements' => $statements 599 | ]; 600 | return $data; 601 | } 602 | 603 | /** 604 | * {@inheritDoc} 605 | */ 606 | public function getName() 607 | { 608 | return 'queries'; 609 | } 610 | 611 | /** 612 | * {@inheritDoc} 613 | */ 614 | public function getWidgets() 615 | { 616 | return [ 617 | "queries" => [ 618 | "icon" => "database", 619 | "widget" => "PhpDebugBar.Widgets.LaravelQueriesWidget", 620 | "map" => "queries", 621 | "default" => "[]" 622 | ], 623 | "queries:badge" => [ 624 | "map" => "queries.nb_statements", 625 | "default" => 0 626 | ] 627 | ]; 628 | } 629 | 630 | private function getSqlQueryToDisplay(array $query): string 631 | { 632 | $sql = $query['query']; 633 | if ($query['type'] === 'query' && $this->renderSqlWithParams && $query['connection']->getQueryGrammar() instanceof \Illuminate\Database\Query\Grammars\Grammar && method_exists($query['connection']->getQueryGrammar(), 'substituteBindingsIntoRawSql')) { 634 | try { 635 | $sql = $query['connection']->getQueryGrammar()->substituteBindingsIntoRawSql($sql, $query['bindings'] ?? []); 636 | return $this->getDataFormatter()->formatSql($sql); 637 | } catch (\Throwable $e) { 638 | // Continue using the old substitute 639 | } 640 | } 641 | 642 | if ($query['type'] === 'query' && $this->renderSqlWithParams) { 643 | $bindings = $this->getDataFormatter()->checkBindings($query['bindings']); 644 | if (!empty($bindings)) { 645 | $pdo = null; 646 | try { 647 | $pdo = $query->connection->getPdo(); 648 | } catch (\Throwable) { 649 | // ignore error for non-pdo laravel drivers 650 | } 651 | 652 | foreach ($bindings as $key => $binding) { 653 | // This regex matches placeholders only, not the question marks, 654 | // nested in quotes, while we iterate through the bindings 655 | // and substitute placeholders by suitable values. 656 | $regex = is_numeric($key) 657 | ? "/(?quote((string) $binding); 665 | } catch (\Exception $e) { 666 | $binding = $this->emulateQuote($binding); 667 | } 668 | } else { 669 | $binding = $this->emulateQuote($binding); 670 | } 671 | } 672 | 673 | $sql = preg_replace($regex, addcslashes($binding, '$'), $sql, 1); 674 | } 675 | } 676 | } 677 | 678 | return $this->getDataFormatter()->formatSql($sql); 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /src/DataCollector/RequestCollector.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | */ 22 | class RequestCollector extends DataCollector implements DataCollectorInterface, Renderable 23 | { 24 | /** @var \Symfony\Component\HttpFoundation\Request $request */ 25 | protected $request; 26 | /** @var \Symfony\Component\HttpFoundation\Response $response */ 27 | protected $response; 28 | /** @var \Symfony\Component\HttpFoundation\Session\SessionInterface $session */ 29 | protected $session; 30 | /** @var string|null */ 31 | protected $currentRequestId; 32 | /** @var array */ 33 | protected $hiddens; 34 | 35 | /** 36 | * Create a new SymfonyRequestCollector 37 | * 38 | * @param \Symfony\Component\HttpFoundation\Request $request 39 | * @param \Symfony\Component\HttpFoundation\Response $response 40 | * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session 41 | * @param string|null $currentRequestId 42 | * @param array $hiddens 43 | */ 44 | public function __construct($request, $response, $session = null, $currentRequestId = null, $hiddens = []) 45 | { 46 | $this->request = $request; 47 | $this->response = $response; 48 | $this->session = $session; 49 | $this->currentRequestId = $currentRequestId; 50 | $this->hiddens = array_merge($hiddens, [ 51 | 'request_request.password', 52 | 'request_headers.php-auth-pw.0', 53 | ]); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | public function getName() 60 | { 61 | return 'request'; 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function getWidgets() 68 | { 69 | $widgets = [ 70 | "request" => [ 71 | "icon" => "tags", 72 | "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", 73 | "map" => "request.data", 74 | "order" => -100, 75 | "default" => "{}" 76 | ], 77 | 'request:badge' => [ 78 | "map" => "request.badge", 79 | "default" => "null" 80 | ] 81 | ]; 82 | 83 | if (Config::get('debugbar.options.request.label', true)) { 84 | $widgets['currentrequest'] = [ 85 | "icon" => "share", 86 | "map" => "request.data.uri", 87 | "link" => "request", 88 | "default" => "" 89 | ]; 90 | $widgets['currentrequest:tooltip'] = [ 91 | "map" => "request.tooltip", 92 | "default" => "{}" 93 | ]; 94 | } 95 | 96 | return $widgets; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function collect() 103 | { 104 | $request = $this->request; 105 | $response = $this->response; 106 | 107 | $responseHeaders = $response->headers->all(); 108 | $cookies = []; 109 | foreach ($response->headers->getCookies() as $cookie) { 110 | $cookies[] = $this->getCookieHeader( 111 | $cookie->getName(), 112 | $cookie->getValue(), 113 | $cookie->getExpiresTime(), 114 | $cookie->getPath(), 115 | $cookie->getDomain(), 116 | $cookie->isSecure(), 117 | $cookie->isHttpOnly() 118 | ); 119 | } 120 | if (count($cookies) > 0) { 121 | $responseHeaders['Set-Cookie'] = $cookies; 122 | } 123 | 124 | $statusCode = $response->getStatusCode(); 125 | $startTime = defined('LARAVEL_START') ? LARAVEL_START : $request->server->get('REQUEST_TIME_FLOAT'); 126 | $query = $request->getQueryString(); 127 | $htmlData = []; 128 | 129 | $data = [ 130 | 'status' => $statusCode . ' ' . (isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : ''), 131 | 'duration' => $startTime ? $this->formatDuration(microtime(true) - $startTime) : null, 132 | 'peak_memory' => $this->formatBytes(memory_get_peak_usage(true), 1), 133 | ]; 134 | 135 | if ($request instanceof Request) { 136 | 137 | if ($route = $request->route()) { 138 | $htmlData += $this->getRouteInformation($route); 139 | } 140 | 141 | $fulLUrl = $request->fullUrl(); 142 | $data += [ 143 | 'full_url' => strlen($fulLUrl) > 100 ? [$fulLUrl] : $fulLUrl, 144 | ]; 145 | } 146 | 147 | if ($response instanceof RedirectResponse) { 148 | $data['response'] = 'Redirect to ' . $response->getTargetUrl(); 149 | } 150 | 151 | $data += [ 152 | 'response' => $response->headers->get('Content-Type') ? $response->headers->get( 153 | 'Content-Type' 154 | ) : 'text/html', 155 | 'request_format' => $request->getRequestFormat(), 156 | 'request_query' => $request->query->all(), 157 | 'request_request' => $request->request->all(), 158 | 'request_headers' => $request->headers->all(), 159 | 'request_cookies' => $request->cookies->all(), 160 | 'response_headers' => $responseHeaders, 161 | ]; 162 | 163 | if ($this->session) { 164 | $data['session_attributes'] = $this->session->all(); 165 | } 166 | 167 | if (isset($data['request_headers']['authorization'][0])) { 168 | $data['request_headers']['authorization'][0] = substr($data['request_headers']['authorization'][0], 0, 12) . '******'; 169 | } 170 | 171 | foreach ($this->hiddens as $key) { 172 | if (Arr::has($data, $key)) { 173 | Arr::set($data, $key, '******'); 174 | } 175 | } 176 | 177 | foreach ($data as $key => $var) { 178 | if (!is_string($data[$key])) { 179 | $data[$key] = DataCollector::getDefaultVarDumper()->renderVar($var); 180 | } else { 181 | $data[$key] = e($data[$key]); 182 | } 183 | } 184 | 185 | if (class_exists(Telescope::class)) { 186 | $entry = IncomingEntry::make([ 187 | 'requestId' => $this->currentRequestId, 188 | ])->type('debugbar'); 189 | Telescope::$entriesQueue[] = $entry; 190 | $url = route('debugbar.telescope', [$entry->uuid]); 191 | $htmlData['telescope'] = 'View in Telescope'; 192 | } 193 | 194 | $tooltip = [ 195 | 'status' => $data['status'], 196 | 'full_url' => Str::limit($request->fullUrl(), 100), 197 | ]; 198 | 199 | if ($this->request instanceof Request) { 200 | $tooltip += [ 201 | 'action_name' => optional($this->request->route())->getName(), 202 | 'controller_action' => optional($this->request->route())->getActionName(), 203 | ]; 204 | } 205 | 206 | unset($htmlData['as'], $htmlData['uses']); 207 | 208 | return [ 209 | 'data' => $tooltip + $htmlData + $data, 210 | 'tooltip' => array_filter($tooltip), 211 | 'badge' => $statusCode >= 300 ? $data['status'] : null, 212 | ]; 213 | } 214 | 215 | protected function getRouteInformation($route) 216 | { 217 | if (!is_a($route, 'Illuminate\Routing\Route')) { 218 | return []; 219 | } 220 | $uri = head($route->methods()) . ' ' . $route->uri(); 221 | $action = $route->getAction(); 222 | 223 | $result = [ 224 | 'uri' => $uri ?: '-', 225 | ]; 226 | 227 | $result = array_merge($result, $action); 228 | $uses = $action['uses'] ?? null; 229 | $controller = is_string($action['controller'] ?? null) ? $action['controller'] : ''; 230 | 231 | if (request()->hasHeader('X-Livewire')) { 232 | try { 233 | $component = request('components')[0]; 234 | $name = json_decode($component['snapshot'], true)['memo']['name']; 235 | $method = $component['calls'][0]['method']; 236 | $class = app(\Livewire\Mechanisms\ComponentRegistry::class)->getClass($name); 237 | if (class_exists($class) && method_exists($class, $method)) { 238 | $controller = $class . '@' . $method; 239 | $result['controller'] = ltrim($controller, '\\'); 240 | } 241 | } catch (\Throwable $e) { 242 | // 243 | } 244 | } 245 | 246 | if (str_contains($controller, '@')) { 247 | list($controller, $method) = explode('@', $controller); 248 | if (class_exists($controller) && method_exists($controller, $method)) { 249 | $reflector = new \ReflectionMethod($controller, $method); 250 | } 251 | unset($result['uses']); 252 | } elseif ($uses instanceof \Closure) { 253 | $reflector = new \ReflectionFunction($uses); 254 | $result['uses'] = $this->formatVar($uses); 255 | } elseif (is_string($uses) && str_contains($uses, '@__invoke')) { 256 | if (class_exists($controller) && method_exists($controller, 'render')) { 257 | $reflector = new \ReflectionMethod($controller, 'render'); 258 | $result['controller'] = $controller . '@render'; 259 | } 260 | } 261 | 262 | if (isset($reflector)) { 263 | $filename = $this->normalizeFilePath($reflector->getFileName()); 264 | 265 | if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { 266 | $result['file'] = sprintf( 267 | '%s:%s-%s', 268 | $link['url'], 269 | $link['ajax'] ? 'event.preventDefault();$.ajax(this.href);' : '', 270 | $filename, 271 | $reflector->getStartLine(), 272 | $reflector->getEndLine() 273 | ); 274 | 275 | if (isset($result['controller']) && is_string($result['controller'])) { 276 | $result['controller'] .= ''; 277 | } 278 | } else { 279 | $result['file'] = sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()); 280 | } 281 | } 282 | 283 | if (isset($result['middleware']) && is_array($result['middleware'])) { 284 | $middleware = implode(', ', $result['middleware']); 285 | unset($result['middleware']); 286 | $result['middleware'] = $middleware; 287 | } 288 | 289 | return array_filter($result); 290 | } 291 | 292 | private function getCookieHeader($name, $value, $expires, $path, $domain, $secure, $httponly) 293 | { 294 | $cookie = sprintf('%s=%s', $name, urlencode($value ?? '')); 295 | 296 | if (0 !== $expires) { 297 | if (is_numeric($expires)) { 298 | $expires = (int) $expires; 299 | } elseif ($expires instanceof \DateTime) { 300 | $expires = $expires->getTimestamp(); 301 | } else { 302 | $expires = strtotime($expires); 303 | if (false === $expires || -1 == $expires) { 304 | throw new \InvalidArgumentException( 305 | sprintf('The "expires" cookie parameter is not valid.', $expires) 306 | ); 307 | } 308 | } 309 | 310 | $cookie .= '; expires=' . substr( 311 | \DateTime::createFromFormat('U', $expires, new \DateTimeZone('UTC'))->format('D, d-M-Y H:i:s T'), 312 | 0, 313 | -5 314 | ); 315 | } 316 | 317 | if ($domain) { 318 | $cookie .= '; domain=' . $domain; 319 | } 320 | 321 | $cookie .= '; path=' . $path; 322 | 323 | if ($secure) { 324 | $cookie .= '; secure'; 325 | } 326 | 327 | if ($httponly) { 328 | $cookie .= '; httponly'; 329 | } 330 | 331 | return $cookie; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/DataCollector/RouteCollector.php: -------------------------------------------------------------------------------- 1 | router = $router; 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | public function collect() 34 | { 35 | $route = $this->router->current(); 36 | return $this->getRouteInformation($route); 37 | } 38 | 39 | /** 40 | * Get the route information for a given route. 41 | * 42 | * @param \Illuminate\Routing\Route $route 43 | * @return array 44 | */ 45 | protected function getRouteInformation($route) 46 | { 47 | if (!is_a($route, 'Illuminate\Routing\Route')) { 48 | return []; 49 | } 50 | $uri = head($route->methods()) . ' ' . $route->uri(); 51 | $action = $route->getAction(); 52 | 53 | $result = [ 54 | 'uri' => $uri ?: '-', 55 | ]; 56 | 57 | $result = array_merge($result, $action); 58 | $uses = $action['uses'] ?? null; 59 | $controller = is_string($action['controller'] ?? null) ? $action['controller'] : ''; 60 | 61 | if (request()->hasHeader('X-Livewire')) { 62 | try { 63 | $component = request('components')[0]; 64 | $name = json_decode($component['snapshot'], true)['memo']['name']; 65 | $method = $component['calls'][0]['method']; 66 | $class = app(\Livewire\Mechanisms\ComponentRegistry::class)->getClass($name); 67 | if (class_exists($class) && method_exists($class, $method)) { 68 | $controller = $class . '@' . $method; 69 | $result['controller'] = ltrim($controller, '\\'); 70 | } 71 | } catch (\Throwable $e) { 72 | // 73 | } 74 | } 75 | 76 | if (str_contains($controller, '@')) { 77 | list($controller, $method) = explode('@', $controller); 78 | if (class_exists($controller) && method_exists($controller, $method)) { 79 | $reflector = new \ReflectionMethod($controller, $method); 80 | } 81 | unset($result['uses']); 82 | } elseif ($uses instanceof \Closure) { 83 | $reflector = new \ReflectionFunction($uses); 84 | $result['uses'] = $this->formatVar($uses); 85 | } elseif (is_string($uses) && str_contains($uses, '@__invoke')) { 86 | if (class_exists($controller) && method_exists($controller, 'render')) { 87 | $reflector = new \ReflectionMethod($controller, 'render'); 88 | $result['controller'] = $controller . '@render'; 89 | } 90 | } 91 | 92 | if (isset($reflector)) { 93 | $filename = $this->normalizeFilePath($reflector->getFileName()); 94 | 95 | if ($link = $this->getXdebugLink($reflector->getFileName(), $reflector->getStartLine())) { 96 | $result['file'] = sprintf( 97 | '%s:%s-%s', 98 | $link['url'], 99 | $link['ajax'] ? 'event.preventDefault();$.ajax(this.href);' : '', 100 | $filename, 101 | $reflector->getStartLine(), 102 | $reflector->getEndLine() 103 | ); 104 | 105 | if (isset($result['controller'])) { 106 | $result['controller'] .= ''; 107 | } 108 | } else { 109 | $result['file'] = sprintf('%s:%s-%s', $filename, $reflector->getStartLine(), $reflector->getEndLine()); 110 | } 111 | } 112 | 113 | if ($middleware = $this->getMiddleware($route)) { 114 | $result['middleware'] = $middleware; 115 | } 116 | 117 | return array_filter($result); 118 | } 119 | 120 | /** 121 | * Get middleware 122 | * 123 | * @param \Illuminate\Routing\Route $route 124 | * @return string 125 | */ 126 | protected function getMiddleware($route) 127 | { 128 | return implode(', ', array_map(function ($middleware) { 129 | return $middleware instanceof Closure ? 'Closure' : $middleware; 130 | }, $route->gatherMiddleware())); 131 | } 132 | 133 | /** 134 | * {@inheritDoc} 135 | */ 136 | public function getName() 137 | { 138 | return 'route'; 139 | } 140 | 141 | /** 142 | * {@inheritDoc} 143 | */ 144 | public function getWidgets() 145 | { 146 | $widgets = [ 147 | "route" => [ 148 | "icon" => "share", 149 | "widget" => "PhpDebugBar.Widgets.HtmlVariableListWidget", 150 | "map" => "route", 151 | "default" => "{}" 152 | ] 153 | ]; 154 | return $widgets; 155 | } 156 | 157 | /** 158 | * Display the route information on the console. 159 | * 160 | * @param array $routes 161 | * @return void 162 | */ 163 | protected function displayRoutes(array $routes) 164 | { 165 | $this->table->setHeaders($this->headers)->setRows($routes); 166 | 167 | $this->table->render($this->getOutput()); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/DataCollector/SessionCollector.php: -------------------------------------------------------------------------------- 1 | session = $session; 26 | $this->hiddens = $hiddens; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function collect() 33 | { 34 | $data = $this->session->all(); 35 | 36 | foreach ($this->hiddens as $key) { 37 | if (Arr::has($data, $key)) { 38 | Arr::set($data, $key, '******'); 39 | } 40 | } 41 | 42 | foreach ($data as $key => $value) { 43 | $data[$key] = is_string($value) ? $value : $this->formatVar($value); 44 | } 45 | 46 | return $data; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function getName() 53 | { 54 | return 'session'; 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function getWidgets() 61 | { 62 | return [ 63 | "session" => [ 64 | "icon" => "archive", 65 | "widget" => "PhpDebugBar.Widgets.VariableListWidget", 66 | "map" => "session", 67 | "default" => "{}" 68 | ] 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/DataCollector/ViewCollector.php: -------------------------------------------------------------------------------- 1 | setDataFormatter(new SimpleFormatter()); 33 | $this->collect_data = $collectData; 34 | $this->templates = []; 35 | $this->exclude_paths = $excludePaths; 36 | $this->group = $group; 37 | $this->timeCollector = $timeCollector; 38 | } 39 | 40 | public function getName() 41 | { 42 | return 'views'; 43 | } 44 | 45 | public function getWidgets() 46 | { 47 | return [ 48 | 'views' => [ 49 | 'icon' => 'leaf', 50 | 'widget' => 'PhpDebugBar.Widgets.TemplatesWidget', 51 | 'map' => 'views', 52 | 'default' => '[]' 53 | ], 54 | 'views:badge' => [ 55 | 'map' => 'views.nb_templates', 56 | 'default' => 0 57 | ] 58 | ]; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function getAssets() 65 | { 66 | return [ 67 | 'css' => 'widgets/templates/widget.css', 68 | 'js' => 'widgets/templates/widget.js', 69 | ]; 70 | } 71 | 72 | /** 73 | * Add a View instance to the Collector 74 | * 75 | * @param \Illuminate\View\View $view 76 | */ 77 | public function addView(View $view) 78 | { 79 | $name = $view->getName(); 80 | $type = null; 81 | $data = $view->getData(); 82 | $path = $view->getPath(); 83 | 84 | if (class_exists('\Inertia\Inertia')) { 85 | list($name, $type, $data, $path) = $this->getInertiaView($name, $data, $path); 86 | } 87 | 88 | if (is_object($path)) { 89 | $type = get_class($view); 90 | $path = null; 91 | } 92 | 93 | if ($path) { 94 | if (!$type) { 95 | if (substr($path, -10) == '.blade.php') { 96 | $type = 'blade'; 97 | } else { 98 | $type = pathinfo($path, PATHINFO_EXTENSION); 99 | } 100 | } 101 | 102 | $shortPath = $this->normalizeFilePath($path); 103 | foreach ($this->exclude_paths as $excludePath) { 104 | if (str_starts_with($shortPath, $excludePath)) { 105 | return; 106 | } 107 | } 108 | } 109 | 110 | $this->addTemplate($name, $data, $type, $path); 111 | 112 | if ($this->timeCollector !== null) { 113 | $time = microtime(true); 114 | $this->timeCollector->addMeasure('View: ' . $name, $time, $time, [], 'views', 'View'); 115 | } 116 | } 117 | 118 | private function getInertiaView(string $name, array $data, ?string $path) 119 | { 120 | if (isset($data['page']) && is_array($data['page'])) { 121 | $data = $data['page']; 122 | } 123 | 124 | if (isset($data['props'], $data['component'])) { 125 | $name = $data['component']; 126 | $data = $data['props']; 127 | 128 | if ($files = glob(resource_path(config('debugbar.options.views.inertia_pages') .'/'. $name . '.*'))) { 129 | $path = $files[0]; 130 | $type = pathinfo($path, PATHINFO_EXTENSION); 131 | 132 | if (in_array($type, ['js', 'jsx'])) { 133 | $type = 'react'; 134 | } 135 | } 136 | } 137 | 138 | return [$name, $type ?? '', $data, $path]; 139 | } 140 | 141 | public function addInertiaAjaxView(array $data) 142 | { 143 | list($name, $type, $data, $path) = $this->getInertiaView('', $data, ''); 144 | 145 | if (! $name) { 146 | return; 147 | } 148 | 149 | $this->addTemplate($name, $data, $type, $path); 150 | } 151 | 152 | private function addTemplate(string $name, array $data, ?string $type, ?string $path) 153 | { 154 | // Prevent duplicates 155 | $hash = $type . $path . $name . ($this->collect_data ? implode(array_keys($data)) : ''); 156 | 157 | if ($this->collect_data === 'keys') { 158 | $params = array_keys($data); 159 | } elseif ($this->collect_data) { 160 | $params = array_map( 161 | fn ($value) => $this->getDataFormatter()->formatVar($value), 162 | $data 163 | ); 164 | } else { 165 | $params = []; 166 | } 167 | 168 | $template = [ 169 | 'name' => $name, 170 | 'param_count' => $this->collect_data ? count($params) : null, 171 | 'params' => $params, 172 | 'start' => microtime(true), 173 | 'type' => $type, 174 | 'hash' => $hash, 175 | ]; 176 | 177 | if ($path && $this->getXdebugLinkTemplate()) { 178 | $template['xdebug_link'] = $this->getXdebugLink($path); 179 | } 180 | 181 | $this->templates[] = $template; 182 | } 183 | 184 | public function collect() 185 | { 186 | if ($this->group === true || count($this->templates) > $this->group) { 187 | $templates = []; 188 | foreach ($this->templates as $template) { 189 | $hash = $template['hash']; 190 | if (!isset($templates[$hash])) { 191 | $template['render_count'] = 0; 192 | $template['name_original'] = $template['name']; 193 | $templates[$hash] = $template; 194 | } 195 | 196 | $templates[$hash]['render_count']++; 197 | $templates[$hash]['name'] = $templates[$hash]['render_count'] . 'x ' . $templates[$hash]['name_original']; 198 | } 199 | $templates = array_values($templates); 200 | } else { 201 | $templates = $this->templates; 202 | } 203 | 204 | return [ 205 | 'count' => count($this->templates), 206 | 'nb_templates' => count($this->templates), 207 | 'templates' => $templates, 208 | ]; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/DataFormatter/QueryFormatter.php: -------------------------------------------------------------------------------- 1 | checkBindings($binding); 39 | $binding = '[' . implode(',', $binding) . ']'; 40 | } 41 | 42 | if (is_object($binding)) { 43 | $binding = json_encode($binding); 44 | } 45 | } 46 | 47 | return $bindings; 48 | } 49 | 50 | /** 51 | * Format a source object. 52 | * 53 | * @param object|null $source If the backtrace is disabled, the $source will be null. 54 | * @return string 55 | */ 56 | public function formatSource($source, $short = false) 57 | { 58 | if (! is_object($source)) { 59 | return ''; 60 | } 61 | 62 | $parts = []; 63 | 64 | if (!$short && $source->namespace) { 65 | $parts['namespace'] = $source->namespace . '::'; 66 | } 67 | 68 | $parts['name'] = $short ? basename($source->name) : $source->name; 69 | $parts['line'] = ':' . $source->line; 70 | 71 | return implode($parts); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/DataFormatter/SimpleFormatter.php: -------------------------------------------------------------------------------- 1 | exportValue($data); 22 | } 23 | 24 | /** 25 | * Converts a PHP value to a string. 26 | * 27 | * @param mixed $value The PHP value 28 | * @param int $depth Only for internal usage 29 | * @param bool $deep Only for internal usage 30 | * 31 | * @return string The string representation of the given value 32 | * @author Bernhard Schussek 33 | */ 34 | private function exportValue($value, $depth = 1, $deep = false) 35 | { 36 | if ($value instanceof \__PHP_Incomplete_Class) { 37 | return sprintf('__PHP_Incomplete_Class(%s)', $this->getClassNameFromIncomplete($value)); 38 | } 39 | 40 | if (is_object($value)) { 41 | if ($value instanceof \DateTimeInterface) { 42 | return sprintf('Object(%s) - %s', get_class($value), $value->format(\DateTime::ATOM)); 43 | } 44 | 45 | return sprintf('Object(%s)', get_class($value)); 46 | } 47 | 48 | if (is_array($value)) { 49 | if (empty($value)) { 50 | return '[]'; 51 | } 52 | 53 | $indent = str_repeat(' ', $depth); 54 | 55 | $a = []; 56 | foreach ($value as $k => $v) { 57 | if (is_array($v)) { 58 | $deep = true; 59 | } 60 | $a[] = sprintf('%s => %s', $k, $this->exportValue($v, $depth + 1, $deep)); 61 | } 62 | 63 | if ($deep) { 64 | $args = [$indent, implode(sprintf(", \n%s", $indent), $a), str_repeat(' ', $depth - 1)]; 65 | return sprintf("[\n%s%s\n%s]", ...$args); 66 | } 67 | 68 | $s = sprintf('[%s]', implode(', ', $a)); 69 | 70 | if (80 > strlen($s)) { 71 | return $s; 72 | } 73 | 74 | return sprintf("[\n%s%s\n]", $indent, implode(sprintf(",\n%s", $indent), $a)); 75 | } 76 | 77 | if (is_resource($value)) { 78 | return sprintf('Resource(%s#%d)', get_resource_type($value), $value); 79 | } 80 | 81 | if (null === $value) { 82 | return 'null'; 83 | } 84 | 85 | if (false === $value) { 86 | return 'false'; 87 | } 88 | 89 | if (true === $value) { 90 | return 'true'; 91 | } 92 | 93 | return (string) $value; 94 | } 95 | 96 | /** 97 | * @param \__PHP_Incomplete_Class $value 98 | * @return mixed 99 | * @author Bernhard Schussek 100 | */ 101 | private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value) 102 | { 103 | $array = new \ArrayObject($value); 104 | 105 | return $array['__PHP_Incomplete_Class_Name']; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/DebugbarViewEngine.php: -------------------------------------------------------------------------------- 1 | engine = $engine; 33 | $this->laravelDebugbar = $laravelDebugbar; 34 | $this->exclude_paths = app('config')->get('debugbar.options.views.exclude_paths', []); 35 | } 36 | 37 | /** 38 | * @param string $path 39 | * @param array $data 40 | * @return string 41 | */ 42 | public function get($path, array $data = []) 43 | { 44 | $basePath = base_path(); 45 | $shortPath = @file_exists((string) $path) ? realpath($path) : $path; 46 | 47 | if (str_starts_with($shortPath, $basePath)) { 48 | $shortPath = ltrim( 49 | str_replace('\\', '/', substr($shortPath, strlen($basePath))), 50 | '/' 51 | ); 52 | } 53 | 54 | foreach ($this->exclude_paths as $excludePath) { 55 | if (str_starts_with($shortPath, $excludePath)) { 56 | return $this->engine->get($path, $data); 57 | } 58 | } 59 | 60 | return $this->laravelDebugbar->measure($shortPath, function () use ($path, $data) { 61 | return $this->engine->get($path, $data); 62 | }, 'views'); 63 | } 64 | 65 | /** 66 | * NOTE: This is done to support other Engine swap (example: Livewire). 67 | * @param $name 68 | * @param $arguments 69 | * @return mixed 70 | */ 71 | public function __call($name, $arguments) 72 | { 73 | return $this->engine->$name(...$arguments); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Facade.php: -------------------------------------------------------------------------------- 1 | cssFiles['laravel'] = __DIR__ . '/Resources/laravel-debugbar.css'; 23 | $this->jsFiles['laravel-cache'] = __DIR__ . '/Resources/cache/widget.js'; 24 | $this->jsFiles['laravel-queries'] = __DIR__ . '/Resources/queries/widget.js'; 25 | 26 | $this->setTheme(config('debugbar.theme', 'auto')); 27 | } 28 | 29 | /** 30 | * Set the URL Generator 31 | * 32 | * @param \Illuminate\Routing\UrlGenerator $url 33 | * @deprecated 34 | */ 35 | public function setUrlGenerator($url) 36 | { 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function renderHead() 43 | { 44 | $cssRoute = preg_replace('/\Ahttps?:\/\/[^\/]+/', '', route('debugbar.assets.css', [ 45 | 'v' => $this->getModifiedTime('css'), 46 | ])); 47 | 48 | $jsRoute = preg_replace('/\Ahttps?:\/\/[^\/]+/', '', route('debugbar.assets.js', [ 49 | 'v' => $this->getModifiedTime('js') 50 | ])); 51 | 52 | $nonce = $this->getNonceAttribute(); 53 | 54 | $html = ""; 55 | $html .= ""; 56 | 57 | if ($this->isJqueryNoConflictEnabled()) { 58 | $html .= "jQuery.noConflict(true);" . "\n"; 59 | } 60 | 61 | $inlineHtml = $this->getInlineHtml(); 62 | if ($nonce != '') { 63 | $inlineHtml = preg_replace("/<(script|style)>/", "<$1{$nonce}>", $inlineHtml); 64 | } 65 | $html .= $inlineHtml; 66 | 67 | 68 | return $html; 69 | } 70 | 71 | protected function getInlineHtml() 72 | { 73 | $html = ''; 74 | 75 | foreach (['head', 'css', 'js'] as $asset) { 76 | foreach ($this->getAssets('inline_' . $asset) as $item) { 77 | $html .= $item . "\n"; 78 | } 79 | } 80 | 81 | return $html; 82 | } 83 | /** 84 | * Get the last modified time of any assets. 85 | * 86 | * @param string $type 'js' or 'css' 87 | * @return int 88 | */ 89 | protected function getModifiedTime($type) 90 | { 91 | $files = $this->getAssets($type); 92 | 93 | $latest = 0; 94 | foreach ($files as $file) { 95 | $mtime = filemtime($file); 96 | if ($mtime > $latest) { 97 | $latest = $mtime; 98 | } 99 | } 100 | return $latest; 101 | } 102 | 103 | /** 104 | * Return assets as a string 105 | * 106 | * @param string $type 'js' or 'css' 107 | * @return string 108 | */ 109 | public function dumpAssetsToString($type) 110 | { 111 | $files = $this->getAssets($type); 112 | 113 | $content = ''; 114 | foreach ($files as $file) { 115 | $content .= file_get_contents($file) . "\n"; 116 | } 117 | 118 | return $content; 119 | } 120 | 121 | /** 122 | * Makes a URI relative to another 123 | * 124 | * @param string|array $uri 125 | * @param string $root 126 | * @return string 127 | */ 128 | protected function makeUriRelativeTo($uri, $root) 129 | { 130 | if (!$root) { 131 | return $uri; 132 | } 133 | 134 | if (is_array($uri)) { 135 | $uris = []; 136 | foreach ($uri as $u) { 137 | $uris[] = $this->makeUriRelativeTo($u, $root); 138 | } 139 | return $uris; 140 | } 141 | 142 | if (substr($uri ?? '', 0, 1) === '/' || preg_match('/^([a-zA-Z]+:\/\/|[a-zA-Z]:\/|[a-zA-Z]:\\\)/', $uri ?? '')) { 143 | return $uri; 144 | } 145 | return rtrim($root, '/') . "/$uri"; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->call( 18 | function () { 19 | $debugBar = $this->app->get(LaravelDebugbar::class); 20 | if ($debugBar->shouldCollect('time', true)) { 21 | $startTime = $this->app['request']->server('REQUEST_TIME_FLOAT'); 22 | 23 | if (!$debugBar->hasCollector('time')) { 24 | $debugBar->addCollector(new TimeDataCollector($startTime)); 25 | } 26 | 27 | if ($this->app['config']->get('debugbar.options.time.memory_usage')) { 28 | $debugBar['time']->showMemoryUsage(); 29 | } 30 | 31 | if ($startTime) { 32 | $debugBar->addMeasure('Booting', $startTime, microtime(true), [], 'time'); 33 | } 34 | } 35 | } 36 | ); 37 | } 38 | 39 | /** 40 | * Get the active router. 41 | * 42 | * @return Application 43 | */ 44 | protected function getRouter() 45 | { 46 | return $this->app->router; 47 | } 48 | 49 | /** 50 | * Get the config path 51 | * 52 | * @return string 53 | */ 54 | protected function getConfigPath() 55 | { 56 | return base_path('config/debugbar.php'); 57 | } 58 | 59 | /** 60 | * Register the Debugbar Middleware 61 | * 62 | * @param string $middleware 63 | */ 64 | protected function registerMiddleware($middleware) 65 | { 66 | $this->app->middleware([$middleware]); 67 | } 68 | 69 | /** 70 | * Get the services provided by the provider. 71 | * 72 | * @return array 73 | */ 74 | public function provides() 75 | { 76 | return ['debugbar', 'command.debugbar.clear']; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Middleware/DebugbarEnabled.php: -------------------------------------------------------------------------------- 1 | debugbar = $debugbar; 26 | } 27 | 28 | /** 29 | * Handle an incoming request. 30 | * 31 | * @param Request $request 32 | * @param Closure $next 33 | * @return mixed 34 | */ 35 | public function handle($request, Closure $next) 36 | { 37 | if (!$this->debugbar->isEnabled()) { 38 | abort(404); 39 | } 40 | 41 | return $next($request); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/InjectDebugbar.php: -------------------------------------------------------------------------------- 1 | container = $container; 45 | $this->debugbar = $debugbar; 46 | $this->except = config('debugbar.except') ?: []; 47 | } 48 | 49 | /** 50 | * Handle an incoming request. 51 | * 52 | * @param Request $request 53 | * @param Closure $next 54 | * @return mixed 55 | */ 56 | public function handle($request, Closure $next) 57 | { 58 | if (!$this->debugbar->isEnabled() || $this->inExceptArray($request)) { 59 | return $next($request); 60 | } 61 | 62 | $this->debugbar->boot(); 63 | 64 | try { 65 | /** @var \Illuminate\Http\Response $response */ 66 | $response = $next($request); 67 | } catch (Throwable $e) { 68 | $response = $this->handleException($request, $e); 69 | } 70 | 71 | // Modify the response to add the Debugbar 72 | $this->debugbar->modifyResponse($request, $response); 73 | 74 | return $response; 75 | } 76 | 77 | /** 78 | * Handle the given exception. 79 | * 80 | * (Copy from Illuminate\Routing\Pipeline by Taylor Otwell) 81 | * 82 | * @param $passable 83 | * @param Throwable $e 84 | * @return mixed 85 | * @throws Exception 86 | */ 87 | protected function handleException($passable, $e) 88 | { 89 | if (! $this->container->bound(ExceptionHandler::class) || ! $passable instanceof Request) { 90 | throw $e; 91 | } 92 | 93 | $handler = $this->container->make(ExceptionHandler::class); 94 | 95 | $handler->report($e); 96 | 97 | return $handler->render($passable, $e); 98 | } 99 | 100 | /** 101 | * Determine if the request has a URI that should be ignored. 102 | * 103 | * @param \Illuminate\Http\Request $request 104 | * @return bool 105 | */ 106 | protected function inExceptArray($request) 107 | { 108 | foreach ($this->except as $except) { 109 | if ($except !== '/') { 110 | $except = trim($except, '/'); 111 | } 112 | 113 | if ($request->is($except)) { 114 | return true; 115 | } 116 | } 117 | 118 | return false; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Resources/cache/widget.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 3 | var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); 4 | 5 | /** 6 | * Widget for the displaying cache events 7 | * 8 | * Options: 9 | * - data 10 | */ 11 | var LaravelCacheWidget = PhpDebugBar.Widgets.LaravelCacheWidget = PhpDebugBar.Widgets.TimelineWidget.extend({ 12 | 13 | tagName: 'ul', 14 | 15 | className: csscls('timeline cache'), 16 | 17 | onForgetClick: function (e, el) { 18 | e.stopPropagation(); 19 | 20 | $.ajax({ 21 | url: $(el).attr("data-url"), 22 | type: 'DELETE', 23 | success: function (result) { 24 | $(el).fadeOut(200); 25 | } 26 | }); 27 | }, 28 | 29 | render: function () { 30 | LaravelCacheWidget.__super__.render.apply(this); 31 | 32 | this.bindAttr('data', function (data) { 33 | 34 | if (data.measures) { 35 | var self = this; 36 | var lines = this.$el.find('.' + csscls('measure')); 37 | 38 | for (var i = 0; i < data.measures.length; i++) { 39 | var measure = data.measures[i]; 40 | var m = lines[i]; 41 | 42 | if (measure.params && !$.isEmptyObject(measure.params)) { 43 | if (measure.params.delete) { 44 | $(m).next().find('td.phpdebugbar-widgets-name:contains(delete)').closest('tr').remove(); 45 | } 46 | if (measure.params.delete && measure.params.key) { 47 | $('') 48 | .addClass(csscls('forget')) 49 | .text('forget') 50 | .attr('data-url', measure.params.delete) 51 | .one('click', function (e) { 52 | self.onForgetClick(e, this); }) 53 | .appendTo(m); 54 | } 55 | } 56 | } 57 | } 58 | }); 59 | } 60 | }); 61 | 62 | })(PhpDebugBar.$); 63 | -------------------------------------------------------------------------------- /src/Resources/queries/widget.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | let css = PhpDebugBar.utils.makecsscls('phpdebugbar-'); 4 | let csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-'); 5 | 6 | /** 7 | * Widget for displaying sql queries. 8 | * 9 | * Options: 10 | * - data 11 | */ 12 | const QueriesWidget = PhpDebugBar.Widgets.LaravelQueriesWidget = PhpDebugBar.Widget.extend({ 13 | 14 | className: csscls('sqlqueries'), 15 | 16 | duplicateQueries: new Set(), 17 | 18 | hiddenConnections: new Set(), 19 | 20 | copyToClipboard: function (code) { 21 | if (document.selection) { 22 | const range = document.body.createTextRange(); 23 | range.moveToElementText(code); 24 | range.select(); 25 | } else if (window.getSelection) { 26 | const range = document.createRange(); 27 | range.selectNodeContents(code); 28 | window.getSelection().removeAllRanges(); 29 | window.getSelection().addRange(range); 30 | } 31 | 32 | var isCopied = false; 33 | try { 34 | isCopied = document.execCommand('copy'); 35 | console.log('Query copied to the clipboard'); 36 | } catch (err) { 37 | alert('Oops, unable to copy'); 38 | } 39 | 40 | window.getSelection().removeAllRanges(); 41 | 42 | return isCopied; 43 | }, 44 | 45 | explainMysql: function ($element, statement, rows, visual) { 46 | const headings = []; 47 | for (const key in rows[0]) { 48 | headings.push($('').text(key)); 49 | } 50 | 51 | const values = []; 52 | for (const row of rows) { 53 | const $tr = $(''); 54 | for (const key in row) { 55 | $tr.append($('').text(row[key])); 56 | } 57 | values.push($tr); 58 | } 59 | 60 | const $table = $('
').addClass(csscls('explain')); 61 | $table.find('thead').append($('').append(headings)); 62 | $table.find('tbody').append(values); 63 | 64 | $element.append($table); 65 | if (visual) { 66 | $element.append(this.explainVisual(statement, visual.confirm)); 67 | } 68 | }, 69 | 70 | explainPgsql: function ($element, statement, rows, visual) { 71 | const $ul = $('