diff --git a/.github/workflows/run-integration.yml b/.github/workflows/run-integration.yml index 40aca15..bd90874 100644 --- a/.github/workflows/run-integration.yml +++ b/.github/workflows/run-integration.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: - php: [8.4, 8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2] + php: [8.4, 8.3, 8.2, 8.1, 8.0] name: PHP${{ matrix.php }} diff --git a/.github/workflows/run-screenshots.yml b/.github/workflows/run-screenshots.yml new file mode 100644 index 0000000..8139c2a --- /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 3c05c2e..676d28f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: - php: [8.4, 8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2] + php: [8.4, 8.3, 8.2, 8.1, 8.0] name: PHP${{ matrix.php }} @@ -38,4 +38,4 @@ jobs: run: composer update --prefer-dist --no-progress - name: Execute Unit Tests - run: vendor/bin/phpunit --testsuite=Unit + run: vendor/bin/phpunit --testsuite=Unit \ No newline at end of file diff --git a/.gitignore b/.gitignore index db0f376..01ff6a4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,10 @@ composer.lock /vendor /demo/bridge/*/vendor /demo/bridge/doctrine/db.sqlite +/demo/profiles /src/DebugBar/Resources/vendor .phpunit.result.cache /drivers +/chromedriver .phpunit.cache/ +/tests/screenshots \ No newline at end of file diff --git a/README.md b/README.md index 04a6a15..2583027 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Displays a debug bar in the browser with information from php. No more `var_dump()` in your code! +> **Note: Debug Bar is for development use only. Never install this on websites that are publicly accessible.** + ![Screenshot](https://raw.github.com/maximebf/php-debugbar/master/docs/screenshot.png) **Features:** @@ -57,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 maximebf/debugbar --dev +``` ## Quick start diff --git a/chromedriver b/chromedriver deleted file mode 100755 index 5a809fa..0000000 Binary files a/chromedriver and /dev/null differ diff --git a/composer.json b/composer.json index da3f121..4e50eed 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,7 +17,7 @@ } ], "require": { - "php": "^7.2|^8", + "php": "^8", "psr/log": "^1|^2|^3", "symfony/var-dumper": "^4|^5|^6|^7" }, @@ -56,7 +56,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.22-dev" + "dev-master": "2.0-dev" } } } diff --git a/demo/bootstrap.php b/demo/bootstrap.php index 32b5057..b7031ba 100644 --- a/demo/bootstrap.php +++ b/demo/bootstrap.php @@ -11,7 +11,9 @@ $debugbar = new StandardDebugBar(); $debugbarRenderer = $debugbar->getJavascriptRenderer() ->setBaseUrl('../src/DebugBar/Resources') ->setAjaxHandlerEnableTab(true) - ->setEnableJqueryNoConflict(false); + ->setHideEmptyTabs(true) + ->setEnableJqueryNoConflict(false) + ->setTheme($_GET['theme'] ?? 'auto'); // // create a writable profiles folder in the demo directory to uncomment the following lines @@ -20,7 +22,7 @@ $debugbarRenderer = $debugbar->getJavascriptRenderer() // $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; ?> diff --git a/demo/bridge/doctrine/index.php b/demo/bridge/doctrine/index.php index 9a00acf..f771502 100644 --- a/demo/bridge/doctrine/index.php +++ b/demo/bridge/doctrine/index.php @@ -11,9 +11,10 @@ $debugbar->addCollector(new DebugBar\Bridge\DoctrineCollector($debugStack)); $product = new Demo\Product(); $product->setName("foobar"); - +$product->setUpdated(); $entityManager->persist($product); $entityManager->flush(); +$entityManager->createQuery("select p from Demo\\Product p where p.updated>:u")->setParameter("u", new \DateTime('1 hour ago'))->execute(); $entityManager->createQuery("select p from Demo\\Product p where p.name=:c")->setParameter("c", "")->execute(); render_demo_page(); diff --git a/demo/bridge/doctrine/src/Demo/Product.php b/demo/bridge/doctrine/src/Demo/Product.php index c7d8f08..d2a8a4f 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 @@ class Product { $this->name = $name; } + + public function setUpdated(): void + { + // will NOT be saved in the database + $this->updated = new \DateTime('now'); + } } diff --git a/src/DebugBar/Bridge/CacheCacheCollector.php b/src/DebugBar/Bridge/CacheCacheCollector.php index 7e7f46f..55d87cc 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 d33b7c9..f5cf0f0 100644 --- a/src/DebugBar/Bridge/DoctrineCollector.php +++ b/src/DebugBar/Bridge/DoctrineCollector.php @@ -60,7 +60,7 @@ class DoctrineCollector extends DataCollector implements Renderable, AssetProvid foreach ($this->debugStack->queries as $q) { $queries[] = array( 'sql' => $q['sql'], - 'params' => (object) $this->getParameters($q), + 'params' => (object) $this->getParameters($q['params'] ?? []), 'duration' => $q['executionMS'], 'duration_str' => $this->formatDuration($q['executionMS']) ); @@ -80,13 +80,22 @@ class DoctrineCollector extends DataCollector implements Renderable, AssetProvid * * @return array */ - public function getParameters($query) : array + public function getParameters($params) : array { - $params = []; - foreach ($query['params'] ?? [] as $name => $param) { - $params[$name] = htmlentities($param?:"", ENT_QUOTES, 'UTF-8', false); - } - return $params; + 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); } /** diff --git a/src/DebugBar/Bridge/MonologCollector.php b/src/DebugBar/Bridge/MonologCollector.php index 49eb37c..25063ea 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/PropelCollector.php b/src/DebugBar/Bridge/PropelCollector.php index 93ad4ff..4a287ee 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 @@ class PropelCollector extends DataCollector implements BasicLogger, Renderable, * @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/Twig/TimeableTwigExtensionProfiler.php b/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php index 9152738..9dea9b6 100644 --- a/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php +++ b/src/DebugBar/Bridge/Twig/TimeableTwigExtensionProfiler.php @@ -36,7 +36,7 @@ class TimeableTwigExtensionProfiler extends ProfilerExtension $this->timeDataCollector = $timeDataCollector; } - public function __construct(Profile $profile, TimeDataCollector $timeDataCollector = null) + public function __construct(Profile $profile, ?TimeDataCollector $timeDataCollector = null) { parent::__construct($profile); diff --git a/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php b/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php index 9e98c19..9dcf0d7 100644 --- a/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php +++ b/src/DebugBar/Bridge/Twig/TraceableTwigEnvironment.php @@ -24,7 +24,7 @@ use Twig_TokenStream; /** * 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/ExceptionsCollector.php b/src/DebugBar/DataCollector/ExceptionsCollector.php index 64adcf4..1ca1cfa 100644 --- a/src/DebugBar/DataCollector/ExceptionsCollector.php +++ b/src/DebugBar/DataCollector/ExceptionsCollector.php @@ -97,11 +97,22 @@ class ExceptionsCollector extends DataCollector implements Renderable 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; } diff --git a/src/DebugBar/DataCollector/PDO/PDOCollector.php b/src/DebugBar/DataCollector/PDO/PDOCollector.php index 0ea563b..6e7f15b 100644 --- a/src/DebugBar/DataCollector/PDO/PDOCollector.php +++ b/src/DebugBar/DataCollector/PDO/PDOCollector.php @@ -26,7 +26,7 @@ class PDOCollector extends DataCollector implements Renderable, AssetProvider * @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) { @@ -138,7 +138,7 @@ class PDOCollector extends DataCollector implements Renderable, AssetProvider * @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'; diff --git a/src/DebugBar/DataCollector/PDO/TracedStatement.php b/src/DebugBar/DataCollector/PDO/TracedStatement.php index 9111489..0a78fa6 100644 --- a/src/DebugBar/DataCollector/PDO/TracedStatement.php +++ b/src/DebugBar/DataCollector/PDO/TracedStatement.php @@ -57,7 +57,7 @@ class TracedStatement * @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; diff --git a/src/DebugBar/DataFormatter/HasXdebugLinks.php b/src/DebugBar/DataFormatter/HasXdebugLinks.php index ea4f734..778c15c 100644 --- a/src/DebugBar/DataFormatter/HasXdebugLinks.php +++ b/src/DebugBar/DataFormatter/HasXdebugLinks.php @@ -120,11 +120,12 @@ trait HasXdebugLinks 'vscode-remote' => 'vscode://vscode-remote/%f:%l', 'vscode-insiders-remote' => 'vscode-insiders://vscode-remote/%f:%l', 'vscodium' => 'vscodium://file/%f:%l', - 'nova' => 'nova://core/open/file?filename=%f&line=%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', ); if (is_string($editor) && isset($editorLinkTemplates[$editor])) { diff --git a/src/DebugBar/DebugBar.php b/src/DebugBar/DebugBar.php index c64e729..2c31c0b 100644 --- a/src/DebugBar/DebugBar.php +++ b/src/DebugBar/DebugBar.php @@ -148,7 +148,7 @@ class DebugBar implements ArrayAccess * @param StorageInterface $storage * @return $this */ - public function setStorage(StorageInterface $storage = null) + public function setStorage(?StorageInterface $storage = null) { $this->storage = $storage; return $this; diff --git a/src/DebugBar/JavascriptRenderer.php b/src/DebugBar/JavascriptRenderer.php index 41c17c5..7377147 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(); @@ -157,6 +161,12 @@ class JavascriptRenderer 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); @@ -397,6 +407,40 @@ class JavascriptRenderer 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 * @@ -1036,7 +1080,7 @@ class JavascriptRenderer 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; @@ -1075,7 +1119,7 @@ class JavascriptRenderer $nonce = $this->getNonceAttribute(); - if ($nonce != '') { + if ($nonce != '') { $js = preg_replace("/