diff --git a/.github/workflows/run-integration.yml b/.github/workflows/run-integration.yml new file mode 100644 index 00000000..25fb8178 --- /dev/null +++ b/.github/workflows/run-integration.yml @@ -0,0 +1,45 @@ +name: Integration + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + schedule: + - cron: '0 0 * * *' + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0] + + name: PHP${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: pdo_sqlite + + - name: Install dependencies + run: | + composer update --prefer-dist --no-progress + composer update --prefer-dist --no-progress --working-dir=demo/bridge/monolog + composer update --prefer-dist --no-progress --working-dir=demo/bridge/doctrine + composer run --working-dir=demo/bridge/doctrine install-schema + + - name: Execute Unit Tests + run: vendor/bin/phpunit --testsuite=Browser diff --git a/.github/workflows/run-screenshots.yml b/.github/workflows/run-screenshots.yml new file mode 100644 index 00000000..8139c2ab --- /dev/null +++ b/.github/workflows/run-screenshots.yml @@ -0,0 +1,51 @@ +name: Screenshots + +on: + push: + branches: + - master + pull_request: + branches: + - "*" + schedule: + - cron: '0 0 * * *' + +jobs: + php-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + COMPOSER_NO_INTERACTION: 1 + + strategy: + matrix: + php: [8.4] + + name: PHP${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: pdo_sqlite + + - name: Install dependencies + run: | + composer update --prefer-dist --no-progress + composer update --prefer-dist --no-progress --working-dir=demo/bridge/monolog + composer update --prefer-dist --no-progress --working-dir=demo/bridge/doctrine + composer run --working-dir=demo/bridge/doctrine install-schema + + - name: Execute Unit Tests + run: vendor/bin/phpunit --testsuite=Browser + + - name: Upload screenshots + uses: actions/upload-artifact@v4 + with: + name: debugbar-screenshots + path: tests/screenshots diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 97b58486..188e0240 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -12,20 +12,20 @@ on: jobs: php-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 strategy: matrix: - php: [8.2, 8.1, 8.0, 7.4, 7.3, 7.2, 7.1] + php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0] name: PHP${{ matrix.php }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -38,4 +38,4 @@ jobs: run: composer update --prefer-dist --no-progress - name: Execute Unit Tests - run: vendor/bin/phpunit + run: vendor/bin/phpunit --testsuite=Unit diff --git a/.gitignore b/.gitignore index babc56e5..01ff6a42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ composer.lock /vendor +/demo/bridge/*/vendor +/demo/bridge/doctrine/db.sqlite +/demo/profiles /src/DebugBar/Resources/vendor -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +/drivers +/chromedriver +.phpunit.cache/ +/tests/screenshots \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cf87671b..19d12ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog +2025-06-26 + + - Add Windsurf editor link template + +2023-09-08 + + - Add SymfonyMailCollector (#554) + 2021-12-21 - Add support for `symfony/var-dumper^6` package @@ -11,40 +19,40 @@ - Use Symfony VarDumper instead of kintLite as DataFormatter (#179) - Better resize handling (#185) - + 2014-11 (1.10.1): - Add disableVendor() option to JavascriptRenderer to remove a specific vendor (#182) - Fix macros in Twig Collector (#167, #177) - - Update Font Awesome to 4.2.0 + - Update Font Awesome to 4.2.0 2014-10 (1.10.0): - Add bindToXHR() as alternative to jQuery ajax handling. - Extend TemplateWidget to show more information + parameters - Extend TimeDataCollector to show parameters + collector source - + 2014-08: - Replace image files with inline data in css - Tweak OpenHandler display - + 2014-06-10: - Add LocalizationCollector - + 2014-03-29: - Add hasMeasure() method to TimeDataCollector - + 2014-03-25: - Duplicate SQL detection - + 2014-03-23: - Add syntax highlighting - + 2014-03-22: - added AssetProvider interface diff --git a/README.md b/README.md index 9ce10160..7fd13e3b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # PHP Debug Bar -[![Latest Stable Version](https://poser.pugx.org/maximebf/debugbar/v/stable.png)](https://packagist.org/packages/maximebf/debugbar) [![Total Downloads](https://poser.pugx.org/maximebf/debugbar/downloads.svg)](https://packagist.org/packages/maximebf/debugbar) [![License](https://poser.pugx.org/maximebf/debugbar/license.svg)](https://packagist.org/packages/maximebf/debugbar) [![Tests](https://github.com/maximebf/php-debugbar/actions/workflows/run-tests.yml/badge.svg)](https://github.com/maximebf/php-debugbar/actions/workflows/run-tests.yml) +[![Latest Stable Version](https://img.shields.io/packagist/v/php-debugbar/php-debugbar?label=Stable)](https://packagist.org/packages/php-debugbar/php-debugbar) [![Total Downloads](https://img.shields.io/packagist/dt/maximebf/debugbar?label=Downloads)](https://packagist.org/packages/php-debugbar/php-debugbar) [![License](https://img.shields.io/badge/Licence-MIT-4d9283)](https://packagist.org/packages/php-debugbar/php-debugbar) [![Tests](https://github.com/maximebf/php-debugbar/actions/workflows/run-tests.yml/badge.svg)](https://github.com/php-debugbar/php-debugbar/actions/workflows/run-tests.yml) Displays a debug bar in the browser with information from php. No more `var_dump()` in your code! -![Screenshot](https://raw.github.com/maximebf/php-debugbar/master/docs/screenshot.png) +> **Note: Debug Bar is for development use only. Never install this on websites that are publicly accessible.** + +![Screenshot](https://raw.github.com/php-debugbar/php-debugbar/master/docs/screenshot.png) **Features:** @@ -17,7 +19,7 @@ No more `var_dump()` in your code! - The client side bar is 100% coded in javascript - Easily create your own collectors and their associated view in the bar - Save and re-open previous requests - - [Very well documented](http://phpdebugbar.com/docs) + - [Very well documented](http://php-debugbar.com/docs/) Includes collectors for: @@ -27,11 +29,12 @@ Includes collectors for: - [Monolog](https://github.com/Seldaek/monolog) - [Propel](http://propelorm.org/) - [Slim](http://slimframework.com) + - [Symfony Mailer](https://symfony.com/doc/current/mailer.html) - [Swift Mailer](http://swiftmailer.org/) - - [Twig](http://twig.sensiolabs.org/) + - [Twig](http://twig.symfony.com/) -Checkout the [demo](https://github.com/maximebf/php-debugbar/tree/master/demo) for -examples and [phpdebugbar.com](http://phpdebugbar.com) for a live example. +Checkout the [demo](https://github.com/php-debugbar/php-debugbar/tree/master/demo) for +examples and [phpdebugbar.com](http://php-debugbar.com) for a live example. Integrations with other frameworks: @@ -46,7 +49,8 @@ Integrations with other frameworks: - [Joomla](https://github.com/joomla/joomla-cms/blob/4.0-dev/plugins/system/debug/debug.php) - [Drupal](https://www.drupal.org/project/debugbar) - [October CMS](https://github.com/rainlab/debugbar-plugin) - - Framework-agnostic middleware and PSR-7 with [php-middleware/phpdebugbar](https://github.com/php-middleware/phpdebugbar). + - Framework-agnostic middleware and PSR-7 with [php-middleware/phpdebugbar](https://github.com/php-middleware/phpdebugbar) + - [Dotkernel Frontend Application](https://github.com/dotkernel/dot-debugbar) *(drop me a message or submit a PR to add your DebugBar related project here)* @@ -55,7 +59,9 @@ Integrations with other frameworks: The best way to install DebugBar is using [Composer](http://getcomposer.org) with the following command: -```composer require maximebf/debugbar``` +```bash +composer require --dev php-debugbar/php-debugbar +``` ## Quick start @@ -103,4 +109,20 @@ $debugbar["messages"]->addMessage("hello world!"); - `TimeDataCollector` (*time*) - `ExceptionsCollector` (*exceptions*) -Learn more about DebugBar in the [docs](http://phpdebugbar.com/docs). +Learn more about DebugBar in the [docs](http://php-debugbar.com/docs/). + + +## Demo + +To run the demo, clone this repository and start the Built-In PHP webserver from the root: + +``` +php -S localhost:8000 +``` + +Then visit http://localhost:8000/demo/ + +## Testing + +To test, run `php vendor/bin/phpunit`. +To debug Browser tests, you can run `PANTHER_NO_HEADLESS=1 vendor/bin/phpunit --debug`. Run `vendor/bin/bdi detect drivers` to download the latest drivers. diff --git a/bower.json b/bower.json index 821e1d3b..d23c4bc8 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "maximebf/php-debugbar", "dependencies": { - "jquery": "^3.3", + "jquery": "^3.7", "font-awesome": "^4.7" } } diff --git a/composer.json b/composer.json index 50793eb6..7e1c5e7d 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "maximebf/debugbar", + "name": "php-debugbar/php-debugbar", "description": "Debug bar in the browser for php application", - "keywords": ["debug", "debugbar"], - "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": ["debug", "debugbar", "debug bar", "dev"], + "homepage": "https://github.com/php-debugbar/php-debugbar", "type": "library", "license": "MIT", "authors": [ @@ -17,19 +17,41 @@ } ], "require": { - "php": "^7.1|^8", + "php": "^8", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^4|^5|^6" + "symfony/var-dumper": "^4|^5|^6|^7" + }, + "replace": { + "maximebf/debugbar": "self.version" }, "require-dev": { - "phpunit/phpunit": ">=7.5.20 <10.0", - "twig/twig": "^1.38|^2.7|^3.0" + "phpunit/phpunit": "^8|^9", + "twig/twig": "^1.38|^2.7|^3.0", + "symfony/panther": "^1|^2.1", + "dbrekelmans/bdi": "^1" }, "autoload": { "psr-4": { "DebugBar\\": "src/DebugBar/" } }, + "autoload-dev": { + "psr-4": { + "DebugBar\\Tests\\": "tests/DebugBar/Tests" + } + }, + "scripts": { + "demo": [ + "Composer\\Config::disableProcessTimeout", + "@php -S localhost:8000" + ], + "unit-test": "@php vendor/bin/phpunit --testsuite=Unit", + "browser-test": "@php vendor/bin/phpunit --testsuite=Browser", + "browser-debug": [ + "@putenv PANTHER_NO_HEADLESS=1", + "@php vendor/bin/phpunit --testsuite=Browser --debug" + ] + }, "suggest": { "kriswallsmith/assetic": "The best way to manage assets", "monolog/monolog": "Log using Monolog", @@ -37,7 +59,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-master": "2.1-dev" } } } diff --git a/demo/bootstrap.php b/demo/bootstrap.php index 3af47052..b7031ba0 100644 --- a/demo/bootstrap.php +++ b/demo/bootstrap.php @@ -1,6 +1,6 @@ getJavascriptRenderer() ->setBaseUrl('../src/DebugBar/Resources') - ->setEnableJqueryNoConflict(false); + ->setAjaxHandlerEnableTab(true) + ->setHideEmptyTabs(true) + ->setEnableJqueryNoConflict(false) + ->setTheme($_GET['theme'] ?? 'auto'); // // create a writable profiles folder in the demo directory to uncomment the following lines @@ -19,7 +22,7 @@ // $debugbar->setStorage(new DebugBar\Storage\RedisStorage(new Predis\Client())); // $debugbarRenderer->setOpenHandlerUrl('open.php'); -function render_demo_page(Closure $callback = null) +function render_demo_page(?Closure $callback = null) { global $debugbarRenderer; ?> @@ -29,9 +32,8 @@ function render_demo_page(Closure $callback = null) ")->execute(); render_demo_page(); diff --git a/demo/bridge/doctrine/src/Demo/Product.php b/demo/bridge/doctrine/src/Demo/Product.php index c7d8f08d..d2a8a4f9 100644 --- a/demo/bridge/doctrine/src/Demo/Product.php +++ b/demo/bridge/doctrine/src/Demo/Product.php @@ -11,6 +11,8 @@ class Product protected $id; /** @Column(type="string") **/ protected $name; + /** @Column(type="datetime", nullable=true) **/ + protected $updated; public function getId() { @@ -26,4 +28,10 @@ public function setName($name) { $this->name = $name; } + + public function setUpdated(): void + { + // will NOT be saved in the database + $this->updated = new \DateTime('now'); + } } diff --git a/demo/bridge/swiftmailer/index.php b/demo/bridge/swiftmailer/index.php index 8dc78ec4..56abaec0 100644 --- a/demo/bridge/swiftmailer/index.php +++ b/demo/bridge/swiftmailer/index.php @@ -8,15 +8,17 @@ use DebugBar\Bridge\SwiftMailer\SwiftLogCollector; use DebugBar\Bridge\SwiftMailer\SwiftMailCollector; -$mailer = Swift_Mailer::newInstance(Swift_NullTransport::newInstance()); +$mailer = new Swift_Mailer(new Swift_NullTransport()); $debugbar['messages']->aggregate(new SwiftLogCollector($mailer)); -$debugbar->addCollector(new SwiftMailCollector($mailer)); +$mailCollector = new SwiftMailCollector($mailer); +$mailCollector->showMessageBody(); +$debugbar->addCollector($mailCollector); -$message = Swift_Message::newInstance('Wonderful Subject') +$message = (new Swift_Message('Wonderful Subject')) ->setFrom(array('john@doe.com' => 'John Doe')) ->setTo(array('receiver@domain.org', 'other@domain.org' => 'A name')) - ->setBody('Here is the message itself'); + ->setBody('
Here is the message itself
'); $mailer->send($message); diff --git a/demo/bridge/symfonymailer/composer.json b/demo/bridge/symfonymailer/composer.json new file mode 100644 index 00000000..75ca9a29 --- /dev/null +++ b/demo/bridge/symfonymailer/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "symfony/event-dispatcher": "*", + "symfony/mailer": "*" + } +} diff --git a/demo/bridge/symfonymailer/index.php b/demo/bridge/symfonymailer/index.php new file mode 100644 index 00000000..557aa495 --- /dev/null +++ b/demo/bridge/symfonymailer/index.php @@ -0,0 +1,50 @@ +setBaseUrl('../../../src/DebugBar/Resources'); + +$mailCollector = new SymfonyMailCollector(); +$mailCollector->showMessageDetail(); +$mailCollector->showMessageBody(); +$debugbar->addCollector($mailCollector); +$logger = new MessagesCollector('mails'); +$debugbar['messages']->aggregate($logger); + +// Add even listener for SentMessageEvent +$dispatcher = new EventDispatcher(); +$dispatcher->addListener(SentMessageEvent::class, function (SentMessageEvent $event) use ($mailCollector): void { + $mailCollector->addSymfonyMessage($event->getMessage()); +}); + +// Creates NullTransport Mailer for testing +$mailer = new Mailer(new class ($dispatcher, $logger) extends AbstractTransport { + protected function doSend(\Symfony\Component\Mailer\SentMessage $message): void + { + $this->getLogger()->debug('Sending message "'.$message->getOriginalMessage()->getSubject().'"'); + } + public function __toString(): string{ return 'null://'; } +}); + +$email = (new Email()) + ->from('john@doe.com') + ->to('you@example.com') + //->cc('cc@example.com') + //->bcc('bcc@example.com') + //->replyTo('fabien@example.com') + //->priority(Email::PRIORITY_HIGH) + ->subject('Wonderful Subject') + ->html('
Here is the message itself
'); + +$mailer->send($email); + +render_demo_page(); diff --git a/demo/bridge/twig/hello.html b/demo/bridge/twig/hello.html index 296b3f63..229e7156 100644 --- a/demo/bridge/twig/hello.html +++ b/demo/bridge/twig/hello.html @@ -1,2 +1,7 @@ Hello {{ name }} {% include "foobar.html" %} + +{% measure 'Measure Debugs' %} +{{ debug('Hello ' ~ name) }} +{{ dump({'name' : name}) }} +{% endmeasure %} diff --git a/demo/bridge/twig/index.php b/demo/bridge/twig/index.php index 3550accb..98b3843f 100644 --- a/demo/bridge/twig/index.php +++ b/demo/bridge/twig/index.php @@ -5,12 +5,23 @@ $debugbarRenderer->setBaseUrl('../../../src/DebugBar/Resources'); -$loader = new Twig_Loader_Filesystem('.'); -$twig = new Twig_Environment($loader); -$profile = new Twig_Profiler_Profile(); +$loader = new Twig\Loader\FilesystemLoader('.'); +$twig = new Twig\Environment($loader); +$profile = new Twig\Profiler\Profile(); + +// enable template measure on timeline $twig->addExtension(new DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler($profile, $debugbar['time'])); -$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); +// enable {% measure 'foo' %} {% endmeasure %} tags for time measure on templates +$twig->addExtension(new DebugBar\Bridge\Twig\MeasureTwigExtension($debugbar['time'])); + +$twig->enableDebug(); +// enable {{ dump('foo') }} function on templates +$twig->addExtension(new DebugBar\Bridge\Twig\DumpTwigExtension()); +// enable {{ debug('foo') }} function on templates +$twig->addExtension(new DebugBar\Bridge\Twig\DebugTwigExtension($debugbar['messages'])); + +$debugbar->addCollector(new DebugBar\Bridge\NamespacedTwigProfileCollector($profile, $twig)); render_demo_page(function() use ($twig) { echo $twig->render('hello.html', array('name' => 'peter pan')); diff --git a/demo/iframes/iframe1.php b/demo/iframes/iframe1.php new file mode 100644 index 00000000..e270195f --- /dev/null +++ b/demo/iframes/iframe1.php @@ -0,0 +1,13 @@ +setBaseUrl('../../src/DebugBar/Resources'); + +$debugbar['messages']->addMessage('I\'m a IFRAME'); + +render_demo_page(function() { +?> + +setBaseUrl('../../src/DebugBar/Resources'); + +$debugbar['messages']->addMessage('I\'m a Deeper Hidden Iframe'); + +render_demo_page(function() { +?> + +setBaseUrl('../../src/DebugBar/Resources'); + +$debugbar['messages']->addMessage('Top Page(Main debugbar)'); + +render_demo_page(function() { +?> + +addMessage(array('toto' => array('titi', 'tata'))); $debugbar['messages']->addMessage('oups', 'error'); +$classDemo = array('FirstClass', 'SecondClass', 'ThirdClass'); +$classEvent = array('Retrieved', 'Saved', 'Deleted'); +$debugbar->addCollector(new \DebugBar\DataCollector\ObjectCountCollector()); +$debugbar['counter']->collectCountSummary(true); +$debugbar['counter']->setKeyMap($classEvent); +for ($i = 0; $i <=20; $i++) { + $debugbar['counter']->countClass($classDemo[rand(0, 2)], 1, $classEvent[rand(0, 2)]); +} + $debugbar['time']->startMeasure('render'); render_demo_page(function() { @@ -25,6 +34,11 @@
  • load ajax content
  • load ajax content with exception
  • +
    +

    IFRAMES

    +

    Stack

    addCollector(new PDOCollector($pdo)); +$debugbar['pdo']->setDurationBackground(true); $pdo->exec('create table users (name varchar)'); $stmt = $pdo->prepare('insert into users (name) values (?)'); @@ -18,6 +19,10 @@ $stmt->execute(array('foo')); $foo = $stmt->fetch(); -$pdo->exec('delete from titi'); +$stmt = $pdo->prepare('select * from users where name=?'); +$stmt->execute(array('')); +$foo = $stmt->fetch(); + +$pdo->exec('delete from users'); render_demo_page(); diff --git a/docs/base_collectors.md b/docs/base_collectors.md index 11407a3c..75f1fff0 100644 --- a/docs/base_collectors.md +++ b/docs/base_collectors.md @@ -115,7 +115,7 @@ Aggregates multiple collectors. Do not provide any widgets, you have to add your $debugbar->addCollector(new DebugBar\DataCollector\AggregatedCollector('all_messages', 'messages', 'time')); $debugbar['all_messages']->addCollector($debugbar['messages']); - $debugbar['all_messages']->addCollector(new MessagesCollector('mails')); + $debugbar['all_messages']->addCollector(new DebugBar\DataCollector\MessagesCollector('mails')); $debugbar['all_messages']['mails']->addMessage('sending mail'); $renderer = $debugbar->getJavascriptRenderer(); diff --git a/docs/bridge_collectors.md b/docs/bridge_collectors.md index f0ab486d..9ea1d613 100644 --- a/docs/bridge_collectors.md +++ b/docs/bridge_collectors.md @@ -86,6 +86,20 @@ Display log messages and sent mail using `DebugBar\Bridge\SwiftMailer\SwiftLogCo $debugbar['messages']->aggregate(new DebugBar\Bridge\SwiftMailer\SwiftLogCollector($mailer)); $debugbar->addCollector(new DebugBar\Bridge\SwiftMailer\SwiftMailCollector($mailer)); +## Symfony Mailer + +https://symfony.com/doc/current/mailer.html + +Display log messages and sent mail using `DebugBar\Bridge\Symfony\SymfonyMailCollector` + + use Symfony\Component\Mailer\Event\SentMessageEvent; + + $mailCollector = new DebugBar\Bridge\Symfony\SymfonyMailCollector(); + $debugbar->addCollector($mailCollector); + $eventDispatcher->addListener(SentMessageEvent::class, function (SentMessageEvent $event) use (&$mailCollector): void { + $mailCollector->addSymfonyMessage($event->getMessage()); + }); + ## Twig http://twig.sensiolabs.org/ @@ -104,17 +118,6 @@ $env->addExtension(new Twig_Extension_Profiler($profile)); $debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); ``` -You can optionally use `DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler` in place of -`Twig_Extension_Profiler` so render operation can be measured. - -```php -$loader = new Twig_Loader_Filesystem('.'); -$env = new Twig_Environment($loader); -$profile = new Twig_Profiler_Profile(); -$env->addExtension(new DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler($profile, $debugbar['time'])); -$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); -``` - ### Version 2 and 3 This collector uses the class `Twig\Extension\ProfilerExtension` to collect info about rendered @@ -135,3 +138,49 @@ $env->addExtension(new ProfilerExtension($profile)); $debugbar->addCollector(new NamespacedTwigProfileCollector($profile)); ``` +### Optional debugbar twig extensions + +You can optionally use `DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler` in place of +`Twig\Profiler\Profile` so render operation can be measured. + +```php +use Twig\Environment; +use Twig\Loader\FilesystemLoader; +use Twig\Profiler\Profile; + +$loader = new FilesystemLoader('.'); +$env = new Environment($loader); +$profile = new Profile(); + +$env->addExtension(new DebugBar\Bridge\Twig\TimeableTwigExtensionProfiler($profile, $debugbar['time'])); +$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); +``` + +Other optional extensions add functions and tags for debugbar integration into templates. + +```php +use Twig\Environment; +use Twig\Loader\FilesystemLoader; +use Twig\Profiler\Profile; + +$loader = new FilesystemLoader('.'); +$env = new Environment($loader); +$profile = new Profile(); + +// enable {% measure 'foo' %} {% endmeasure %} tags for time measure on templates +// this extension adds timeline items to TimeDataCollector +$twig->addExtension(new DebugBar\Bridge\Twig\MeasureTwigExtension($debugbar['time'])); + +$twig->enableDebug(); // if Twig\Environment debug is disabled, dump/debug are ignored + +// enable {{ dump('foo') }} function on templates +// this extension allows dumping data using debugbar DataFormatter +$twig->addExtension(new DebugBar\Bridge\Twig\DumpTwigExtension()); + +// enable {{ debug('foo') }} function on templates +// this extension allows debugging in MessageCollector +$twig->addExtension(new DebugBar\Bridge\Twig\DebugTwigExtension($debugbar['messages'])); + +$debugbar->addCollector(new DebugBar\Bridge\TwigProfileCollector($profile)); +``` + diff --git a/docs/data_collectors.md b/docs/data_collectors.md index d586f114..9baf7a51 100644 --- a/docs/data_collectors.md +++ b/docs/data_collectors.md @@ -46,7 +46,7 @@ same time the `DebugBar::collect()` method is called. This however won't show anything in the debug bar as no information are provided on how to display these data. You could do that manually as you'll see in later chapter -or implement the `DebugBar\DataSource\Renderable` interface. +or implement the `DebugBar\DataCollector\Renderable` interface. To implement it, you must define a `getWidgets()` function which returns an array of key/value pairs where key are control names and values control options as defined diff --git a/docs/rendering.md b/docs/rendering.md index fff23c4f..d10283c0 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -92,8 +92,8 @@ Thus in almost all cases, you should only have to use `render()` right away: This will print the initialization code for the toolbar and the dataset for the request. When you are performing AJAX requests, you do not want to initialize a new toolbar but -add the dataset to the existing one. You can disable initialization using ̀false` as -the first argument of ̀render()`. +add the dataset to the existing one. You can disable initialization using `false` as +the first argument of `render()`.

    my ajax content

    render(false) ?> diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fa4e13bd..3e57d743 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,24 +1,24 @@ - - - - ./tests/DebugBar/ - - - - - - ./src/DebugBar/ - - + + + + ./tests/DebugBar/Tests + ./tests/DebugBar/Tests/Browser + + + ./tests/DebugBar/Tests/Browser + + + + + + + + + + + + ./src/DebugBar/ + + diff --git a/src/DebugBar/Bridge/CacheCacheCollector.php b/src/DebugBar/Bridge/CacheCacheCollector.php index 7e7f46f9..55d87ccc 100644 --- a/src/DebugBar/Bridge/CacheCacheCollector.php +++ b/src/DebugBar/Bridge/CacheCacheCollector.php @@ -38,7 +38,7 @@ class CacheCacheCollector extends MonologCollector * @param bool $level * @param bool $bubble */ - public function __construct(Cache $cache = null, Logger $logger = null, $level = Logger::DEBUG, $bubble = true) + public function __construct(?Cache $cache = null, ?Logger $logger = null, $level = Logger::DEBUG, $bubble = true) { parent::__construct(null, $level, $bubble); diff --git a/src/DebugBar/Bridge/DoctrineCollector.php b/src/DebugBar/Bridge/DoctrineCollector.php index 7c91da9b..0387cd41 100644 --- a/src/DebugBar/Bridge/DoctrineCollector.php +++ b/src/DebugBar/Bridge/DoctrineCollector.php @@ -29,6 +29,8 @@ * $entityManager->getConnection()->getConfiguration()->setSQLLogger($debugStack); * $debugbar->addCollector(new DoctrineCollector($debugStack)); * + * + * @deprecated use https://github.com/php-debugbar/doctrine-bridge instead */ class DoctrineCollector extends DataCollector implements Renderable, AssetProvider { @@ -60,7 +62,7 @@ public function collect() foreach ($this->debugStack->queries as $q) { $queries[] = array( 'sql' => $q['sql'], - 'params' => (object) $q['params'], + 'params' => (object) $this->getParameters($q['params'] ?? []), 'duration' => $q['executionMS'], 'duration_str' => $this->formatDuration($q['executionMS']) ); @@ -75,6 +77,29 @@ public function collect() ); } + /** + * Returns an array of parameters used with the query + * + * @return array + */ + public function getParameters($params) : array + { + return array_map(function ($param) { + if (is_string($param)) { + return htmlentities($param, ENT_QUOTES, 'UTF-8', false); + } elseif (is_array($param)) { + return '[' . implode(', ', $this->getParameters($param)) . ']'; + } elseif (is_numeric($param)) { + return strval($param); + } elseif ($param instanceof \DateTimeInterface) { + return $param->format('Y-m-d H:i:s'); + } elseif (is_object($param)) { + return json_encode($param); + } + return $param ?: ''; + }, $params); + } + /** * @return string */ diff --git a/src/DebugBar/Bridge/MonologCollector.php b/src/DebugBar/Bridge/MonologCollector.php index 49eb37c2..25063ea1 100644 --- a/src/DebugBar/Bridge/MonologCollector.php +++ b/src/DebugBar/Bridge/MonologCollector.php @@ -37,7 +37,7 @@ class MonologCollector extends AbstractProcessingHandler implements DataCollecto * @param boolean $bubble * @param string $name */ - public function __construct(Logger $logger = null, $level = Logger::DEBUG, $bubble = true, $name = 'monolog') + public function __construct(?Logger $logger = null, $level = Logger::DEBUG, $bubble = true, $name = 'monolog') { parent::__construct($level, $bubble); $this->name = $name; diff --git a/src/DebugBar/Bridge/NamespacedTwigProfileCollector.php b/src/DebugBar/Bridge/NamespacedTwigProfileCollector.php index 02e7306e..09de97fa 100644 --- a/src/DebugBar/Bridge/NamespacedTwigProfileCollector.php +++ b/src/DebugBar/Bridge/NamespacedTwigProfileCollector.php @@ -79,9 +79,18 @@ class NamespacedTwigProfileCollector extends DataCollector implements Renderable public function __construct(Profile $profile, $loaderOrEnv = null) { $this->profile = $profile; + $this->setLoaderOrEnv($loaderOrEnv); + } + + /** + * @param LoaderInterface|Environment $loaderOrEnv + */ + public function setLoaderOrEnv($loaderOrEnv) + { if ($loaderOrEnv instanceof Environment) { $loaderOrEnv = $loaderOrEnv->getLoader(); } + $this->loader = $loaderOrEnv; } diff --git a/src/DebugBar/Bridge/PropelCollector.php b/src/DebugBar/Bridge/PropelCollector.php index 93ad4ff8..4a287eed 100644 --- a/src/DebugBar/Bridge/PropelCollector.php +++ b/src/DebugBar/Bridge/PropelCollector.php @@ -49,7 +49,7 @@ class PropelCollector extends DataCollector implements BasicLogger, Renderable, * * @param PropelConfiguration $config Apply profiling on a specific config */ - public static function enablePropelProfiling(PropelConfiguration $config = null) + public static function enablePropelProfiling(?PropelConfiguration $config = null) { if ($config === null) { $config = Propel::getConfiguration(PropelConfiguration::TYPE_OBJECT); @@ -74,7 +74,7 @@ public static function enablePropelProfiling(PropelConfiguration $config = null) * @param LoggerInterface $logger A logger to forward non-query log lines to * @param PropelPDO $conn Bound this collector to a connection only */ - public function __construct(LoggerInterface $logger = null, PropelPDO $conn = null) + public function __construct(?LoggerInterface $logger = null, ?PropelPDO $conn = null) { if ($conn) { $conn->setLogger($this); diff --git a/src/DebugBar/Bridge/SwiftMailer/SwiftMailCollector.php b/src/DebugBar/Bridge/SwiftMailer/SwiftMailCollector.php index 01a5e906..e574b346 100644 --- a/src/DebugBar/Bridge/SwiftMailer/SwiftMailCollector.php +++ b/src/DebugBar/Bridge/SwiftMailer/SwiftMailCollector.php @@ -25,20 +25,31 @@ class SwiftMailCollector extends DataCollector implements Renderable, AssetProvi { protected $messagesLogger; + /** @var bool */ + private $showBody = false; + public function __construct(Swift_Mailer $mailer) { $this->messagesLogger = new Swift_Plugins_MessageLogger(); $mailer->registerPlugin($this->messagesLogger); } + public function showMessageBody($show = true) + { + $this->showBody = $show; + } + public function collect() { $mails = array(); foreach ($this->messagesLogger->getMessages() as $msg) { + $html = $this->showBody ? $msg->getBody() : null; $mails[] = array( 'to' => $this->formatTo($msg->getTo()), 'subject' => $msg->getSubject(), - 'headers' => $msg->getHeaders()->toString() + 'headers' => $msg->getHeaders()->toString(), + 'body' => $html, + 'html' => $html, ); } return array( diff --git a/src/DebugBar/Bridge/Symfony/SymfonyMailCollector.php b/src/DebugBar/Bridge/Symfony/SymfonyMailCollector.php new file mode 100644 index 00000000..668076e5 --- /dev/null +++ b/src/DebugBar/Bridge/Symfony/SymfonyMailCollector.php @@ -0,0 +1,110 @@ +messages[] = $message->getOriginalMessage(); + } + + /** + * @deprecated use showMessageBody() + */ + public function showMessageDetail() + { + $this->showMessageBody(true); + } + + public function showMessageBody($show = true) + { + $this->showBody = $show; + } + + public function collect() + { + $mails = array(); + + foreach ($this->messages as $message) { + /* @var \Symfony\Component\Mime\Message $message */ + $mail = [ + 'to' => array_map(function ($address) { + /* @var \Symfony\Component\Mime\Address $address */ + return $address->toString(); + }, $message->getTo()), + 'subject' => $message->getSubject(), + 'headers' => $message->getHeaders()->toString(), + 'body' => null, + 'html' => null, + ]; + + if ($this->showBody) { + $body = $message->getBody(); + if ($body instanceof AbstractPart) { + $mail['html'] = $message->getHtmlBody(); + $mail['body'] = $message->getTextBody(); + } else { + $mail['body'] = $body->bodyToString(); + } + } + + $mails[] = $mail; + } + + return array( + 'count' => count($mails), + 'mails' => $mails, + ); + } + + public function getName() + { + return 'symfonymailer_mails'; + } + + public function getWidgets() + { + return array( + 'emails' => array( + 'icon' => 'inbox', + 'widget' => 'PhpDebugBar.Widgets.MailsWidget', + 'map' => 'symfonymailer_mails.mails', + 'default' => '[]', + 'title' => 'Mails' + ), + 'emails:badge' => array( + 'map' => 'symfonymailer_mails.count', + 'default' => 'null' + ) + ); + } + + public function getAssets() + { + return array( + 'css' => 'widgets/mails/widget.css', + 'js' => 'widgets/mails/widget.js' + ); + } +} diff --git a/src/DebugBar/Bridge/Twig/DebugTwigExtension.php b/src/DebugBar/Bridge/Twig/DebugTwigExtension.php new file mode 100644 index 00000000..a7f6bcee --- /dev/null +++ b/src/DebugBar/Bridge/Twig/DebugTwigExtension.php @@ -0,0 +1,94 @@ +messagesCollector = $messagesCollector; + $this->functionName = $functionName; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return static::class; + } + + /** + * {@inheritDoc} + */ + public function getFunctions() + { + return [ + new TwigFunction( + $this->functionName, + [$this, 'debug'], + ['needs_context' => true, 'needs_environment' => true] + ), + ]; + } + + /** + * Based on Twig_Extension_Debug / twig_var_dump + * + * @param Environment $env + * @param $context + */ + public function debug(Environment $env, $context) + { + if (!$env->isDebug() || !$this->messagesCollector) { + return; + } + + $count = func_num_args(); + if (2 === $count) { + $data = []; + foreach ($context as $key => $value) { + if (is_object($value)) { + if (method_exists($value, 'toArray')) { + $data[$key] = $value->toArray(); + } else { + $data[$key] = "Object (" . get_class($value) . ")"; + } + } else { + $data[$key] = $value; + } + } + $this->messagesCollector->addMessage($data, 'debug'); + } else { + for ($i = 2; $i < $count; $i++) { + $this->messagesCollector->addMessage(func_get_arg($i), 'debug'); + } + } + + return; + } +} diff --git a/src/DebugBar/Bridge/Twig/DumpTwigExtension.php b/src/DebugBar/Bridge/Twig/DumpTwigExtension.php new file mode 100644 index 00000000..83714cbb --- /dev/null +++ b/src/DebugBar/Bridge/Twig/DumpTwigExtension.php @@ -0,0 +1,99 @@ +functionName = $functionName; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return static::class; + } + + /** + * {@inheritDoc} + */ + public function getFunctions() + { + return [ + new TwigFunction( + $this->functionName, + [$this, 'dump'], + ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true] + ), + ]; + } + + /** + * Based on Twig_Extension_Debug / twig_var_dump + * + * @param Environment $env + * @param $context + * + * @return string + */ + public function dump(Environment $env, $context) + { + if (!$env->isDebug()) { + return; + } + + $output = ''; + + $count = func_num_args(); + if (2 === $count) { + $data = []; + foreach ($context as $key => $value) { + if (is_object($value)) { + if (method_exists($value, 'toArray')) { + $data[$key] = $value->toArray(); + } else { + $data[$key] = "Object (" . get_class($value) . ")"; + } + } else { + $data[$key] = $value; + } + } + $output .= $this->formatVar($data); + } else { + for ($i = 2; $i < $count; $i++) { + $output .= $this->formatVar(func_get_arg($i)); + } + } + + if ($this->isHtmlVarDumperUsed()) { + return $output; + } + + return '
    ' . $output . '
    '; + } +} diff --git a/src/DebugBar/Bridge/Twig/MeasureTwigExtension.php b/src/DebugBar/Bridge/Twig/MeasureTwigExtension.php new file mode 100644 index 00000000..b89ab6fe --- /dev/null +++ b/src/DebugBar/Bridge/Twig/MeasureTwigExtension.php @@ -0,0 +1,77 @@ +timeCollector = $timeCollector; + $this->tagName = $tagName; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return static::class; + } + + /** + * @return \Twig\TokenParser\TokenParserInterface[] + */ + public function getTokenParsers() + { + return [ + /* + * {% measure foo %} + * Some stuff which will be recorded on the timeline + * {% endmeasure %} + */ + new MeasureTwigTokenParser(!is_null($this->timeCollector), $this->tagName, $this->getName()), + ]; + } + + public function startMeasure(...$arg) + { + if (!$this->timeCollector) { + return; + } + + $this->timeCollector->startMeasure(...$arg); + } + + public function stopMeasure(...$arg) + { + if (!$this->timeCollector) { + return; + } + + $this->timeCollector->stopMeasure(...$arg); + } +} diff --git a/src/DebugBar/Bridge/Twig/MeasureTwigNode.php b/src/DebugBar/Bridge/Twig/MeasureTwigNode.php new file mode 100644 index 00000000..565dd6ec --- /dev/null +++ b/src/DebugBar/Bridge/Twig/MeasureTwigNode.php @@ -0,0 +1,50 @@ + $body, 'name' => $name, 'var' => $var], [], $lineno, $tag); + $this->extName = $extName ?: MeasureTwigExtension::class; + } + + public function compile(Compiler $compiler) + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('var')) + ->raw(' = ') + ->subcompile($this->getNode('name')) + ->write(";\n") + ->write("\$this->env->getExtension('".$this->extName."')->startMeasure(") + ->subcompile($this->getNode('var')) + ->raw(");\n") + ->subcompile($this->getNode('body')) + ->write("\$this->env->getExtension('".$this->extName."')->stopMeasure(") + ->subcompile($this->getNode('var')) + ->raw(");\n"); + } +} diff --git a/src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php b/src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php new file mode 100644 index 00000000..6926a085 --- /dev/null +++ b/src/DebugBar/Bridge/Twig/MeasureTwigTokenParser.php @@ -0,0 +1,78 @@ +enabled = $enabled; + $this->tagName = $tagName; + $this->extName = $extName; + } + + public function parse(Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + // {% measure 'bar' %} + $name = $this->parser->getExpressionParser()->parseExpression(); + + $stream->expect(Token::BLOCK_END_TYPE); + + // {% endmeasure %} + $body = $this->parser->subparse([$this, 'decideMeasureEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + + if ($this->enabled) { + return new MeasureTwigNode( + $name, + $body, + new AssignNameExpression($this->parser->getVarName(), $token->getLine()), + $lineno, + $this->getTag(), + $this->extName + ); + } + + return $body; + } + + public function getTag() + { + return $this->tagName; + } + + public function decideMeasureEnd(Token $token) + { + return $token->test('end'.$this->getTag()); + } +} diff --git a/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php b/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php index 30238cc2..9dea9b61 100644 --- a/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php +++ b/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php @@ -11,16 +11,17 @@ namespace DebugBar\Bridge\Twig; use DebugBar\DataCollector\TimeDataCollector; -use Twig_Profiler_Profile; +use Twig\Extension\ProfilerExtension; +use Twig\Profiler\Profile; /** * Class TimeableTwigExtensionProfiler * - * Extends Twig_Extension_Profiler to add rendering times to the TimeDataCollector + * Extends ProfilerExtension to add rendering times to the TimeDataCollector * * @package DebugBar\Bridge\Twig */ -class TimeableTwigExtensionProfiler extends \Twig_Extension_Profiler +class TimeableTwigExtensionProfiler extends ProfilerExtension { /** * @var \DebugBar\DataCollector\TimeDataCollector @@ -35,14 +36,14 @@ public function setTimeDataCollector(TimeDataCollector $timeDataCollector) $this->timeDataCollector = $timeDataCollector; } - public function __construct(\Twig_Profiler_Profile $profile, TimeDataCollector $timeDataCollector = null) + public function __construct(Profile $profile, ?TimeDataCollector $timeDataCollector = null) { parent::__construct($profile); $this->timeDataCollector = $timeDataCollector; } - public function enter(Twig_Profiler_Profile $profile) + public function enter(Profile $profile) { if ($this->timeDataCollector && $profile->isTemplate()) { $this->timeDataCollector->startMeasure($profile->getName(), 'template ' . $profile->getName()); @@ -50,7 +51,7 @@ public function enter(Twig_Profiler_Profile $profile) parent::enter($profile); } - public function leave(Twig_Profiler_Profile $profile) + public function leave(Profile $profile) { parent::leave($profile); if ($this->timeDataCollector && $profile->isTemplate()) { diff --git a/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php b/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php index 9e98c191..9dcf0d7a 100644 --- a/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php +++ b/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php @@ -24,7 +24,7 @@ /** * Wrapped a Twig Environment to provide profiling features - * + * * @deprecated */ class TraceableTwigEnvironment extends Twig_Environment @@ -39,7 +39,7 @@ class TraceableTwigEnvironment extends Twig_Environment * @param Twig_Environment $twig * @param TimeDataCollector $timeDataCollector */ - public function __construct(Twig_Environment $twig, TimeDataCollector $timeDataCollector = null) + public function __construct(Twig_Environment $twig, ?TimeDataCollector $timeDataCollector = null) { $this->twig = $twig; $this->timeDataCollector = $timeDataCollector; diff --git a/src/DebugBar/DataCollector/ConfigCollector.php b/src/DebugBar/DataCollector/ConfigCollector.php index 75817ba2..355b661f 100644 --- a/src/DebugBar/DataCollector/ConfigCollector.php +++ b/src/DebugBar/DataCollector/ConfigCollector.php @@ -19,34 +19,6 @@ class ConfigCollector extends DataCollector implements Renderable, AssetProvider protected $data; - // The HTML var dumper requires debug bar users to support the new inline assets, which not all - // may support yet - so return false by default for now. - protected $useHtmlVarDumper = false; - - /** - * Sets a flag indicating whether the Symfony HtmlDumper will be used to dump variables for - * rich variable rendering. - * - * @param bool $value - * @return $this - */ - public function useHtmlVarDumper($value = true) - { - $this->useHtmlVarDumper = $value; - return $this; - } - - /** - * Indicates whether the Symfony HtmlDumper will be used to dump variables for rich variable - * rendering. - * - * @return mixed - */ - public function isHtmlVarDumperUsed() - { - return $this->useHtmlVarDumper; - } - /** * @param array $data * @param string $name diff --git a/src/DebugBar/DataCollector/DataCollector.php b/src/DebugBar/DataCollector/DataCollector.php index 5e3d52f2..8b6c5f84 100644 --- a/src/DebugBar/DataCollector/DataCollector.php +++ b/src/DebugBar/DataCollector/DataCollector.php @@ -10,225 +10,18 @@ namespace DebugBar\DataCollector; -use DebugBar\DataFormatter\DataFormatter; -use DebugBar\DataFormatter\DataFormatterInterface; -use DebugBar\DataFormatter\DebugBarVarDumper; +use DebugBar\DataFormatter\HasDataFormatter; +use DebugBar\DataFormatter\HasXdebugLinks; /** * Abstract class for data collectors */ abstract class DataCollector implements DataCollectorInterface { - private static $defaultDataFormatter; - private static $defaultVarDumper; + use HasDataFormatter, HasXdebugLinks; - protected $dataFormater; - protected $varDumper; - protected $xdebugLinkTemplate = ''; - protected $xdebugShouldUseAjax = false; - protected $xdebugReplacements = array(); + public static $defaultDataFormatter; + public static $defaultVarDumper; - /** - * Sets the default data formater instance used by all collectors subclassing this class - * - * @param DataFormatterInterface $formater - */ - public static function setDefaultDataFormatter(DataFormatterInterface $formater) - { - self::$defaultDataFormatter = $formater; - } - /** - * Returns the default data formater - * - * @return DataFormatterInterface - */ - public static function getDefaultDataFormatter() - { - if (self::$defaultDataFormatter === null) { - self::$defaultDataFormatter = new DataFormatter(); - } - return self::$defaultDataFormatter; - } - - /** - * Sets the data formater instance used by this collector - * - * @param DataFormatterInterface $formater - * @return $this - */ - public function setDataFormatter(DataFormatterInterface $formater) - { - $this->dataFormater = $formater; - return $this; - } - - /** - * @return DataFormatterInterface - */ - public function getDataFormatter() - { - if ($this->dataFormater === null) { - $this->dataFormater = self::getDefaultDataFormatter(); - } - return $this->dataFormater; - } - - /** - * Get an Xdebug Link to a file - * - * @param string $file - * @param int $line - * - * @return array { - * @var string $url - * @var bool $ajax should be used to open the url instead of a normal links - * } - */ - public function getXdebugLink($file, $line = 1) - { - if (count($this->xdebugReplacements)) { - $file = strtr($file, $this->xdebugReplacements); - } - - $url = strtr($this->getXdebugLinkTemplate(), ['%f' => $file, '%l' => $line]); - if ($url) { - return ['url' => $url, 'ajax' => $this->getXdebugShouldUseAjax()]; - } - } - - /** - * Sets the default variable dumper used by all collectors subclassing this class - * - * @param DebugBarVarDumper $varDumper - */ - public static function setDefaultVarDumper(DebugBarVarDumper $varDumper) - { - self::$defaultVarDumper = $varDumper; - } - - /** - * Returns the default variable dumper - * - * @return DebugBarVarDumper - */ - public static function getDefaultVarDumper() - { - if (self::$defaultVarDumper === null) { - self::$defaultVarDumper = new DebugBarVarDumper(); - } - return self::$defaultVarDumper; - } - - /** - * Sets the variable dumper instance used by this collector - * - * @param DebugBarVarDumper $varDumper - * @return $this - */ - public function setVarDumper(DebugBarVarDumper $varDumper) - { - $this->varDumper = $varDumper; - return $this; - } - - /** - * Gets the variable dumper instance used by this collector; note that collectors using this - * instance need to be sure to return the static assets provided by the variable dumper. - * - * @return DebugBarVarDumper - */ - public function getVarDumper() - { - if ($this->varDumper === null) { - $this->varDumper = self::getDefaultVarDumper(); - } - return $this->varDumper; - } - - /** - * @deprecated - */ - public function formatVar($var) - { - return $this->getDataFormatter()->formatVar($var); - } - - /** - * @deprecated - */ - public function formatDuration($seconds) - { - return $this->getDataFormatter()->formatDuration($seconds); - } - - /** - * @deprecated - */ - public function formatBytes($size, $precision = 2) - { - return $this->getDataFormatter()->formatBytes($size, $precision); - } - - /** - * @return string - */ - public function getXdebugLinkTemplate() - { - if (empty($this->xdebugLinkTemplate) && !empty(ini_get('xdebug.file_link_format'))) { - $this->xdebugLinkTemplate = ini_get('xdebug.file_link_format'); - } - - return $this->xdebugLinkTemplate; - } - - /** - * @param string $xdebugLinkTemplate - * @param bool $shouldUseAjax - */ - public function setXdebugLinkTemplate($xdebugLinkTemplate, $shouldUseAjax = false) - { - if ($xdebugLinkTemplate === 'idea') { - $this->xdebugLinkTemplate = 'http://localhost:63342/api/file/?file=%f&line=%l'; - $this->xdebugShouldUseAjax = true; - } else { - $this->xdebugLinkTemplate = $xdebugLinkTemplate; - $this->xdebugShouldUseAjax = $shouldUseAjax; - } - } - - /** - * @return bool - */ - public function getXdebugShouldUseAjax() - { - return $this->xdebugShouldUseAjax; - } - - /** - * returns an array of filename-replacements - * - * this is useful f.e. when using vagrant or remote servers, - * where the path of the file is different between server and - * development environment - * - * @return array key-value-pairs of replacements, key = path on server, value = replacement - */ - public function getXdebugReplacements() - { - return $this->xdebugReplacements; - } - - /** - * @param array $xdebugReplacements - */ - public function setXdebugReplacements($xdebugReplacements) - { - $this->xdebugReplacements = $xdebugReplacements; - } - - public function setXdebugReplacement($serverPath, $replacement) - { - $this->xdebugReplacements[$serverPath] = $replacement; - } } diff --git a/src/DebugBar/DataCollector/ExceptionsCollector.php b/src/DebugBar/DataCollector/ExceptionsCollector.php index 3fcac398..729f020c 100644 --- a/src/DebugBar/DataCollector/ExceptionsCollector.php +++ b/src/DebugBar/DataCollector/ExceptionsCollector.php @@ -10,7 +10,7 @@ namespace DebugBar\DataCollector; -use Exception; +use Throwable; use Symfony\Component\Debug\Exception\FatalThrowableError; /** @@ -21,17 +21,13 @@ class ExceptionsCollector extends DataCollector implements Renderable protected $exceptions = array(); protected $chainExceptions = false; - // The HTML var dumper requires debug bar users to support the new inline assets, which not all - // may support yet - so return false by default for now. - protected $useHtmlVarDumper = false; - /** * Adds an exception to be profiled in the debug bar * - * @param Exception $e + * @param \Exception $e * @deprecated in favor on addThrowable */ - public function addException(Exception $e) + public function addException(\Exception $e) { $this->addThrowable($e); } @@ -39,7 +35,7 @@ public function addException(Exception $e) /** * Adds a Throwable to be profiled in the debug bar * - * @param \Throwable $e + * @param Throwable $e */ public function addThrowable($e) { @@ -60,37 +56,73 @@ public function setChainExceptions($chainExceptions = true) } /** - * Returns the list of exceptions being profiled + * Start collecting warnings, notices and deprecations * - * @return array[\Throwable] + * @param bool $preserveOriginalHandler */ - public function getExceptions() - { - return $this->exceptions; + public function collectWarnings($preserveOriginalHandler = true) { + $self = $this; + $originalHandler = $preserveOriginalHandler ? set_error_handler(null) : null; + + set_error_handler(function ($errno, $errstr, $errfile, $errline) use ($self, $originalHandler) { + $self->addWarning($errno, $errstr, $errfile, $errline); + + if ($originalHandler) { + return call_user_func($originalHandler, $errno, $errstr, $errfile, $errline); + } + + return false; + }); } /** - * Sets a flag indicating whether the Symfony HtmlDumper will be used to dump variables for - * rich variable rendering. + * Adds an warning to be profiled in the debug bar * - * @param bool $value - * @return $this + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * @return void */ - public function useHtmlVarDumper($value = true) + public function addWarning($errno, $errstr, $errfile = '', $errline = 0) { - $this->useHtmlVarDumper = $value; - return $this; + $errorTypes = array( + 1 => 'E_ERROR', + 2 => 'E_WARNING', + 4 => 'E_PARSE', + 8 => 'E_NOTICE', + 16 => 'E_CORE_ERROR', + 32 => 'E_CORE_WARNING', + 64 => 'E_COMPILE_ERROR', + 128 => 'E_COMPILE_WARNING', + 256 => 'E_USER_ERROR', + 512 => 'E_USER_WARNING', + 1024 => 'E_USER_NOTICE', + 2048 => 'E_STRICT', + 4096 => 'E_RECOVERABLE_ERROR', + 8192 => 'E_DEPRECATED', + 16384 => 'E_USER_DEPRECATED' + ); + + $this->exceptions[] = array( + 'type' => $errorTypes[$errno] ?? 'UNKNOWN', + 'message' => $errstr, + 'code' => $errno, + 'file' => $this->normalizeFilePath($errfile), + 'line' => $errline, + 'xdebug_link' => $this->getXdebugLink($errfile, $errline) + ); } + /** - * Indicates whether the Symfony HtmlDumper will be used to dump variables for rich variable - * rendering. + * Returns the list of exceptions being profiled * - * @return mixed + * @return array */ - public function isHtmlVarDumperUsed() + public function getExceptions() { - return $this->useHtmlVarDumper; + return $this->exceptions; } public function collect() @@ -104,44 +136,101 @@ public function collect() /** * Returns exception data as an array * - * @param Exception $e + * @param \Exception $e * @return array * @deprecated in favor on formatThrowableData */ - public function formatExceptionData(Exception $e) + public function formatExceptionData(\Exception $e) { return $this->formatThrowableData($e); } + /** + * Returns Throwable trace as an formated array + * + * @return array + */ + public function formatTrace(array $trace) + { + if (! empty($this->xdebugReplacements)) { + $trace = array_map(function ($track) { + if (isset($track['file'])) { + $track['file'] = $this->normalizeFilePath($track['file']); + } + return $track; + }, $trace); + } + + // Remove large objects from the trace + $trace = array_map(function ($track) { + if (isset($track['args'])) { + foreach ($track['args'] as $key => $arg) { + if (is_object($arg)) { + $track['args'][$key] = '[object ' . $this->getDataFormatter()->formatClassName($arg) . ']'; + } + } + } + return $track; + }, $trace); + + return $trace; + } + + /** + * Returns Throwable data as an string + * + * @param Throwable $e + * @return string + */ + public function formatTraceAsString($e) + { + if (! empty($this->xdebugReplacements)) { + return implode("\n", array_map(function ($track) { + $track = explode(' ', $track); + if (isset($track[1])) { + $track[1] = $this->normalizeFilePath($track[1]); + } + + return implode(' ', $track); + }, explode("\n", $e->getTraceAsString()))); + } + + return $e->getTraceAsString(); + } + /** * Returns Throwable data as an array * - * @param \Throwable $e + * @param Throwable|array $e * @return array */ public function formatThrowableData($e) { + if (is_array($e)) { + return $e; + } + $filePath = $e->getFile(); if ($filePath && file_exists($filePath)) { $lines = file($filePath); $start = $e->getLine() - 4; $lines = array_slice($lines, $start < 0 ? 0 : $start, 7); } else { - $lines = array("Cannot open the file ($filePath) in which the exception occurred "); + $lines = array('Cannot open the file ('.$this->normalizeFilePath($filePath).') in which the exception occurred'); } $traceHtml = null; if ($this->isHtmlVarDumperUsed()) { - $traceHtml = $this->getVarDumper()->renderVar($e->getTrace()); + $traceHtml = $this->getVarDumper()->renderVar($this->formatTrace($e->getTrace())); } return array( 'type' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), - 'file' => $filePath, + 'file' => $this->normalizeFilePath($filePath), 'line' => $e->getLine(), - 'stack_trace' => $e->getTraceAsString(), + 'stack_trace' => $traceHtml ? null : $this->formatTraceAsString($e), 'stack_trace_html' => $traceHtml, 'surrounding_lines' => $lines, 'xdebug_link' => $this->getXdebugLink($filePath, $e->getLine()) diff --git a/src/DebugBar/DataCollector/MemoryCollector.php b/src/DebugBar/DataCollector/MemoryCollector.php index 1a34f3bd..07da408a 100644 --- a/src/DebugBar/DataCollector/MemoryCollector.php +++ b/src/DebugBar/DataCollector/MemoryCollector.php @@ -17,8 +17,24 @@ class MemoryCollector extends DataCollector implements Renderable { protected $realUsage = false; + protected $memoryRealStart = 0; + + protected $memoryStart = 0; + protected $peakUsage = 0; + protected $precision = 0; + + /** + * Set the precision of the 'peak_usage_str' output. + * + * @param int $precision + */ + public function setPrecision($precision) + { + $this->precision = $precision; + } + /** * Returns whether total allocated memory page size is used instead of actual used memory size * by the application. See $real_usage parameter on memory_get_peak_usage for details. @@ -41,6 +57,17 @@ public function setRealUsage($realUsage) $this->realUsage = $realUsage; } + /** + * Reset memory baseline, to measure multiple requests in a long running process + * + * @return void + */ + public function resetMemoryBaseline() + { + $this->memoryStart = memory_get_usage(false); + $this->memoryRealStart = memory_get_usage(true); + } + /** * Returns the peak memory usage * @@ -48,7 +75,7 @@ public function setRealUsage($realUsage) */ public function getPeakUsage() { - return $this->peakUsage; + return $this->peakUsage - ($this->realUsage ? $this->memoryRealStart : $this->memoryStart); } /** @@ -66,8 +93,8 @@ public function collect() { $this->updatePeakUsage(); return array( - 'peak_usage' => $this->peakUsage, - 'peak_usage_str' => $this->getDataFormatter()->formatBytes($this->peakUsage, 0) + 'peak_usage' => $this->getPeakUsage(), + 'peak_usage_str' => $this->getDataFormatter()->formatBytes($this->getPeakUsage(), $this->precision) ); } diff --git a/src/DebugBar/DataCollector/MessagesCollector.php b/src/DebugBar/DataCollector/MessagesCollector.php index 8859e7d0..6e15cbd8 100644 --- a/src/DebugBar/DataCollector/MessagesCollector.php +++ b/src/DebugBar/DataCollector/MessagesCollector.php @@ -10,28 +10,31 @@ namespace DebugBar\DataCollector; +use DebugBar\DataFormatter\HasXdebugLinks; use Psr\Log\AbstractLogger; -use DebugBar\DataFormatter\DataFormatterInterface; -use DebugBar\DataFormatter\DebugBarVarDumper; +use DebugBar\DataFormatter\HasDataFormatter; /** * Provides a way to log messages */ class MessagesCollector extends AbstractLogger implements DataCollectorInterface, MessagesAggregateInterface, Renderable, AssetProvider { + use HasDataFormatter, HasXdebugLinks; + protected $name; protected $messages = array(); protected $aggregates = array(); - protected $dataFormater; + /** @var bool */ + protected $collectFile = false; - protected $varDumper; + /** @var int */ + protected $backtraceLimit = 5; - // The HTML var dumper requires debug bar users to support the new inline assets, which not all - // may support yet - so return false by default for now. - protected $useHtmlVarDumper = false; + /** @var array */ + protected $backtraceExcludePaths = ['/vendor/']; /** * @param string $name @@ -41,77 +44,83 @@ public function __construct($name = 'messages') $this->name = $name; } - /** - * Sets the data formater instance used by this collector - * - * @param DataFormatterInterface $formater - * @return $this - */ - public function setDataFormatter(DataFormatterInterface $formater) + /** @return void */ + public function collectFileTrace($enabled = true) { - $this->dataFormater = $formater; - return $this; + $this->collectFile = $enabled; } /** - * @return DataFormatterInterface + * @param int $limit + * + * @return void */ - public function getDataFormatter() + public function limitBacktrace($limit) { - if ($this->dataFormater === null) { - $this->dataFormater = DataCollector::getDefaultDataFormatter(); - } - return $this->dataFormater; + $this->backtraceLimit = $limit; } /** - * Sets the variable dumper instance used by this collector + * Set paths to exclude from the backtrace * - * @param DebugBarVarDumper $varDumper - * @return $this + * @param array $excludePaths Array of file paths to exclude from backtrace */ - public function setVarDumper(DebugBarVarDumper $varDumper) + public function addBacktraceExcludePaths($excludePaths) { - $this->varDumper = $varDumper; - return $this; + $this->backtraceExcludePaths = array_merge($this->backtraceExcludePaths, $excludePaths); } /** - * Gets the variable dumper instance used by this collector + * Check if the given file is to be excluded from analysis * - * @return DebugBarVarDumper + * @param string $file + * @return bool */ - public function getVarDumper() + protected function fileIsInExcludedPath($file) { - if ($this->varDumper === null) { - $this->varDumper = DataCollector::getDefaultVarDumper(); + $normalizedPath = str_replace('\\', '/', $file); + + foreach ($this->backtraceExcludePaths as $excludedPath) { + if (strpos($normalizedPath, $excludedPath) !== false) { + return true; + } } - return $this->varDumper; + + return false; } /** - * Sets a flag indicating whether the Symfony HtmlDumper will be used to dump variables for - * rich variable rendering. Be sure to set this flag before logging any messages for the - * first time. + * @param string|null $messageHtml + * @param mixed $message * - * @param bool $value - * @return $this + * @return string|null */ - public function useHtmlVarDumper($value = true) + protected function customizeMessageHtml($messageHtml, $message) { - $this->useHtmlVarDumper = $value; - return $this; + $pos = strpos((string) $messageHtml, 'sf-dump-expanded'); + if ($pos !== false) { + $messageHtml = substr_replace($messageHtml, 'sf-dump-compact', $pos, 16); + } + + return $messageHtml; } /** - * Indicates whether the Symfony HtmlDumper will be used to dump variables for rich variable - * rendering. + * @param array $stacktrace * - * @return mixed + * @return array */ - public function isHtmlVarDumperUsed() + protected function getStackTraceItem($stacktrace) { - return $this->useHtmlVarDumper; + foreach ($stacktrace as $trace) { + if (!isset($trace['file']) || $this->fileIsInExcludedPath($trace['file'])) { + continue; + } + + return $trace; + } + + return $stacktrace[0]; } /** @@ -134,12 +143,19 @@ public function addMessage($message, $label = 'info', $isString = true) } $isString = false; } + + $stackItem = []; + if ($this->collectFile) { + $stackItem = $this->getStackTraceItem(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $this->backtraceLimit)); + } + $this->messages[] = array( 'message' => $messageText, - 'message_html' => $messageHtml, + 'message_html' => $this->customizeMessageHtml($messageHtml, $message), 'is_string' => $isString, 'label' => $label, - 'time' => microtime(true) + 'time' => microtime(true), + 'xdebug_link' => $stackItem ? $this->getXdebugLink($stackItem['file'], $stackItem['line'] ?? null) : null, ); } @@ -150,6 +166,10 @@ public function addMessage($message, $label = 'info', $isString = true) */ public function aggregate(MessagesAggregateInterface $messages) { + if ($this->collectFile && method_exists($messages, 'collectFileTrace')) { + $messages->collectFileTrace(); + } + $this->aggregates[] = $messages; } @@ -195,7 +215,7 @@ public function log($level, $message, array $context = array()): void /** * Interpolates context values into the message placeholders. * - * @param $message + * @param string $message * @param array $context * @return string */ @@ -204,9 +224,24 @@ function interpolate($message, array $context = array()) // build a replacement array with braces around the context keys $replace = array(); foreach ($context as $key => $val) { + $placeholder = '{' . $key . '}'; + if (strpos($message, $placeholder) === false) { + continue; + } // check that the value can be cast to string - if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { - $replace['{' . $key . '}'] = $val; + if (null === $val || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { + $replace[$placeholder] = $val; + } elseif ($val instanceof \DateTimeInterface) { + $replace[$placeholder] = $val->format("Y-m-d\TH:i:s.uP"); + } elseif ($val instanceof \UnitEnum) { + $replace[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name; + } elseif (is_object($val)) { + $replace[$placeholder] = '[object ' . $this->getDataFormatter()->formatClassName($val) . ']'; + } elseif (is_array($val)) { + $json = @json_encode($val); + $replace[$placeholder] = false === $json ? 'null' : 'array' . $json; + } else { + $replace[$placeholder] = '['.gettype($val).']'; } } diff --git a/src/DebugBar/DataCollector/ObjectCountCollector.php b/src/DebugBar/DataCollector/ObjectCountCollector.php new file mode 100644 index 00000000..cbda75ea --- /dev/null +++ b/src/DebugBar/DataCollector/ObjectCountCollector.php @@ -0,0 +1,138 @@ + 'Count']; + + /** + * @param string $name + * @param string $icon + */ + public function __construct($name = 'counter', $icon = 'cubes') + { + $this->name = $name; + $this->icon = $icon; + } + + /** + * Allows to define an array to map internal keys to human-readable labels + */ + public function setKeyMap(array $keyMap) + { + $this->keyMap = $keyMap; + } + + /** + * Allows to add a summary row + */ + public function collectCountSummary(bool $enable = true) + { + $this->collectSummary = $enable; + } + + /** + * @param string|mixed $class + * @param int $count + * @param string $key + */ + public function countClass($class, $count = 1, $key = 'value') { + if (! is_string($class)) { + $class = get_class($class); + } + + if (!isset($this->classList[$class])) { + $this->classList[$class] = []; + } + + if ($this->collectSummary) { + $this->classSummary[$key] = ($this->classSummary[$key] ?? 0) + $count; + } + + $this->classList[$class][$key] = ($this->classList[$class][$key] ?? 0) + $count; + $this->classCount += $count; + } + + /** + * {@inheritDoc} + */ + public function collect() + { + uasort($this->classList, fn($a, $b) => array_sum($b) <=> array_sum($a)); + + $collect = [ + 'data' => $this->classList, + 'count' => $this->classCount, + 'key_map' => $this->keyMap, + 'is_counter' => true + ]; + + if ($this->collectSummary) { + $collect['badges'] = $this->classSummary; + } + + if (! $this->getXdebugLinkTemplate()) { + return $collect; + } + + foreach ($this->classList as $class => $count) { + $reflector = class_exists($class) ? new \ReflectionClass($class) : null; + + if ($reflector && $link = $this->getXdebugLink($reflector->getFileName())) { + $collect['data'][$class]['xdebug_link'] = $link; + } + } + + return $collect; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + $name = $this->getName(); + + return [ + "$name" => [ + 'icon' => $this->icon, + 'widget' => 'PhpDebugBar.Widgets.TableVariableListWidget', + 'map' => "$name", + 'default' => '{}' + ], + "$name:badge" => [ + 'map' => "$name.count", + 'default' => 0 + ] + ]; + } +} diff --git a/src/DebugBar/DataCollector/PDO/PDOCollector.php b/src/DebugBar/DataCollector/PDO/PDOCollector.php index 77e36307..dede553c 100644 --- a/src/DebugBar/DataCollector/PDO/PDOCollector.php +++ b/src/DebugBar/DataCollector/PDO/PDOCollector.php @@ -20,11 +20,15 @@ class PDOCollector extends DataCollector implements Renderable, AssetProvider protected $sqlQuotationChar = '<>'; + protected $durationBackground = false; + + protected $slowThreshold; + /** * @param \PDO $pdo * @param TimeDataCollector $timeCollector */ - public function __construct(\PDO $pdo = null, TimeDataCollector $timeCollector = null) + public function __construct(?\PDO $pdo = null, ?TimeDataCollector $timeCollector = null) { $this->timeCollector = $timeCollector; if ($pdo !== null) { @@ -43,6 +47,26 @@ public function setRenderSqlWithParams($enabled = true, $quotationChar = '<>') $this->sqlQuotationChar = $quotationChar; } + /** + * Enable/disable the shaded duration background on queries + * + * @param bool $enabled + */ + public function setDurationBackground($enabled) + { + $this->durationBackground = $enabled; + } + + /** + * Highlights queries that exceed the threshold + * + * @param int|float $threshold miliseconds value + */ + public function setSlowThreshold($threshold) + { + $this->slowThreshold = $threshold / 1000; + } + /** * @return bool */ @@ -126,7 +150,7 @@ public function collect() * @param string|null $connectionName the pdo connection (eg default | read | write) * @return array */ - protected function collectPDO(TraceablePDO $pdo, TimeDataCollector $timeCollector = null, $connectionName = null) + protected function collectPDO(TraceablePDO $pdo, ?TimeDataCollector $timeCollector = null, $connectionName = null) { if (empty($connectionName) || $connectionName == 'default') { $connectionName = 'pdo'; @@ -137,6 +161,7 @@ protected function collectPDO(TraceablePDO $pdo, TimeDataCollector $timeCollecto foreach ($pdo->getExecutedStatements() as $stmt) { $stmts[] = array( 'sql' => $this->renderSqlWithParams ? $stmt->getSqlWithParams($this->sqlQuotationChar) : $stmt->getSql(), + 'type' => $stmt->getQueryType(), 'row_count' => $stmt->getRowCount(), 'stmt_id' => $stmt->getPreparedId(), 'prepared_stmt' => $stmt->getSql(), @@ -149,18 +174,37 @@ protected function collectPDO(TraceablePDO $pdo, TimeDataCollector $timeCollecto 'end_memory_str' => $this->getDataFormatter()->formatBytes($stmt->getEndMemory()), 'is_success' => $stmt->isSuccess(), 'error_code' => $stmt->getErrorCode(), - 'error_message' => $stmt->getErrorMessage() + 'error_message' => $stmt->getErrorMessage(), + 'slow' => $this->slowThreshold && $this->slowThreshold <= $stmt->getDuration() ); if ($timeCollector !== null) { $timeCollector->addMeasure($stmt->getSql(), $stmt->getStartTime(), $stmt->getEndTime(), array(), $connectionName); } } + $totalTime = $pdo->getAccumulatedStatementsDuration(); + if ($this->durationBackground && $totalTime > 0) { + // For showing background measure on Queries tab + $start_percent = 0; + foreach ($stmts as $i => $stmt) { + if (!isset($stmt['duration'])) { + continue; + } + + $width_percent = $stmt['duration'] / $totalTime * 100; + $stmts[$i] = array_merge($stmt, [ + 'start_percent' => round($start_percent, 3), + 'width_percent' => round($width_percent, 3), + ]); + $start_percent += $width_percent; + } + } + return array( 'nb_statements' => count($stmts), 'nb_failed_statements' => count($pdo->getFailedExecutedStatements()), - 'accumulated_duration' => $pdo->getAccumulatedStatementsDuration(), - 'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($pdo->getAccumulatedStatementsDuration()), + 'accumulated_duration' => $totalTime, + 'accumulated_duration_str' => $this->getDataFormatter()->formatDuration($totalTime), 'memory_usage' => $pdo->getMemoryUsage(), 'memory_usage_str' => $this->getDataFormatter()->formatBytes($pdo->getPeakMemoryUsage()), 'peak_memory_usage' => $pdo->getPeakMemoryUsage(), diff --git a/src/DebugBar/DataCollector/PDO/TraceablePDO.php b/src/DebugBar/DataCollector/PDO/TraceablePDO.php index 57882264..04aaba00 100644 --- a/src/DebugBar/DataCollector/PDO/TraceablePDO.php +++ b/src/DebugBar/DataCollector/PDO/TraceablePDO.php @@ -31,6 +31,8 @@ public function __construct(PDO $pdo) */ public function beginTransaction() : bool { + $this->addPdoEvent('Begin Transaction'); + return $this->pdo->beginTransaction(); } @@ -42,6 +44,8 @@ public function beginTransaction() : bool */ public function commit() : bool { + $this->addPdoEvent('Commit Transaction'); + return $this->pdo->commit(); } @@ -180,6 +184,8 @@ public function quote($string, $parameter_type = PDO::PARAM_STR) */ public function rollBack() : bool { + $this->addPdoEvent('Rollback Transaction'); + return $this->pdo->rollBack(); } @@ -241,6 +247,21 @@ public function addExecutedStatement(TracedStatement $stmt) : void $this->executedStatements[] = $stmt; } + /** + * Adds a PDO event + * + * @param string $event + */ + public function addPdoEvent(string $event) : void + { + $stmt = new TracedStatement($event); + $stmt->setQueryType('transaction'); + $stmt->start(); + $stmt->end(); + + $this->executedStatements[] = $stmt; + } + /** * Returns the accumulated execution time of statements * diff --git a/src/DebugBar/DataCollector/PDO/TraceablePDOStatement.php b/src/DebugBar/DataCollector/PDO/TraceablePDOStatement.php index 011bbfe4..da037207 100644 --- a/src/DebugBar/DataCollector/PDO/TraceablePDOStatement.php +++ b/src/DebugBar/DataCollector/PDO/TraceablePDOStatement.php @@ -44,7 +44,7 @@ public function bindColumn($column, &$param, $type = null, $maxlen = null, $driv { $this->boundParameters[$column] = $param; $args = array_merge([$column, &$param], array_slice(func_get_args(), 2)); - return call_user_func_array(['parent', 'bindColumn'], $args); + return parent::bindColumn(...$args); } /** @@ -66,7 +66,7 @@ public function bindParam($parameter, &$variable, $data_type = PDO::PARAM_STR, $ { $this->boundParameters[$parameter] = $variable; $args = array_merge([$parameter, &$variable], array_slice(func_get_args(), 2)); - return call_user_func_array(['parent', 'bindParam'], $args); + return parent::bindParam(...$args); } /** @@ -84,7 +84,7 @@ public function bindParam($parameter, &$variable, $data_type = PDO::PARAM_STR, $ public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR) : bool { $this->boundParameters[$parameter] = $value; - return call_user_func_array(['parent', 'bindValue'], func_get_args()); + return parent::bindValue(...func_get_args()); } /** diff --git a/src/DebugBar/DataCollector/PDO/TracedStatement.php b/src/DebugBar/DataCollector/PDO/TracedStatement.php index 9111489b..77a37b20 100644 --- a/src/DebugBar/DataCollector/PDO/TracedStatement.php +++ b/src/DebugBar/DataCollector/PDO/TracedStatement.php @@ -9,6 +9,8 @@ class TracedStatement { protected $sql; + protected $type; + protected $rowCount; protected $parameters; @@ -41,6 +43,14 @@ public function __construct(string $sql, array $params = [], ?string $preparedId $this->preparedId = $preparedId; } + /** + * @param string $type + */ + public function setQueryType(string $type) : void + { + $this->type = $type; + } + /** * @param null $startTime * @param null $startMemory @@ -57,7 +67,7 @@ public function start($startTime = null, $startMemory = null) : void * @param float $endTime * @param int $endMemory */ - public function end(\Exception $exception = null, int $rowCount = 0, float $endTime = null, int $endMemory = null) : void + public function end(?\Exception $exception = null, int $rowCount = 0, ?float $endTime = null, ?int $endMemory = null) : void { $this->endTime = $endTime ?: microtime(true); $this->duration = $this->endTime - $this->startTime; @@ -114,9 +124,12 @@ public function getSqlWithParams(string $quotationChar = '<>') : string foreach ($this->parameters as $k => $v) { - $backRefSafeV = strtr($v, $cleanBackRefCharMap); - - $v = "$quoteLeft$backRefSafeV$quoteRight"; + if (null === $v) { + $v = 'NULL'; + } else { + $backRefSafeV = strtr($v, $cleanBackRefCharMap); + $v = "$quoteLeft$backRefSafeV$quoteRight"; + } if (is_numeric($k)) { $marker = "\?"; @@ -274,4 +287,14 @@ public function getErrorMessage() : string { return $this->exception !== null ? $this->exception->getMessage() : ''; } + + /** + * Returns the query type + * + * @return string + */ + public function getQueryType() : string + { + return $this->type !== null ? $this->type : ''; + } } diff --git a/src/DebugBar/DataCollector/PhpInfoCollector.php b/src/DebugBar/DataCollector/PhpInfoCollector.php index 15a3f22d..a200b499 100644 --- a/src/DebugBar/DataCollector/PhpInfoCollector.php +++ b/src/DebugBar/DataCollector/PhpInfoCollector.php @@ -42,7 +42,7 @@ public function getWidgets() return array( "php_version" => array( "icon" => "code", - "tooltip" => "Version", + "tooltip" => "PHP Version", "map" => "php.version", "default" => "" ), diff --git a/src/DebugBar/DataCollector/RequestDataCollector.php b/src/DebugBar/DataCollector/RequestDataCollector.php index 6bd781ee..a6018fe0 100644 --- a/src/DebugBar/DataCollector/RequestDataCollector.php +++ b/src/DebugBar/DataCollector/RequestDataCollector.php @@ -15,54 +15,89 @@ */ class RequestDataCollector extends DataCollector implements Renderable, AssetProvider { - // The HTML var dumper requires debug bar users to support the new inline assets, which not all - // may support yet - so return false by default for now. - protected $useHtmlVarDumper = false; + /** + * @var array[] + */ + private $blacklist = [ + '_GET' => [], + '_POST' => [], + '_COOKIE' => [], + '_SESSION' => [], + ]; /** - * Sets a flag indicating whether the Symfony HtmlDumper will be used to dump variables for - * rich variable rendering. - * - * @param bool $value - * @return $this + * @return array */ - public function useHtmlVarDumper($value = true) + public function collect() { - $this->useHtmlVarDumper = $value; - return $this; + $vars = array_keys($this->blacklist); + $data = array(); + + foreach ($vars as $var) { + if (! isset($GLOBALS[$var])) { + continue; + } + + $key = "$" . $var; + $value = $this->masked($GLOBALS[$var], $var); + + if ($this->isHtmlVarDumperUsed()) { + $data[$key] = $this->getVarDumper()->renderVar($value); + } else { + $data[$key] = $this->getDataFormatter()->formatVar($value); + } + } + + return $data; } /** - * Indicates whether the Symfony HtmlDumper will be used to dump variables for rich variable - * rendering. + * Hide a sensitive value within one of the superglobal arrays. * - * @return mixed + * @param string $superGlobalName The name of the superglobal array, e.g. '_GET' + * @param string|array $key The key within the superglobal + * @return void */ - public function isHtmlVarDumperUsed() + public function hideSuperglobalKeys($superGlobalName, $keys) { - return $this->useHtmlVarDumper; + if (!is_array($keys)) { + $keys = [$keys]; + } + + if (!isset($this->blacklist[$superGlobalName])) { + $this->blacklist[$superGlobalName] = []; + } + + foreach ($keys as $key) { + $this->blacklist[$superGlobalName][] = $key; + } } /** - * @return array + * Checks all values within the given superGlobal array. + * + * Blacklisted values will be replaced by a equal length string containing + * only '*' characters for string values. + * Non-string values will be replaced with a fixed asterisk count. + * + * @param array|\ArrayAccess $superGlobal One of the superglobal arrays + * @param string $superGlobalName The name of the superglobal array, e.g. '_GET' + * + * @return array $values without sensitive data */ - public function collect() + private function masked($superGlobal, $superGlobalName) { - $vars = array('_GET', '_POST', '_SESSION', '_COOKIE', '_SERVER'); - $data = array(); + $blacklisted = $this->blacklist[$superGlobalName]; - foreach ($vars as $var) { - if (isset($GLOBALS[$var])) { - $key = "$" . $var; - if ($this->isHtmlVarDumperUsed()) { - $data[$key] = $this->getVarDumper()->renderVar($GLOBALS[$var]); - } else { - $data[$key] = $this->getDataFormatter()->formatVar($GLOBALS[$var]); - } + $values = $superGlobal; + + foreach ($blacklisted as $key) { + if (isset($superGlobal[$key])) { + $values[$key] = str_repeat('*', is_string($superGlobal[$key]) ? strlen($superGlobal[$key]) : 3); } } - return $data; + return $values; } /** diff --git a/src/DebugBar/DataCollector/TimeDataCollector.php b/src/DebugBar/DataCollector/TimeDataCollector.php index 5794ccd7..822baa4e 100644 --- a/src/DebugBar/DataCollector/TimeDataCollector.php +++ b/src/DebugBar/DataCollector/TimeDataCollector.php @@ -38,6 +38,11 @@ class TimeDataCollector extends DataCollector implements Renderable */ protected $measures = array(); + /** + * @var bool + */ + protected $memoryMeasure = false; + /** * @param float $requestStartTime */ @@ -51,6 +56,15 @@ public function __construct($requestStartTime = null) } } $this->requestStartTime = (float)$requestStartTime; + static::getDefaultDataFormatter(); // initializes formatter for lineal timeline + } + + /** + * Starts memory measuring + */ + public function showMemoryUsage() + { + $this->memoryMeasure = true; } /** @@ -59,14 +73,17 @@ public function __construct($requestStartTime = null) * @param string $name Internal name, used to stop the measure * @param string|null $label Public name * @param string|null $collector The source of the collector + * @param string|null $group The group for aggregates */ - public function startMeasure($name, $label = null, $collector = null) + public function startMeasure($name, $label = null, $collector = null, $group = null) { $start = microtime(true); $this->startedMeasures[$name] = array( 'label' => $label ?: $name, 'start' => $start, - 'collector' => $collector + 'memory' => $this->memoryMeasure ? memory_get_usage(false) : null, + 'collector' => $collector, + 'group' => $group, ); } @@ -94,12 +111,16 @@ public function stopMeasure($name, $params = array()) if (!$this->hasStartedMeasure($name)) { throw new DebugBarException("Failed stopping measure '$name' because it hasn't been started"); } + if (! is_null($this->startedMeasures[$name]['memory'])) { + $params['memoryUsage'] = memory_get_usage(false) - $this->startedMeasures[$name]['memory']; + } $this->addMeasure( $this->startedMeasures[$name]['label'], $this->startedMeasures[$name]['start'], $end, $params, - $this->startedMeasures[$name]['collector'] + $this->startedMeasures[$name]['collector'], + $this->startedMeasures[$name]['group'] ); unset($this->startedMeasures[$name]); } @@ -112,9 +133,15 @@ public function stopMeasure($name, $params = array()) * @param float $end * @param array $params * @param string|null $collector + * @param string|null $group */ - public function addMeasure($label, $start, $end, $params = array(), $collector = null) + public function addMeasure($label, $start, $end, $params = array(), $collector = null, $group = null) { + if (isset($params['memoryUsage'])) { + $memory = $this->memoryMeasure ? $params['memoryUsage'] : 0; + unset($params['memoryUsage']); + } + $this->measures[] = array( 'label' => $label, 'start' => $start, @@ -123,8 +150,11 @@ public function addMeasure($label, $start, $end, $params = array(), $collector = 'relative_end' => $end - $this->requestEndTime, 'duration' => $end - $start, 'duration_str' => $this->getDataFormatter()->formatDuration($end - $start), + 'memory' => $memory ?? 0, + 'memory_str' => $this->getDataFormatter()->formatBytes($memory ?? 0), 'params' => $params, - 'collector' => $collector + 'collector' => $collector, + 'group' => $group, ); } @@ -134,12 +164,13 @@ public function addMeasure($label, $start, $end, $params = array(), $collector = * @param string $label * @param \Closure $closure * @param string|null $collector + * @param string|null $group * @return mixed */ - public function measure($label, \Closure $closure, $collector = null) + public function measure($label, \Closure $closure, $collector = null, $group = null) { $name = spl_object_hash($closure); - $this->startMeasure($name, $label, $collector); + $this->startMeasure($name, $label, $collector, $group); $result = $closure(); $params = is_array($result) ? $result : array(); $this->stopMeasure($name, $params); @@ -208,6 +239,7 @@ public function collect() }); return array( + 'count' => count($this->measures), 'start' => $this->requestStartTime, 'end' => $this->requestEndTime, 'duration' => $this->getRequestDuration(), @@ -234,6 +266,7 @@ public function getWidgets() "icon" => "clock-o", "tooltip" => "Request Duration", "map" => "time.duration_str", + 'link' => 'timeline', "default" => "'0ms'" ), "timeline" => array( diff --git a/src/DebugBar/DataFormatter/DataFormatter.php b/src/DebugBar/DataFormatter/DataFormatter.php index d933f346..b77f175c 100644 --- a/src/DebugBar/DataFormatter/DataFormatter.php +++ b/src/DebugBar/DataFormatter/DataFormatter.php @@ -84,4 +84,23 @@ public function formatBytes($size, $precision = 2) $suffixes = array('B', 'KB', 'MB', 'GB', 'TB'); return $sign . round(pow(1024, $base - floor($base)), $precision) . $suffixes[(int) floor($base)]; } + + /** + * @param object $object + * @return string + */ + public function formatClassName($object) + { + $class = \get_class($object); + + if (false === ($pos = \strpos($class, "@anonymous\0"))) { + return $class; + } + + if (false === ($parent = \get_parent_class($class))) { + return \substr($class, 0, $pos + 10); + } + + return $parent . '@anonymous'; + } } diff --git a/src/DebugBar/DataFormatter/HasDataFormatter.php b/src/DebugBar/DataFormatter/HasDataFormatter.php new file mode 100644 index 00000000..1b33edcd --- /dev/null +++ b/src/DebugBar/DataFormatter/HasDataFormatter.php @@ -0,0 +1,164 @@ +useHtmlVarDumper = $value; + return $this; + } + + /** + * Indicates whether the Symfony HtmlDumper will be used to dump variables for rich variable + * rendering. + * + * @return mixed + */ + public function isHtmlVarDumperUsed() + { + return $this->useHtmlVarDumper; + } + + /** + * Sets the default data formater instance used by all collectors subclassing this class + * + * @param DataFormatterInterface $formater + */ + public static function setDefaultDataFormatter(DataFormatterInterface $formater) + { + DataCollector::$defaultDataFormatter = $formater; + } + + /** + * Returns the default data formater + * + * @return DataFormatterInterface + */ + public static function getDefaultDataFormatter() + { + if (DataCollector::$defaultDataFormatter === null) { + DataCollector::$defaultDataFormatter = new DataFormatter(); + } + return DataCollector::$defaultDataFormatter; + } + + /** + * Sets the data formater instance used by this collector + * + * @param DataFormatterInterface $formater + * @return $this + */ + public function setDataFormatter(DataFormatterInterface $formater) + { + $this->dataFormater = $formater; + return $this; + } + + /** + * @return DataFormatterInterface + */ + public function getDataFormatter() + { + if ($this->dataFormater === null) { + $this->dataFormater = DataCollector::getDefaultDataFormatter(); + } + return $this->dataFormater; + } + /** + * Sets the default variable dumper used by all collectors subclassing this class + * + * @param DebugBarVarDumper $varDumper + */ + public static function setDefaultVarDumper(DebugBarVarDumper $varDumper) + { + DataCollector::$defaultVarDumper = $varDumper; + } + + /** + * Returns the default variable dumper + * + * @return DebugBarVarDumper + */ + public static function getDefaultVarDumper() + { + if (DataCollector::$defaultVarDumper === null) { + DataCollector::$defaultVarDumper = new DebugBarVarDumper(); + } + return DataCollector::$defaultVarDumper; + } + + /** + * Sets the variable dumper instance used by this collector + * + * @param DebugBarVarDumper $varDumper + * @return $this + */ + public function setVarDumper(DebugBarVarDumper $varDumper) + { + $this->varDumper = $varDumper; + return $this; + } + + /** + * Gets the variable dumper instance used by this collector; note that collectors using this + * instance need to be sure to return the static assets provided by the variable dumper. + * + * @return DebugBarVarDumper + */ + public function getVarDumper() + { + if ($this->varDumper === null) { + $this->varDumper = DataCollector::getDefaultVarDumper(); + } + return $this->varDumper; + } + + /** + * @deprecated + */ + public function formatVar($var) + { + return $this->getDataFormatter()->formatVar($var); + } + + /** + * @deprecated + */ + public function formatDuration($seconds) + { + return $this->getDataFormatter()->formatDuration($seconds); + } + + /** + * @deprecated + */ + public function formatBytes($size, $precision = 2) + { + return $this->getDataFormatter()->formatBytes($size, $precision); + } +} diff --git a/src/DebugBar/DataFormatter/HasXdebugLinks.php b/src/DebugBar/DataFormatter/HasXdebugLinks.php new file mode 100644 index 00000000..3e83b87f --- /dev/null +++ b/src/DebugBar/DataFormatter/HasXdebugLinks.php @@ -0,0 +1,200 @@ +xdebugReplacements) as $path) { + if (strpos($file, $path) === 0) { + $file = substr($file, strlen($path)); + break; + } + } + + return ltrim(str_replace('\\', '/', $file), '/'); + } + + /** + * Get an Xdebug Link to a file + * + * @param string $file + * @param int|null $line + * + * @return array { + * @var string $url + * @var bool $ajax should be used to open the url instead of a normal links + * } + */ + public function getXdebugLink($file, $line = null) + { + if (empty($file)) { + return null; + } + + if (@file_exists($file)) { + $file = realpath($file); + } + + foreach ($this->xdebugReplacements as $path => $replacement) { + if (strpos($file, $path) === 0) { + $file = $replacement . substr($file, strlen($path)); + break; + } + } + + $url = strtr($this->getXdebugLinkTemplate(), [ + '%f' => rawurlencode(str_replace('\\', '/', $file)), + '%l' => rawurlencode((string) $line ?: 1), + ]); + if ($url) { + return [ + 'url' => $url, + 'ajax' => $this->getXdebugShouldUseAjax(), + 'filename' => basename($file), + 'line' => (string) $line ?: '?' + ]; + } + } + + /** + * @return string + */ + public function getXdebugLinkTemplate() + { + if (empty($this->xdebugLinkTemplate) && !empty(ini_get('xdebug.file_link_format'))) { + $this->xdebugLinkTemplate = ini_get('xdebug.file_link_format'); + } + + return $this->xdebugLinkTemplate; + } + + /** + * @param string $editor + */ + public function setEditorLinkTemplate($editor) + { + $editorLinkTemplates = array( + 'sublime' => 'subl://open?url=file://%f&line=%l', + 'textmate' => 'txmt://open?url=file://%f&line=%l', + 'emacs' => 'emacs://open?url=file://%f&line=%l', + 'macvim' => 'mvim://open/?url=file://%f&line=%l', + 'codelite' => 'codelite://open?file=%f&line=%l', + 'phpstorm' => 'phpstorm://open?file=%f&line=%l', + 'phpstorm-remote' => 'javascript:(()=>{let r=new XMLHttpRequest;' . + 'r.open(\'get\',\'http://localhost:63342/api/file/%f:%l/');r.send();})()', + 'idea' => 'idea://open?file=%f&line=%l', + 'idea-remote' => 'javascript:(()=>{let r=new XMLHttpRequest;' . + 'r.open(\'get\',\'http://localhost:63342/api/file/?file=%f&line=%l\');r.send();})()', + 'vscode' => 'vscode://file/%f:%l', + 'vscode-insiders' => 'vscode-insiders://file/%f:%l', + 'vscode-remote' => 'vscode://vscode-remote/%f:%l', + 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/%f:%l', + 'vscodium' => 'vscodium://file/%f:%l', + 'nova' => 'nova://open?path=%f&line=%l', + 'xdebug' => 'xdebug://%f@%l', + 'atom' => 'atom://core/open/file?filename=%f&line=%l', + 'espresso' => 'x-espresso://open?filepath=%f&lines=%l', + 'netbeans' => 'netbeans://open/?f=%f:%l', + 'cursor' => 'cursor://file/%f:%l', + 'windsurf' => 'windsurf://file/%f:%l', + ); + + if (is_string($editor) && isset($editorLinkTemplates[$editor])) { + $this->setXdebugLinkTemplate($editorLinkTemplates[$editor]); + } + } + + /** + * @param string $xdebugLinkTemplate + * @param bool $shouldUseAjax + */ + public function setXdebugLinkTemplate($xdebugLinkTemplate, $shouldUseAjax = false) + { + if ($xdebugLinkTemplate === 'idea') { + $this->xdebugLinkTemplate = 'http://localhost:63342/api/file/?file=%f&line=%l'; + $this->xdebugShouldUseAjax = true; + } else { + $this->xdebugLinkTemplate = $xdebugLinkTemplate; + $this->xdebugShouldUseAjax = $shouldUseAjax; + } + } + + /** + * @return bool + */ + public function getXdebugShouldUseAjax() + { + return $this->xdebugShouldUseAjax; + } + + /** + * returns an array of filename-replacements + * + * this is useful f.e. when using vagrant or remote servers, + * where the path of the file is different between server and + * development environment + * + * @return array key-value-pairs of replacements, key = path on server, value = replacement + */ + public function getXdebugReplacements() + { + return $this->xdebugReplacements; + } + + /** + * @param array $xdebugReplacements + */ + public function addXdebugReplacements($xdebugReplacements) + { + foreach ($xdebugReplacements as $serverPath => $replacement) { + $this->setXdebugReplacement($serverPath, $replacement); + } + } + + /** + * @param array $xdebugReplacements + */ + public function setXdebugReplacements($xdebugReplacements) + { + $this->xdebugReplacements = $xdebugReplacements; + } + + /** + * @param string $serverPath + * @param string $replacement + */ + public function setXdebugReplacement($serverPath, $replacement) + { + $this->xdebugReplacements[$serverPath] = $replacement; + } +} diff --git a/src/DebugBar/DataFormatter/VarDumper/DebugBarHtmlDumper.php b/src/DebugBar/DataFormatter/VarDumper/DebugBarHtmlDumper.php index 136d1ae8..15b216d8 100644 --- a/src/DebugBar/DataFormatter/VarDumper/DebugBarHtmlDumper.php +++ b/src/DebugBar/DataFormatter/VarDumper/DebugBarHtmlDumper.php @@ -18,8 +18,19 @@ public function resetDumpHeader() $this->dumpHeader = null; } - public function getDumpHeaderByDebugBar() { - // getDumpHeader is protected: - return str_replace('pre.sf-dump', '.phpdebugbar pre.sf-dump', $this->getDumpHeader()); + public function getDumpHeaderByDebugBar() + { + $header = str_replace('pre.sf-dump', '.phpdebugbar pre.sf-dump', $this->getDumpHeader()); + + if (isset(self::$themes['dark'])) { + $line = ''; + foreach (self::$themes['dark'] as $class => $style) { + $line .= ".phpdebugbar[data-theme='dark'] pre.sf-dump".('default' === $class ? ', pre.sf-dump' : '').' .sf-dump-'.$class.'{'.$style.'}'; + } + $line .= ".phpdebugbar[data-theme='dark'] " . 'pre.sf-dump .sf-dump-ellipsis-note{'.self::$themes['dark']['note'].'}'; + $header = str_replace('', $line . '', $header); + } + + return $header; } } diff --git a/src/DebugBar/DebugBar.php b/src/DebugBar/DebugBar.php index b3999b47..37054498 100644 --- a/src/DebugBar/DebugBar.php +++ b/src/DebugBar/DebugBar.php @@ -146,7 +146,7 @@ public function getCurrentRequestId() * @param StorageInterface $storage * @return $this */ - public function setStorage(StorageInterface $storage = null) + public function setStorage(?StorageInterface $storage = null) { $this->storage = $storage; return $this; @@ -386,7 +386,7 @@ public function getStackedData($delete = true) $datasets = $stackedData; } - return $datasets; + return array_filter($datasets); } /** diff --git a/src/DebugBar/JavascriptRenderer.php b/src/DebugBar/JavascriptRenderer.php index b61d74fd..6d652f1a 100644 --- a/src/DebugBar/JavascriptRenderer.php +++ b/src/DebugBar/JavascriptRenderer.php @@ -62,6 +62,10 @@ class JavascriptRenderer protected $useRequireJs = false; + protected $theme = null; + + protected $hideEmptyTabs = null; + protected $initialization; protected $controls = array(); @@ -72,12 +76,16 @@ class JavascriptRenderer protected $ajaxHandlerBindToFetch = false; - protected $ajaxHandlerBindToJquery = true; + protected $ajaxHandlerBindToJquery = false; - protected $ajaxHandlerBindToXHR = false; + protected $ajaxHandlerBindToXHR = true; protected $ajaxHandlerAutoShow = true; + protected $ajaxHandlerEnableTab = false; + + protected $deferDatasets = false; + protected $openHandlerClass = 'PhpDebugBar.OpenHandler'; protected $openHandlerUrl; @@ -93,16 +101,20 @@ public function __construct(DebugBar $debugBar, $baseUrl = null, $basePath = nul { $this->debugBar = $debugBar; - if ($baseUrl === null) { - $baseUrl = '/vendor/maximebf/debugbar/src/DebugBar/Resources'; - } - $this->baseUrl = $baseUrl; - if ($basePath === null) { $basePath = __DIR__ . DIRECTORY_SEPARATOR . 'Resources'; } $this->basePath = $basePath; + if ($baseUrl === null) { + if ($basePath && str_contains($basePath, '/vendor/')) { + $baseUrl = strstr($basePath, '/vendor/'); + } else { + $baseUrl = '/vendor/php-debugbar/php-debugbar/src/DebugBar/Resources'; + } + } + $this->baseUrl = $baseUrl; + // bitwise operations cannot be done in class definition :( $this->initialization = self::INITIALIZE_CONSTRUCTOR | self::INITIALIZE_CONTROLS; } @@ -155,6 +167,12 @@ public function setOptions(array $options) if (array_key_exists('use_requirejs', $options)) { $this->setUseRequireJs($options['use_requirejs']); } + if (array_key_exists('theme', $options)) { + $this->setTheme($options['theme']); + } + if (array_key_exists('hide_empty_tabs', $options)) { + $this->setHideEmptyTabs($options['hide_empty_tabs']); + } if (array_key_exists('controls', $options)) { foreach ($options['controls'] as $name => $control) { $this->addControl($name, $control); @@ -179,6 +197,12 @@ public function setOptions(array $options) if (array_key_exists('ajax_handler_auto_show', $options)) { $this->setAjaxHandlerAutoShow($options['ajax_handler_auto_show']); } + if (array_key_exists('ajax_handler_enable_tab', $options)) { + $this->setAjaxHandlerEnableTab($options['ajax_handler_enable_tab']); + } + if (array_key_exists('defer_datasets', $options)) { + $this->setDeferDatasets($options['defer_datasets']); + } if (array_key_exists('open_handler_classname', $options)) { $this->setOpenHandlerClass($options['open_handler_classname']); } @@ -392,6 +416,40 @@ public function isRequireJsUsed() return $this->useRequireJs; } + /** + * Sets the default theme + * + * @param boolean $hide + * @return $this + */ + public function setTheme($theme='auto') + { + $this->theme = $theme; + return $this; + } + + /** + * Sets whether to hide empty tabs or not + * + * @param boolean $hide + * @return $this + */ + public function setHideEmptyTabs($hide = true) + { + $this->hideEmptyTabs = $hide; + return $this; + } + + /** + * Checks if empty tabs are hidden or not + * + * @return boolean + */ + public function areEmptyTabsHidden() + { + return $this->hideEmptyTabs; + } + /** * Adds a control to initialize * @@ -509,6 +567,7 @@ public function isAjaxHandlerBoundToFetch() * Sets whether to call bindToJquery() on the ajax handler * * @param boolean $bind + * @deprecated use setBindAjaxHandlerToXHR */ public function setBindAjaxHandlerToJquery($bind = true) { @@ -520,6 +579,7 @@ public function setBindAjaxHandlerToJquery($bind = true) * Checks whether bindToJquery() will be called on the ajax handler * * @return boolean + * @deprecated use isAjaxHandlerBoundToXHR */ public function isAjaxHandlerBoundToJquery() { @@ -569,6 +629,50 @@ public function isAjaxHandlerAutoShow() return $this->ajaxHandlerAutoShow; } + /** + * Sets whether new ajax debug data will be shown in a separate tab instead of dropdown. + * + * @param boolean $enabled + */ + public function setAjaxHandlerEnableTab($enabled = true) + { + $this->ajaxHandlerEnableTab = $enabled; + return $this; + } + + /** + * Check if the Ajax Handler History tab is enabled + * + * @return boolean + */ + public function isAjaxHandlerTabEnabled() + { + return $this->ajaxHandlerEnableTab; + } + + + /** + * Sets whether datasets are directly loaded or deferred + * + * @param boolean $enabled + */ + public function setDeferDatasets($defer = true) + { + $this->deferDatasets = $defer; + return $this; + } + + /** + * Check if the datasets are deffered + * + * @return boolean + */ + public function areDatasetsDeferred() + { + return $this->deferDatasets; + } + + /** * Sets the class name of the js open handler * @@ -1007,7 +1111,7 @@ public function renderOnShutdownWithHead($here = true, $initialize = true, $rend public function replaceTagInBuffer($here = true, $initialize = true, $renderStackedData = true, $head = false) { $render = ($head ? $this->renderHead() : "") - . $this->render($initialize, $renderStackedData); + . $this->render($initialize, $renderStackedData); $current = ($here && ob_get_level() > 0) ? ob_get_clean() : self::REPLACEABLE_TAG; @@ -1037,15 +1141,29 @@ public function render($initialize = true, $renderStackedData = true) if ($renderStackedData && $this->debugBar->hasStackedData()) { foreach ($this->debugBar->getStackedData() as $id => $data) { - $js .= $this->getAddDatasetCode($id, $data, '(stacked)'); + if ($this->areDatasetsDeferred()) { + $js .= $this->getLoadDatasetCode($id, '(stacked)'); + } else { + $js .= $this->getAddDatasetCode($id, $data, '(stacked)'); + + } } } $suffix = !$initialize ? '(ajax)' : null; - $js .= $this->getAddDatasetCode($this->debugBar->getCurrentRequestId(), $this->debugBar->getData(), $suffix); + if ($this->areDatasetsDeferred()) { + $this->debugBar->getData(); + $js .= $this->getLoadDatasetCode($this->debugBar->getCurrentRequestId(), $suffix); + } else { + $js .= $this->getAddDatasetCode($this->debugBar->getCurrentRequestId(), $this->debugBar->getData(), $suffix); + } $nonce = $this->getNonceAttribute(); + if ($nonce != '') { + $js = preg_replace("/\n"; } else { @@ -1064,7 +1182,8 @@ protected function getJsInitializationCode() $js = ''; if (($this->initialization & self::INITIALIZE_CONSTRUCTOR) === self::INITIALIZE_CONSTRUCTOR) { - $js .= sprintf("var %s = new %s();\n", $this->variableName, $this->javascriptClass); + $initializeOptions = $this->getInitializeOptions(); + $js .= sprintf("var %s = new %s(%s);\n", $this->variableName, $this->javascriptClass, $initializeOptions ? json_encode((object) $initializeOptions) : ''); } if (($this->initialization & self::INITIALIZE_CONTROLS) === self::INITIALIZE_CONTROLS) { @@ -1097,6 +1216,21 @@ protected function getJsInitializationCode() return $js; } + protected function getInitializeOptions() + { + $options = []; + + if ($this->theme !== null) { + $options['theme'] = $this->theme; + } + + if ($this->hideEmptyTabs !== null) { + $options['hideEmptyTabs'] = $this->hideEmptyTabs; + } + + return $options; + } + /** * Returns the js code needed to initialized the controls and data mapping of the debug bar * @@ -1122,6 +1256,11 @@ protected function getJsControlsDefinitionCode($varname) } $controls = array_merge($widgets, $this->controls); + // Allow widgets to be sorted by order if specified + uasort($controls, function(array $controlA, array $controlB){ + return ($controlA['order'] ?? 0) <=> ($controlB['order'] ?? 0); + }); + foreach (array_filter($controls) as $name => $options) { $opts = array_diff_key($options, array_flip($excludedOptions)); @@ -1161,6 +1300,9 @@ protected function getJsControlsDefinitionCode($varname) // activate state restoration $js .= sprintf("%s.restoreState();\n", $varname); + if ($this->ajaxHandlerEnableTab) { + $js .= sprintf("%s.enableAjaxHandlerTab();\n", $varname); + } return $js; } @@ -1176,7 +1318,24 @@ protected function getAddDatasetCode($requestId, $data, $suffix = null) { $js = sprintf("%s.addDataSet(%s, \"%s\"%s);\n", $this->variableName, - json_encode($data), + json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_INVALID_UTF8_IGNORE), + $requestId, + $suffix ? ", " . json_encode($suffix) : '' + ); + return $js; + } + + /** + * Returns the js code needed to load a dataset with the OpenHandler + * + * @param string $requestId + * @param mixed $suffix + * @return string + */ + protected function getLoadDatasetCode($requestId, $suffix = null) + { + $js = sprintf("%s.loadDataSet(\"%s\"%s);\n", + $this->variableName, $requestId, $suffix ? ", " . json_encode($suffix) : '' ); diff --git a/src/DebugBar/RequestIdGenerator.php b/src/DebugBar/RequestIdGenerator.php index 90c1728b..4b99d32d 100644 --- a/src/DebugBar/RequestIdGenerator.php +++ b/src/DebugBar/RequestIdGenerator.php @@ -15,29 +15,11 @@ */ class RequestIdGenerator implements RequestIdGeneratorInterface { - protected $index = 0; - /** * @return string */ public function generate() { - if (function_exists('random_bytes')) { - // PHP 7 only - return 'X' . bin2hex(random_bytes(16)); - } else if (function_exists('openssl_random_pseudo_bytes')) { - // PHP >= 5.3.0, but OpenSSL may not always be available - return 'X' . bin2hex(openssl_random_pseudo_bytes(16)); - } else { - // Fall back to a rudimentary ID generator: - // * $_SERVER array will make the ID unique to this request. - // * spl_object_hash($this) will make the ID unique to this object instance. - // (note that object hashes can be reused, but the other data here should prevent issues here). - // * uniqid('', true) will use the current microtime(), plus additional random data. - // * $this->index guarantees the uniqueness of IDs from the current object. - $this->index++; - $entropy = serialize($_SERVER) . uniqid('', true) . spl_object_hash($this) . $this->index; - return 'X' . md5($entropy); - } + return 'X'.bin2hex(random_bytes(16)); } } diff --git a/src/DebugBar/Resources/debugbar.css b/src/DebugBar/Resources/debugbar.css index 606e51da..1d02ed52 100644 --- a/src/DebugBar/Resources/debugbar.css +++ b/src/DebugBar/Resources/debugbar.css @@ -5,23 +5,112 @@ } } +div.phpdebugbar, +div.phpdebugbar-openhandler { + --debugbar-background: #fff; + --debugbar-background-alt: #fafafa; + --debugbar-text: #222; + --debugbar-text-muted: #888; + --debugbar-border: #eee; + + --debugbar-header: #efefef; + --debugbar-header-text: #555; + --debugbar-header-border: #ddd; + + --debugbar-active: #ccc; + --debugbar-active-text: #666; + + --debugbar-icons: #555; + --debugbar-badge: #ccc; + --debugbar-badge-text: #555; + + --debugbar-badge-active: #555; + --debugbar-badge-active-text: #fff; + + --debugbar-link: #888; + --debugbar-hover: #aaa; + + --debugbar-accent: #6BB7D8; + --debugbar-accent-border: #477e96; + + --debugbar-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --debugbar-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +div.phpdebugbar[data-theme='dark'], +div.phpdebugbar-openhandler[data-theme='dark'] { + --debugbar-background: #2a2a2a; + --debugbar-background-alt: #333333; + --debugbar-text: #e0e0e0; + --debugbar-text-muted: #aaaaaa; + --debugbar-border: #3a3a3a; + + --debugbar-header: #1e1e1e; + --debugbar-header-text: #cccccc; + --debugbar-header-border: #444; + + --debugbar-active: #444; + --debugbar-active-text: #e0e0e0; + + --debugbar-icons: #cccccc; + --debugbar-badge: #444; + --debugbar-badge-text: #cccccc; + + --debugbar-badge-active: #cccccc; + --debugbar-badge-active-text: #1e1e1e; + + --debugbar-accent: #4F8FB3; + --debugbar-accent-border: #3F7A94; + + --debugbar-link: #aaaaaa; + --debugbar-hover: #888888; +} + + div.phpdebugbar { position: fixed; bottom: 0; left: 0; width: 100%; border-top: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; - background: #fff; - z-index: 10000; + font-family: var(--debugbar-font-sans); + background: var(--debugbar-background); + z-index: 10000000000; font-size: 14px; - color: #000; + color: var(--debugbar-text); text-align: left; - line-height: 1; + line-height: 1.2em; letter-spacing: normal; direction: ltr; } +div.phpdebugbar[data-openBtnPosition="bottomRight"].phpdebugbar-closed, +div.phpdebugbar[data-openBtnPosition="topRight"].phpdebugbar-closed { + left:auto; + right: 0; +} + +div.phpdebugbar[data-openBtnPosition="topRight"].phpdebugbar-closed, +div.phpdebugbar[data-openBtnPosition="topLeft"].phpdebugbar-closed { + bottom:auto; + top: 0; + border-bottom: 1px solid var(--debugbar-header-border); +} + +div.phpdebugbar[data-openBtnPosition="bottomRight"].phpdebugbar-closed, +div.phpdebugbar[data-openBtnPosition="bottomLeft"].phpdebugbar-closed { + border-top: 1px solid var(--debugbar-header-border); +} + +.phpdebugbar-closed[data-openBtnPosition="bottomLeft"], +.phpdebugbar-closed[data-openBtnPosition="topLeft"] { + border-right: 1px solid var(--debugbar-header-border); +} +.phpdebugbar-closed[data-openBtnPosition="bottomRight"], +.phpdebugbar-closed[data-openBtnPosition="topRight"] { + border-left: 1px solid var(--debugbar-header-border); +} + div.phpdebugbar a, div.phpdebugbar-openhandler { cursor: pointer; @@ -51,54 +140,84 @@ div.phpdebugbar * { text-decoration: none; clear: initial; width: auto; + direction: ltr; + text-align: left; -moz-box-sizing: content-box; box-sizing: content-box; } +div.phpdebugbar select, div.phpdebugbar input { + appearance: auto; +} + div.phpdebugbar ol, div.phpdebugbar ul { list-style: none; } -div.phpdebugbar table { +div.phpdebugbar ul li, div.phpdebugbar ol li, div.phpdebugbar dl li { + line-height: normal; +} + +div.phpdebugbar table, .phpdebugbar-openhandler table { border-collapse: collapse; border-spacing: 0; + color: inherit; } -div.phpdebugbar input[type='text'], div.phpdebugbar input[type='password'] { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; - background: #fff; +div.phpdebugbar input[type='text'], div.phpdebugbar input[type='password'], div.phpdebugbar select { + font-family: var(--debugbar-font-sans); + background: var(--debugbar-background); font-size: 14px; - color: #000; - border: 0; + color: var(--debugbar-text); padding: 0; + border: 1px solid var(--debugbar-border); + border-radius: 0.25rem; margin: 0; } div.phpdebugbar code, div.phpdebugbar pre, div.phpdebugbar samp { background: none; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-family: var(--debugbar-font-mono); font-size: 1em; - border: 0; + border: 0 !important; padding: 0; margin: 0; } div.phpdebugbar code, div.phpdebugbar pre { - color: #000; + color: var(--debugbar-text); } div.phpdebugbar pre.sf-dump { + background: none !important; + z-index: 0 !important; + display: block !important; color: #a0a000; outline: 0; } +div.phpdebugbar pre.sf-dump .sf-dump-private { + color: grey; +} + +div.phpdebugbar[data-theme='dark'] pre.sf-dump .sf-dump-public { + color: #ffcc00; +} + +div.phpdebugbar[data-theme='dark'] pre.sf-dump .sf-dump-protected { + color: #a0a000; +} + +div.phpdebugbar[data-theme='dark'] pre.sf-dump .sf-dump-private { + color: #9d9266; +} + a.phpdebugbar-restore-btn { float: left; padding: 5px 8px; font-size: 14px; - color: #555; + color: var(--debugbar-icons); text-decoration: none; - border-right: 1px solid #ddd; } div.phpdebugbar-resize-handle { @@ -107,20 +226,22 @@ div.phpdebugbar-resize-handle { margin-top: -4px; width: 100%; background: none; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid var(--debugbar-header-border); cursor: ns-resize; } -div.phpdebugbar-closed, div.phpdebugbar-minimized{ - border-top: 1px solid #ccc; +div.phpdebugbar-minimized{ + border-top: 1px solid var(--debugbar-header-border); } + /* -------------------------------------- */ -a.phpdebugbar-restore-btn { - background: #efefef url(data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ccircle%20fill%3D%22%23000%22%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%229%22%2F%3E%3Cpath%20d%3D%22M6.039%208.342c.463%200%20.772.084.927.251.154.168.191.455.11.862-.084.424-.247.727-.487.908-.241.182-.608.272-1.1.272h-.743l.456-2.293h.837zm-2.975%204.615h1.22l.29-1.457H5.62c.461%200%20.84-.047%201.139-.142.298-.095.569-.254.812-.477.205-.184.37-.387.497-.608.127-.222.217-.466.27-.734.13-.65.032-1.155-.292-1.518-.324-.362-.84-.543-1.545-.543H4.153l-1.089%205.479zM9.235%206.02h1.21l-.289%201.458h1.079c.679%200%201.147.115%201.405.347.258.231.335.607.232%201.125l-.507%202.55h-1.23l.481-2.424c.055-.276.035-.464-.06-.565-.095-.1-.298-.15-.608-.15H9.98L9.356%2011.5h-1.21l1.089-5.48M15.566%208.342c.464%200%20.773.084.928.251.154.168.19.455.11.862-.084.424-.247.727-.488.908-.24.182-.607.272-1.1.272h-.742l.456-2.293h.836zm-2.974%204.615h1.22l.29-1.457h1.046c.461%200%20.84-.047%201.139-.142.298-.095.569-.254.812-.477.205-.184.37-.387.497-.608.127-.222.217-.466.27-.734.129-.65.032-1.155-.292-1.518-.324-.362-.84-.543-1.545-.543H13.68l-1.089%205.479z%22%20fill%3D%22%23FFF%22%2F%3E%3C%2Fsvg%3E) no-repeat 5px 4px / 20px 20px; +a.phpdebugbar-restore-btn:after { + background: var(--debugbar-header) url(data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ccircle%20fill%3D%22%23000%22%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%229%22%2F%3E%3Cpath%20d%3D%22M6.039%208.342c.463%200%20.772.084.927.251.154.168.191.455.11.862-.084.424-.247.727-.487.908-.241.182-.608.272-1.1.272h-.743l.456-2.293h.837zm-2.975%204.615h1.22l.29-1.457H5.62c.461%200%20.84-.047%201.139-.142.298-.095.569-.254.812-.477.205-.184.37-.387.497-.608.127-.222.217-.466.27-.734.13-.65.032-1.155-.292-1.518-.324-.362-.84-.543-1.545-.543H4.153l-1.089%205.479zM9.235%206.02h1.21l-.289%201.458h1.079c.679%200%201.147.115%201.405.347.258.231.335.607.232%201.125l-.507%202.55h-1.23l.481-2.424c.055-.276.035-.464-.06-.565-.095-.1-.298-.15-.608-.15H9.98L9.356%2011.5h-1.21l1.089-5.48M15.566%208.342c.464%200%20.773.084.928.251.154.168.19.455.11.862-.084.424-.247.727-.488.908-.24.182-.607.272-1.1.272h-.742l.456-2.293h.836zm-2.974%204.615h1.22l.29-1.457h1.046c.461%200%20.84-.047%201.139-.142.298-.095.569-.254.812-.477.205-.184.37-.387.497-.608.127-.222.217-.466.27-.734.129-.65.032-1.155-.292-1.518-.324-.362-.84-.543-1.545-.543H13.68l-1.089%205.479z%22%20fill%3D%22%23FFF%22%2F%3E%3C%2Fsvg%3E) no-repeat center / 20px 20px; } div.phpdebugbar-header { - min-height: 26px; + background-color: var(--debugbar-header); + min-height: 32px; line-height: 16px; } div.phpdebugbar-header:before, div.phpdebugbar-header:after { @@ -138,11 +259,18 @@ div.phpdebugbar-header-right { float: right; } div.phpdebugbar-header > div > * { - padding: 5px 5px; - font-size: 14px; - color: #555; + padding: 5px; + font-size: 13px; + height: 22px; + color: var(--debugbar-header-text); text-decoration: none; } +div.phpdebugbar-header-left > *, +div.phpdebugbar-header-right > * { + line-height: 0; + display: flex; + align-items: center; +} div.phpdebugbar-header-left > * { float: left; } @@ -151,39 +279,47 @@ div.phpdebugbar-header-right > * { } div.phpdebugbar-header-right > select { padding: 0; + line-height: 1em; + background-color: var(--debugbar-header); + color: var(--debugbar-header-text); } /* -------------------------------------- */ span.phpdebugbar-indicator, -a.phpdebugbar-indicator, -a.phpdebugbar-close-btn { - border-right: 1px solid #ddd; +a.phpdebugbar-indicator +{ + border-right: 1px solid var(--debugbar-header-border); +} + +.phpdebugbar[data-hideEmptyTabs=true] .phpdebugbar-tab[data-empty=true] { + display: none; } a.phpdebugbar-tab.phpdebugbar-active { - background: #ccc; - color: #444; - background-image: linear-gradient(bottom, rgb(173,173,173) 41%, rgb(209,209,209) 71%); - background-image: -o-linear-gradient(bottom, rgb(173,173,173) 41%, rgb(209,209,209) 71%); - background-image: -moz-linear-gradient(bottom, rgb(173,173,173) 41%, rgb(209,209,209) 71%); - background-image: -webkit-linear-gradient(bottom, rgb(173,173,173) 41%, rgb(209,209,209) 71%); - background-image: -ms-linear-gradient(bottom, rgb(173,173,173) 41%, rgb(209,209,209) 71%); - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.41, rgb(173,173,173)), color-stop(0.71, rgb(209,209,209))); + background: var(--debugbar-active); + color: var(--debugbar-active-text); } + a.phpdebugbar-tab .phpdebugbar-text { + font-size: 14px; + } a.phpdebugbar-tab span.phpdebugbar-badge { display: none; margin-left: 5px; font-size: 11px; line-height: 14px; padding: 0 6px; - background: #ccc; + background: var(--debugbar-badge); border-radius: 4px; - color: #555; + color: var(--debugbar-badge-text); font-weight: normal; text-shadow: none; - vertical-align: middle; } + a.phpdebugbar-tab.phpdebugbar-active span.phpdebugbar-badge { + background: var(--debugbar-badge-active); + color: var(--debugbar-badge-active-text); + } + a.phpdebugbar-tab i { display: none; vertical-align: middle; @@ -196,13 +332,43 @@ a.phpdebugbar-tab.phpdebugbar-active { color: white; } -a.phpdebugbar-close-btn, a.phpdebugbar-open-btn, a.phpdebugbar-restore-btn, a.phpdebugbar-minimize-btn , a.phpdebugbar-maximize-btn { +a.phpdebugbar-close-btn, +a.phpdebugbar-open-btn, +a.phpdebugbar-restore-btn, +a.phpdebugbar-minimize-btn, +a.phpdebugbar-maximize-btn, +a.phpdebugbar-tab.phpdebugbar-tab-history, +a.phpdebugbar-tab.phpdebugbar-tab-settings { width: 16px; - height: 16px; + height: 22px; } -a.phpdebugbar-minimize-btn , a.phpdebugbar-maximize-btn { - padding-right: 0 !important; +a.phpdebugbar-close-btn, +a.phpdebugbar-open-btn, +a.phpdebugbar-restore-btn, +a.phpdebugbar-minimize-btn , +a.phpdebugbar-maximize-btn { + width: 16px; + height: 22px; + position: relative; +} + +a.phpdebugbar-close-btn:after, +a.phpdebugbar-open-btn:after, +a.phpdebugbar-restore-btn:after, +a.phpdebugbar-minimize-btn:after, +a.phpdebugbar-maximize-btn:after { + background-color: var(--debugbar-icons); + content: " "; + display: block; + left: 0; + position: absolute; + top: 0; + width: 100%; + height: 100%; +} +a.phpdebugbar-restore-btn:after { + background-color: var(--debugbar-header); } a.phpdebugbar-maximize-btn { display: none} @@ -213,20 +379,20 @@ div.phpdebugbar-minimized a.phpdebugbar-maximize-btn { display: block} div.phpdebugbar-minimized a.phpdebugbar-minimize-btn { display: none} -a.phpdebugbar-minimize-btn { - background:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201792%201792%22%20id%3D%22chevron-down%22%3E%3Cpath%20d%3D%22M1683%20808l-742%20741q-19%2019-45%2019t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19%2045-19t45%2019l531%20531%20531-531q19-19%2045-19t45%2019l166%20165q19%2019%2019%2045.5t-19%2045.5z%22%2F%3E%3C%2Fsvg%3E) no-repeat 6px 6px / 14px 14px; +a.phpdebugbar-minimize-btn:after { + mask:url(data:image/svg+xml,%3Csvg%20viewBox=%220%200%201792%201792%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d=%22m1683%20653.5-742%20741c-12.667%2012.67-27.667%2019-45%2019s-32.333-6.33-45-19l-742-741c-12.667-12.667-19-27.833-19-45.5s6.333-32.833%2019-45.5l166-165c12.667-12.667%2027.667-19%2045-19s32.333%206.333%2045%2019l531%20531%20531-531c12.67-12.667%2027.67-19%2045-19s32.33%206.333%2045%2019l166%20165c12.67%2012.667%2019%2027.833%2019%2045.5s-6.33%2032.833-19%2045.5Z%22%20fill=%22%23555000%22%2F%3E%3C%2Fsvg%3E) no-repeat center / 14px 14px; } -a.phpdebugbar-maximize-btn { - background:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201792%201792%22%20id%3D%22chevron-up%22%3E%3Cpath%20d%3D%22M1683%201331l-166%20165q-19%2019-45%2019t-45-19l-531-531-531%20531q-19%2019-45%2019t-45-19l-166-165q-19-19-19-45.5t19-45.5l742-741q19-19%2045-19t45%2019l742%20741q19%2019%2019%2045.5t-19%2045.5z%22%2F%3E%3C%2Fsvg%3E) no-repeat 6px 6px / 14px 14px; +a.phpdebugbar-maximize-btn:after { + mask: url(data:image/svg+xml,%3Csvg%20viewBox=%220%200%201792%201792%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d=%22m1683%201229.5-166%20165c-12.67%2012.67-27.67%2019-45%2019s-32.33-6.33-45-19l-531-531-531%20531c-12.667%2012.67-27.667%2019-45%2019s-32.333-6.33-45-19l-166-165c-12.667-12.67-19-27.83-19-45.5s6.333-32.83%2019-45.5l742-741c12.667-12.667%2027.667-19%2045-19s32.333%206.333%2045%2019l742%20741c12.67%2012.67%2019%2027.83%2019%2045.5s-6.33%2032.83-19%2045.5Z%22%20fill=%22%23000%22%2F%3E%3C%2Fsvg%3E) no-repeat center / 14px 14px; } -a.phpdebugbar-close-btn { - background: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201792%201792%22%20id%3D%22close%22%3E%3Cpath%20d%3D%22M1490%201322q0%2040-28%2068l-136%20136q-28%2028-68%2028t-68-28l-294-294-294%20294q-28%2028-68%2028t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28%2068-28t68%2028l294%20294%20294-294q28-28%2068-28t68%2028l136%20136q28%2028%2028%2068t-28%2068l-294%20294%20294%20294q28%2028%2028%2068z%22%2F%3E%3C%2Fsvg%3E) no-repeat 9px 6px / 14px 14px; +a.phpdebugbar-close-btn:after { + mask: url(data:image/svg+xml,%3Csvg%20viewBox=%220%200%201792%201792%22%20fill=%22none%22%20xmlns=%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d=%22M1490%201258c0%2026.67-9.33%2049.33-28%2068l-136%20136c-18.67%2018.67-41.33%2028-68%2028s-49.33-9.33-68-28l-294-294-294%20294c-18.667%2018.67-41.333%2028-68%2028s-49.333-9.33-68-28l-136-136c-18.667-18.67-28-41.33-28-68s9.333-49.33%2028-68l294-294-294-294c-18.667-18.667-28-41.333-28-68s9.333-49.333%2028-68l136-136c18.667-18.667%2041.333-28%2068-28s49.333%209.333%2068%2028l294%20294%20294-294c18.67-18.667%2041.33-28%2068-28s49.33%209.333%2068%2028l136%20136c18.67%2018.667%2028%2041.333%2028%2068s-9.33%2049.333-28%2068l-294%20294%20294%20294c18.67%2018.67%2028%2041.33%2028%2068Z%22%20fill=%22%23000%22%2F%3E%3C%2Fsvg%3E) no-repeat center / 14px 14px; } -a.phpdebugbar-open-btn { - background: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201792%201792%22%20id%3D%22folder-open%22%3E%3Cpath%20d%3D%22M1815%20952q0%2031-31%2066l-336%20396q-43%2051-120.5%2086.5t-143.5%2035.5h-1088q-34%200-60.5-13t-26.5-43q0-31%2031-66l336-396q43-51%20120.5-86.5t143.5-35.5h1088q34%200%2060.5%2013t26.5%2043zm-343-344v160h-832q-94%200-197%2047.5t-164%20119.5l-337%20396-5%206q0-4-.5-12.5t-.5-12.5v-960q0-92%2066-158t158-66h320q92%200%20158%2066t66%20158v32h544q92%200%20158%2066t66%20158z%22%2F%3E%3C%2Fsvg%3E) no-repeat 8px 6px / 14px 14px; +a.phpdebugbar-open-btn:after { + mask: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 1792 1792'%3e%3cpath fill='%23000' d='M1646 991.796c0 16.494-8.25 34.064-24.75 52.684l-268.22 316.13c-22.89 27.14-54.95 50.16-96.2 69.05S1177.4 1458 1142.27 1458H273.728c-18.095 0-34.194-3.46-48.297-10.38-14.104-6.92-21.155-18.36-21.155-34.32 0-16.5 8.249-34.06 24.747-52.69l268.228-316.13c22.884-27.14 54.949-50.156 96.194-69.049 41.246-18.893 79.431-28.34 114.556-28.34h868.549c18.09 0 34.19 3.459 48.3 10.378 14.1 6.918 21.15 18.361 21.15 34.327Zm-273.82-274.615v127.728H708.001c-50.027 0-102.448 12.64-157.264 37.919-54.817 25.28-98.457 57.078-130.921 95.397L150.79 1294.35l-3.992 4.79c0-2.13-.133-5.46-.399-9.98-.266-4.52-.399-7.85-.399-9.98V512.817c0-48.962 17.563-91.005 52.688-126.13 35.125-35.126 77.168-52.688 126.131-52.688h255.455c48.962 0 91.005 17.562 126.13 52.688 35.126 35.125 52.688 77.168 52.688 126.13v25.546h434.278c48.96 0 91 17.563 126.13 52.688 35.12 35.125 52.68 77.168 52.68 126.13Z'/%3e%3c/svg%3e") no-repeat center / 14px 14px; } .phpdebugbar-indicator { @@ -239,35 +405,64 @@ a.phpdebugbar-open-btn { .phpdebugbar-indicator span.phpdebugbar-tooltip { display: none; position: absolute; - top: -30px; - background: #efefef; - opacity: .7; - border: 1px solid #ccc; - color: #555; + bottom: 38px; + background: var(--debugbar-header); + border: 1px solid var(--debugbar-header-border); + color: var(--debugbar-header-text); font-size: 11px; - padding: 2px 3px; + padding: 2px 6px; z-index: 1000; text-align: center; - width: 200%; + white-space: nowrap; right: 0; + line-height: 1.5; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); } .phpdebugbar-indicator:hover span.phpdebugbar-tooltip:not(.phpdebugbar-disabled) { display: block; } + .phpdebugbar-indicator span.phpdebugbar-tooltip dl { + display: grid; + grid-gap: 4px 10px; + grid-template-columns: max-content; + } + .phpdebugbar-indicator span.phpdebugbar-tooltip dl dt { + font-weight: bold; + text-align: left; + } + .phpdebugbar-indicator span.phpdebugbar-tooltip dl dd { + margin: 0; + grid-column-start: 2; + text-align: left; + } -select.phpdebugbar-datasets-switcher { +.phpdebugbar select.phpdebugbar-datasets-switcher { float: right; display: none; - margin: 2px 0 0 7px; max-width: 200px; - max-height: 23px; - padding: 0; + height: 22px; + padding: 4px 0; + border: none; +} + +.phpdebugbar button, +.phpdebugbar-openhandler button { + color: var(--debugbar-header-text); + background-color: var(--debugbar-header); + border: 1px solid var(--debugbar-header-border); + border-radius: 0.25rem; + margin: 0 5px; + padding: 0 12px; + height: 20px; + line-height: normal; + cursor: pointer; } /* -------------------------------------- */ div.phpdebugbar-body { - border-top: 1px solid #ccc; + border-top: 1px solid var(--debugbar-header-border); display: none; position: relative; height: 300px; @@ -289,7 +484,7 @@ div.phpdebugbar-panel.phpdebugbar-active { div.phpdebugbar-mini-design a.phpdebugbar-tab { position: relative; - border-right: 1px solid #ddd; + border-right: 1px solid var(--debugbar-header-border); } div.phpdebugbar-mini-design a.phpdebugbar-tab span.phpdebugbar-text { display: none; @@ -298,16 +493,124 @@ div.phpdebugbar-mini-design a.phpdebugbar-tab { display: block; position: absolute; top: -30px; - background: #efefef; - opacity: .7; - border: 1px solid #ccc; - color: #555; + background: var(--debugbar-background); + opacity: 1; + border: 1px solid var(--debugbar-header-border); + color: var(--debugbar-header-text); font-size: 11px; - padding: 2px 3px; + padding: 2px 6px; z-index: 1000; text-align: center; right: 0; + line-height: 1.5; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); } div.phpdebugbar-mini-design a.phpdebugbar-tab i { display:inline-block; } + +/* -------------------------------------- */ + + a.phpdebugbar-tab.phpdebugbar-tab-history { + width: auto; + min-width: 22px; + } + a.phpdebugbar-tab.phpdebugbar-tab-history, + a.phpdebugbar-tab.phpdebugbar-tab-settings { + display: flex; + justify-content: center; + align-items: center; + } + + a.phpdebugbar-tab.phpdebugbar-tab-history .phpdebugbar-text, + a.phpdebugbar-tab.phpdebugbar-tab-settings .phpdebugbar-text { + display: none; + white-space: nowrap; + } + a.phpdebugbar-tab.phpdebugbar-tab-history i, + a.phpdebugbar-tab.phpdebugbar-tab-settings i{ + display:inline-block; + } + .phpdebugbar-widgets-dataset-history table { + width: 100%; + table-layout: fixed; + } + .phpdebugbar-widgets-dataset-history table th { + font-weight: bold; + } + .phpdebugbar-widgets-dataset-history table td, .phpdebugbar-widgets-dataset-history table th { + padding: 6px 3px; + border-bottom: 1px solid var(--debugbar-border); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .phpdebugbar-widgets-dataset-history table td a{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .phpdebugbar-widgets-dataset-history table tr.phpdebugbar-widgets-active { + background: var(--debugbar-active); + color: var(--debugbar-active-text); + } + .phpdebugbar-widgets-dataset-history span.phpdebugbar-badge { + margin: 0 5px 0 2px; + font-size: 11px; + line-height: 14px; + padding: 0 6px; + background: var(--debugbar-badge); + border-radius: 4px; + color: var(--debugbar-badge-text); + font-weight: normal; + text-shadow: none; + vertical-align: middle; + } + .phpdebugbar-widgets-dataset-history .phpdebugbar-widgets-dataset-actions { + text-align: center; + padding: 7px 0; + position: sticky; + top: 0; + background: var(--debugbar-background); + } + .phpdebugbar-widgets-dataset-history .phpdebugbar-widgets-dataset-actions a { + margin: 0 10px; + } + .phpdebugbar-widgets-dataset-history .phpdebugbar-widgets-dataset-actions input { + margin: 5px; + } + + +/* -------------------------------------- */ + + +.phpdebugbar-settings .phpdebugbar-form-row { + display: block; + border-top: 1px solid var(--debugbar-border); + min-height: 17px; + padding: 5px 10px; +} +.phpdebugbar-settings .phpdebugbar-form-label { + width: 200px; + font-weight: bold; + display: inline-block; + clear: none; +} +.phpdebugbar-settings .phpdebugbar-form-input { + font-weight: bold; + display: inline-block; + clear: none; +} +.phpdebugbar-settings input[type="text"], +.phpdebugbar-settings select +{ + margin: 0 5px; + min-width: 200px; +} + +.phpdebugbar-settings input[type="checkbox"] +{ + margin: 0 5px; +} + diff --git a/src/DebugBar/Resources/debugbar.js b/src/DebugBar/Resources/debugbar.js index 66658d25..0ee70ba5 100644 --- a/src/DebugBar/Resources/debugbar.js +++ b/src/DebugBar/Resources/debugbar.js @@ -186,7 +186,7 @@ if (typeof(PhpDebugBar) == 'undefined') { * @param {Function} cb */ bindAttr: function(attr, cb) { - if ($.isArray(attr)) { + if (Array.isArray(attr)) { for (var i = 0, c = attr.length; i < c; i++) { this.bindAttr(attr[i], cb); } @@ -256,7 +256,6 @@ if (typeof(PhpDebugBar) == 'undefined') { render: function() { this.$tab = $('').addClass(csscls('tab')); - this.$icon = $('').appendTo(this.$tab); this.bindAttr('icon', function(icon) { if (icon) { @@ -285,6 +284,7 @@ if (typeof(PhpDebugBar) == 'undefined') { this.bindAttr('data', function(data) { if (this.has('widget')) { this.get('widget').set('data', data); + this.$tab.attr('data-empty', $.isEmptyObject(data) || data.count === 0); } }) } @@ -321,12 +321,31 @@ if (typeof(PhpDebugBar) == 'undefined') { } }); + this.bindAttr('link', function(link) { + if (link) { + this.$el.on('click', () => { + this.get('debugbar').showTab(link); + }).css('cursor', 'pointer') + } else { + this.$el.off('click', false).css('cursor', '') + } + }); + this.bindAttr(['title', 'data'], $('').addClass(csscls('text')).appendTo(this.$el)); this.$tooltip = $('').addClass(csscls('tooltip disabled')).appendTo(this.$el); this.bindAttr('tooltip', function(tooltip) { if (tooltip) { - this.$tooltip.text(tooltip).removeClass(csscls('disabled')); + var dl = $('
    '); + if (Array.isArray(tooltip) || typeof tooltip === 'object') { + $.each(tooltip, function(key, value) { + $('
    ').text(key).appendTo(dl); + $('
    ').text(value).appendTo(dl); + }); + this.$tooltip.html(dl).removeClass(csscls('disabled')); + } else { + this.$tooltip.text(tooltip).removeClass(csscls('disabled')); + } } else { this.$tooltip.addClass(csscls('disabled')); } @@ -335,6 +354,151 @@ if (typeof(PhpDebugBar) == 'undefined') { }); + + /** + * Displays datasets in a table + * + */ + var Settings = Widget.extend({ + + tagName: 'form', + + className: csscls('settings'), + + settings: {}, + + initialize: function(options) { + this.set(options); + + var debugbar = this.get('debugbar'); + this.settings = JSON.parse(localStorage.getItem('phpdebugbar-settings')) || {}; + + $.each(debugbar.options, (key, value)=> { + if (key in this.settings) { + debugbar.options[key] = this.settings[key]; + } + + // Theme requires dark/light mode detection + if (key === 'theme') { + debugbar.setTheme(debugbar.options[key]); + } else { + debugbar.$el.attr('data-' + key, debugbar.options[key]); + } + }) + }, + + clearSettings: function() { + var debugbar = this.get('debugbar'); + + // Remove item from storage + localStorage.removeItem('phpdebugbar-settings'); + localStorage.removeItem('phpdebugbar-ajaxhandler-autoshow'); + this.settings = {}; + + // Reset options + debugbar.options = { ...debugbar.defaultOptions }; + + // Reset ajax handler + if (debugbar.ajaxHandler) { + var autoshow = debugbar.ajaxHandler.defaultAutoShow; + debugbar.ajaxHandler.setAutoShow(autoshow); + this.set('autoshow', autoshow); + if(debugbar.controls['__datasets']) { + debugbar.controls['__datasets'].get('widget').set('autoshow', $(this).is(':checked')); + } + } + + this.initialize(debugbar.options); + }, + + storeSetting: function(key, value) { + this.settings[key] = value; + + var debugbar = this.get('debugbar'); + debugbar.options[key] = value; + if (key !== 'theme') { + debugbar.$el.attr('data-' + key, value); + } + + localStorage.setItem('phpdebugbar-settings', JSON.stringify(this.settings)); + }, + + render: function() { + this.$el.empty(); + + var debugbar = this.get('debugbar'); + var self = this; + + var fields = {}; + + // Set Theme + fields["Theme"] = $('') + .val(debugbar.options.theme) + .on('change', function() { + self.storeSetting('theme', $(this).val()) + debugbar.setTheme($(this).val()); + }); + + fields["Open Button Position"] = $('') + .val(debugbar.options.openBtnPosition) + .on('change', function() { + self.storeSetting('openBtnPosition', $(this).val()) + }); + + this.$hideEmptyTabs = $('') + .prop('checked', debugbar.options.hideEmptyTabs) + .on('click', function() { + self.storeSetting('hideEmptyTabs', $(this).is(':checked')); + }); + + fields["Hide Empty Tabs"] = $('
    ').addClass(csscls('form-row')).append( + $('
    ').addClass(csscls('form-label')).text(key), + $('
    ').addClass(csscls('form-input')).html(value) + ).appendTo(self.$el); + + }) + + if (!this.ajaxHandler) { + this.$autoshow.closest('.form-row').hide(); + } + }, + + }); + // ------------------------------------------------------------------ /** @@ -357,27 +521,29 @@ if (typeof(PhpDebugBar) == 'undefined') { * @param {String} suffix * @return {String} */ - format: function(id, data, suffix) { + format: function(id, data, suffix, nb) { if (suffix) { suffix = ' ' + suffix; } else { suffix = ''; } - var nb = getObjectSize(this.debugbar.datasets) + 1; + var nb = nb || getObjectSize(this.debugbar.datasets) ; if (typeof(data['__meta']) === 'undefined') { return "#" + nb + suffix; } - var uri = data['__meta']['uri'], filename; - if (uri.length && uri.charAt(uri.length - 1) === '/') { - // URI ends in a trailing /: get the portion before then to avoid returning an empty string - filename = uri.substr(0, uri.length - 1); // strip trailing '/' - filename = filename.substr(filename.lastIndexOf('/') + 1); // get last path segment - filename += '/'; // add the trailing '/' back - } else { - filename = uri.substr(uri.lastIndexOf('/') + 1); + var uri = data['__meta']['uri'].split('/'), filename = uri.pop(); + + // URI ends in a trailing /, avoid returning an empty string + if (!filename) { + filename = (uri.pop() || '') + '/'; // add the trailing '/' back + } + + // filename is a number, path could be like /action/{id} + if (uri.length && !isNaN(filename)) { + filename = uri.pop() + '/' + filename; } // truncate the filename in the label, if it's too long @@ -411,18 +577,34 @@ if (typeof(PhpDebugBar) == 'undefined') { options: { bodyMarginBottom: true, - bodyMarginBottomHeight: 0 + theme: 'auto', + openBtnPosition: 'bottomLeft', + hideEmptyTabs: false, }, - initialize: function() { + initialize: function(options = {}) { + this.options = $.extend(this.options, options); + this.defaultOptions = { ...this.options }; this.controls = {}; this.dataMap = {}; this.datasets = {}; this.firstTabName = null; this.activePanelName = null; + this.activeDatasetId = null; this.datesetTitleFormater = new DatasetTitleFormater(this); - this.options.bodyMarginBottomHeight = parseInt($('body').css('margin-bottom')); + this.bodyMarginBottomHeight = parseInt($('body').css('margin-bottom')); + try { + this.isIframe = window.self !== window.top && window.top.phpdebugbar; + } catch (error) { + this.isIframe = false; + } this.registerResizeHandler(); + this.registerMediaListener(); + + // Attach settings + this.settings = new PhpDebugBar.DebugBar.Tab({"icon":"sliders", "title":"Settings", "widget": new Settings({ + 'debugbar': this, + })}); }, /** @@ -431,7 +613,7 @@ if (typeof(PhpDebugBar) == 'undefined') { * @this {DebugBar} */ registerResizeHandler: function() { - if (typeof this.resize.bind == 'undefined') return; + if (typeof this.resize.bind == 'undefined' || this.isIframe) return; var f = this.resize.bind(this); this.respCSSSize = 0; @@ -439,19 +621,44 @@ if (typeof(PhpDebugBar) == 'undefined') { setTimeout(f, 20); }, + registerMediaListener: function() { + const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); + mediaQueryList.addEventListener('change', event => { + if (this.options.theme === 'auto') { + this.setTheme('auto'); + } + }) + }, + + setTheme: function(theme) { + this.options.theme = theme; + + if (theme === 'auto') { + const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); + theme = mediaQueryList.matches ? 'dark' : 'light'; + } + + this.$el.attr('data-theme', theme) + if (this.openHandler) { + this.openHandler.$el.attr('data-theme', theme) + } + }, + /** * Resizes the debugbar to fit the current browser window */ resize: function() { + if (this.isIframe) return; + var contentSize = this.respCSSSize; if (this.respCSSSize == 0) { - this.$header.find("> div > *:visible").each(function () { - contentSize += $(this).outerWidth(); + this.$header.find("> *:visible").each(function () { + contentSize += $(this).outerWidth(true); }); } var currentSize = this.$header.width(); - var cssClass = "phpdebugbar-mini-design"; + var cssClass = csscls("mini-design"); var bool = this.$header.hasClass(cssClass); if (currentSize <= contentSize && !bool) { @@ -472,6 +679,10 @@ if (typeof(PhpDebugBar) == 'undefined') { * @this {DebugBar} */ render: function() { + if (this.isIframe) { + this.$el.hide(); + } + var self = this; this.$el.appendTo('body'); this.$dragCapture = $('
    ').addClass(csscls('drag-capture')).appendTo(this.$el); @@ -537,11 +748,26 @@ if (typeof(PhpDebugBar) == 'undefined') { }); // select box for data sets - this.$datasets = $('').addClass(csscls('datasets-switcher')).attr('name', 'datasets-switcher') + .appendTo(this.$headerRight); this.$datasets.change(function() { - self.dataChangeHandler(self.datasets[this.value]); - self.showTab(); + self.showDataSet(this.value); }); + + this.controls['__settings'] = this.settings; + this.settings.$tab.addClass(csscls('tab-settings')); + this.settings.$tab.attr('data-collector', '__settings'); + this.settings.$el.attr('data-collector', '__settings'); + this.settings.$tab.insertAfter(this.$minimizebtn).show(); + this.settings.$tab.click(() => { + if (!this.isMinimized() && this.activePanelName == '__settings') { + this.minimize(); + } else { + this.showTab('__settings'); + this.settings.get('widget').render(); + } + }); + this.settings.$el.appendTo(this.$body); }, /** @@ -553,13 +779,13 @@ if (typeof(PhpDebugBar) == 'undefined') { * @this {DebugBar} */ setHeight: function(height) { - var min_h = 40; - var max_h = $(window).innerHeight() - this.$header.height() - 10; - height = Math.min(height, max_h); - height = Math.max(height, min_h); - this.$body.css('height', height); - localStorage.setItem('phpdebugbar-height', height); - this.recomputeBottomOffset(); + var min_h = 40; + var max_h = window.innerHeight - this.$header.height() - 10; + height = Math.min(height, max_h); + height = Math.max(height, min_h); + this.$body.css('height', height); + localStorage.setItem('phpdebugbar-height', height); + this.recomputeBottomOffset(); }, /** @@ -570,6 +796,7 @@ if (typeof(PhpDebugBar) == 'undefined') { * @this {DebugBar} */ restoreState: function() { + if (this.isIframe) return; // bar height var height = localStorage.getItem('phpdebugbar-height'); this.setHeight(height || this.$body.height()); @@ -584,6 +811,8 @@ if (typeof(PhpDebugBar) == 'undefined') { var tab = localStorage.getItem('phpdebugbar-tab'); if (this.isTab(tab)) { this.showTab(tab); + } else { + this.showTab(); } } } @@ -626,7 +855,10 @@ if (typeof(PhpDebugBar) == 'undefined') { } else { self.showTab(name); } - }); + }) + tab.$tab.attr('data-empty', true); + tab.$tab.attr('data-collector', name); + tab.$el.attr('data-collector', name); tab.$el.appendTo(this.$body); this.controls[name] = tab; @@ -642,7 +874,7 @@ if (typeof(PhpDebugBar) == 'undefined') { * @this {DebugBar} * @param {String} name Internal name * @param {String} icon - * @param {String} tooltip + * @param {String|Object} tooltip * @param {String} position "right" or "left", default is "right" * @return {Indicator} */ @@ -667,6 +899,8 @@ if (typeof(PhpDebugBar) == 'undefined') { throw new Error(name + ' already exists'); } + indicator.set('debugbar', this); + if (position == 'left') { indicator.$el.insertBefore(this.$headerLeft.children().first()); } else { @@ -772,6 +1006,7 @@ if (typeof(PhpDebugBar) == 'undefined') { this.$el.removeClass(csscls('minimized')); localStorage.setItem('phpdebugbar-visible', '1'); localStorage.setItem('phpdebugbar-tab', name); + this.resize(); }, @@ -850,10 +1085,10 @@ if (typeof(PhpDebugBar) == 'undefined') { recomputeBottomOffset: function() { if (this.options.bodyMarginBottom) { if (this.isClosed()) { - return $('body').css('margin-bottom', this.options.bodyMarginBottomHeight || ''); + return $('body').css('margin-bottom', this.bodyMarginBottomHeight || ''); } - var offset = parseInt(this.$el.height()) + (this.options.bodyMarginBottomHeight || 0); + var offset = parseInt(this.$el.height()) + (this.bodyMarginBottomHeight || 0); $('body').css('margin-bottom', offset); } }, @@ -920,10 +1155,27 @@ if (typeof(PhpDebugBar) == 'undefined') { * @return {String} Dataset's id */ addDataSet: function(data, id, suffix, show) { - var label = this.datesetTitleFormater.format(id, data, suffix); - id = id || (getObjectSize(this.datasets) + 1); + if (!data || !data.__meta) return; + if (this.isIframe) { + window.top.phpdebugbar.addDataSet(data, id, '(iframe)' + (suffix || ''), show); + return; + } + + var nb = getObjectSize(this.datasets) + 1; + id = id || nb; + data.__meta['nb'] = nb; + data.__meta['suffix'] = suffix; this.datasets[id] = data; + var label = this.datesetTitleFormater.format(id, this.datasets[id], suffix, nb); + + if (this.datasetTab) { + this.datasetTab.set('data', this.datasets); + var datasetSize = getObjectSize(this.datasets); + this.datasetTab.set('badge', datasetSize > 1 ? datasetSize : null); + this.datasetTab.$tab.show(); + } + this.$datasets.append($('')); if (this.$datasets.children().length > 1) { this.$datasets.show(); @@ -932,6 +1184,9 @@ if (typeof(PhpDebugBar) == 'undefined') { if (typeof(show) == 'undefined' || show) { this.showDataSet(id); } + + this.resize(); + return id; }, @@ -971,8 +1226,16 @@ if (typeof(PhpDebugBar) == 'undefined') { * @param {String} id */ showDataSet: function(id) { + this.activeDatasetId = id; this.dataChangeHandler(this.datasets[id]); - this.$datasets.val(id); + + if (this.$datasets.val() !== id) { + this.$datasets.val(id); + } + + if (this.datasetTab) { + this.datasetTab.get('widget').set('id', id); + } }, /** @@ -992,6 +1255,7 @@ if (typeof(PhpDebugBar) == 'undefined') { self.getControl(key).set('data', d); } }); + self.resize(); }, /** @@ -1002,6 +1266,7 @@ if (typeof(PhpDebugBar) == 'undefined') { */ setOpenHandler: function(handler) { this.openHandler = handler; + this.openHandler.$el.attr('data-theme', this.$el.attr('data-theme')); if (handler !== null) { this.$openbtn.show(); } else { @@ -1017,8 +1282,26 @@ if (typeof(PhpDebugBar) == 'undefined') { */ getOpenHandler: function() { return this.openHandler; - } + }, + enableAjaxHandlerTab: function() { + this.datasetTab = new PhpDebugBar.DebugBar.Tab({"icon":"history", "title":"Request history", "widget": new PhpDebugBar.Widgets.DatasetWidget({ + 'debugbar': this + })}); + this.datasetTab.$tab.addClass(csscls('tab-history')); + this.datasetTab.$tab.attr('data-collector', '__datasets'); + this.datasetTab.$el.attr('data-collector', '__datasets'); + this.datasetTab.$tab.insertAfter(this.$openbtn).hide(); + this.datasetTab.$tab.click(() => { + if (!this.isMinimized() && this.activePanelName == '__datasets') { + this.minimize(); + } else { + this.showTab('__datasets'); + } + }); + this.datasetTab.$el.appendTo(this.$body); + this.controls['__datasets'] = this.datasetTab; + }, }); DebugBar.Tab = Tab; @@ -1037,6 +1320,13 @@ if (typeof(PhpDebugBar) == 'undefined') { this.debugbar = debugbar; this.headerName = headerName || 'phpdebugbar'; this.autoShow = typeof(autoShow) == 'undefined' ? true : autoShow; + this.defaultAutoShow = this.autoShow; + if (localStorage.getItem('phpdebugbar-ajaxhandler-autoshow') !== null) { + this.autoShow = localStorage.getItem('phpdebugbar-ajaxhandler-autoshow') == '1'; + } + if (debugbar.controls['__settings']) { + debugbar.controls['__settings'].get('widget').set('autoshow', this.autoShow); + } }; $.extend(AjaxHandler.prototype, { @@ -1077,6 +1367,11 @@ if (typeof(PhpDebugBar) == 'undefined') { return Object.prototype.toString.call(response) == '[object XMLHttpRequest]' }, + setAutoShow: function(autoshow) { + this.autoShow = autoshow; + localStorage.setItem('phpdebugbar-ajaxhandler-autoshow', autoshow ? '1' : '0'); + }, + /** * Checks if the HEADER-id exists and loads the dataset using the open handler * @@ -1174,6 +1469,9 @@ if (typeof(PhpDebugBar) == 'undefined') { promise.then(function (response) { self.handle(response); + }).catch(function(reason) { + // Fetch request failed or aborted via AbortController.abort(). + // Catch is required to not trigger React's error handler. }); return promise; @@ -1181,10 +1479,7 @@ if (typeof(PhpDebugBar) == 'undefined') { }, /** - * Attaches an event listener to jQuery.ajaxComplete() - * - * @this {AjaxHandler} - * @param {jQuery} jq Optional + * @deprecated use bindToXHR instead */ bindToJquery: function(jq) { var self = this; @@ -1208,7 +1503,7 @@ if (typeof(PhpDebugBar) == 'undefined') { this.addEventListener("readystatechange", function() { var skipUrl = self.debugbar.openHandler ? self.debugbar.openHandler.get('url') : null; var href = (typeof url === 'string') ? url : url.href; - + if (xhr.readyState == 4 && href.indexOf(skipUrl) !== 0) { self.handle(xhr); } diff --git a/src/DebugBar/Resources/openhandler.css b/src/DebugBar/Resources/openhandler.css index 863a8bd5..ed32216a 100644 --- a/src/DebugBar/Resources/openhandler.css +++ b/src/DebugBar/Resources/openhandler.css @@ -16,31 +16,52 @@ div.phpdebugbar-openhandler { bottom: 0; left: 0; right: 0; - width: 70%; + width: 80%; height: 70%; - background: #fff; - color: #000; - border: 2px solid #888; + background: var(--debugbar-background); + color: var(--debugbar-text); + border: 2px solid var(--debugbar-header-border); overflow: auto; z-index: 20001; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: var(--debugbar-font-sans); font-size: 14px; - padding-bottom: 10px; + padding: 0; } + div.phpdebugbar-openhandler select, div.phpdebugbar-openhandler input { + appearance: auto; + } + + div.phpdebugbar-openhandler input, div.phpdebugbar-openhandler select { + color: var(--debugbar-header-text); + background-color: var(--debugbar-header); + border: 1px solid var(--debugbar-header-border); + border-radius: 0.25rem; + height: 20px; + margin: 0 5px; + padding: 0; + } + + div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions input[name="uri"] { + width: 200px; + } + + div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions input[name="ip"] { + width: 90px; + } div.phpdebugbar-openhandler a { - color: #555; + color: var(--debugbar-header-text); } div.phpdebugbar-openhandler .phpdebugbar-openhandler-header { - background: #efefef url(data:image/png;base64,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) no-repeat 5px 4px; + background: var(--debugbar-header) url(data:image/png;base64,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) no-repeat 5px 4px; padding-left: 29px; min-height: 26px; line-height: 25px; - color: #555; + color: var(--debugbar-header-text); margin-bottom: 10px; } div.phpdebugbar-openhandler .phpdebugbar-openhandler-header a { font-size: 14px; - color: #555; + color: var(--debugbar-header-text); text-decoration: none; float: right; padding: 5px 8px; @@ -50,9 +71,23 @@ div.phpdebugbar-openhandler { table-layout: fixed; font-size: 14px; } + div.phpdebugbar-openhandler table td, + div.phpdebugbar-openhandler table th { + border: 0px solid var(--debugbar-border); + padding: 2px 8px; + } + div.phpdebugbar-openhandler table th, + div.phpdebugbar-openhandler table tr:nth-child(2n) { + background-color: var(--debugbar-background-alt); + } + div.phpdebugbar-openhandler table th:nth-child(2), div.phpdebugbar-openhandler table td:nth-child(2), /* Method */ + div.phpdebugbar-openhandler table th:nth-child(4), div.phpdebugbar-openhandler table td:nth-child(4), /* IP */ + div.phpdebugbar-openhandler table th:nth-child(5), div.phpdebugbar-openhandler table td:nth-child(5) { /* Filter */ + text-align: center; + } div.phpdebugbar-openhandler table td { padding: 6px 3px; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--debugbar-border); } div.phpdebugbar-openhandler table td a{ display: block; @@ -65,6 +100,10 @@ div.phpdebugbar-openhandler { padding: 7px 0; } div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions a { - margin: 0 10px; - color: #555; + color: var(--debugbar-header-text); + background-color: var(--debugbar-header); + border: 1px solid var(--debugbar-header-border); + border-radius: 0.25rem; + margin: 5px; + padding: 4px 12px 4px; } diff --git a/src/DebugBar/Resources/openhandler.js b/src/DebugBar/Resources/openhandler.js index 5633043f..5b673206 100644 --- a/src/DebugBar/Resources/openhandler.js +++ b/src/DebugBar/Resources/openhandler.js @@ -25,7 +25,7 @@ if (typeof(PhpDebugBar) == 'undefined') { this.$closebtn = $(''); this.$table = $(''); $('
    PHP DebugBar | Open
    ').addClass(csscls('header')).append(this.$closebtn).appendTo(this.$el); - $('
    DateMethodURLIPFilter data
    ').append(this.$table).appendTo(this.$el); + $('
    DateMethodURLIPFilter data
    ').append(this.$table).appendTo(this.$el); this.$actions = $('
    ').addClass(csscls('actions')).appendTo(this.$el); this.$closebtn.on('click', function() { @@ -58,7 +58,7 @@ if (typeof(PhpDebugBar) == 'undefined') { self.hide(); }); }); - + this.addSearch(); this.$overlay = $('
    ').addClass(csscls('overlay')).hide().appendTo('body'); @@ -72,7 +72,7 @@ if (typeof(PhpDebugBar) == 'undefined') { this.$loadmorebtn.show(); this.find({}, 0, this.handleFind.bind(this)); }, - + addSearch: function(){ var self = this; var searchBtn = $('