From 836050cea5494468d5889941b61415f7831963a9 Mon Sep 17 00:00:00 2001 From: maximebf Date: Thu, 13 Jun 2013 18:48:23 +0800 Subject: [PATCH] huge refactoring, comments and docs --- LICENSE | 19 + README.md | 84 +++- composer.json | 19 + demo/bootstrap.php | 3 +- demo/demo.php | 5 +- demo/demo_ajax.php | 3 +- demo/demo_ajax_exception.php | 3 +- docs/data_collectors.md | 80 +++ docs/javascript_bar.md | 120 +++++ docs/manifest.json | 10 + docs/rendering.md | 117 +++++ docs/screenshot.png | Bin 0 -> 15430 bytes src/DebugBar/DataCollector/DataCollector.php | 17 + .../DataCollector/DataCollectorInterface.php | 25 +- .../DataCollector/DependencyCollector.php | 24 +- .../DataCollector/MemoryCollector.php | 59 ++- .../DataCollector/MessagesCollector.php | 50 +- .../DataCollector/PhpInfoCollector.php | 27 +- src/DebugBar/DataCollector/Renderable.php | 25 + .../DataCollector/RequestDataCollector.php | 43 +- ...imeCollector.php => TimeDataCollector.php} | 116 ++++- src/DebugBar/DebugBar.php | 109 ++++- src/DebugBar/DebugBarException.php | 8 + src/DebugBar/JavascriptRenderer.php | 407 ++++++++++++++++ src/DebugBar/Renderer/JavascriptRenderer.php | 159 ------ src/DebugBar/StandardDebugBar.php | 18 +- tests/DebugBar/Tests/DebugBarTestCase.php | 8 + web/debugbar.css | 338 +++++++------ web/debugbar.js | 461 +++++++++++++++--- web/standard-debugbar.js | 30 -- web/widgets.js | 266 +++++++--- 31 files changed, 2082 insertions(+), 571 deletions(-) create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 docs/data_collectors.md create mode 100644 docs/javascript_bar.md create mode 100644 docs/manifest.json create mode 100644 docs/rendering.md create mode 100644 docs/screenshot.png create mode 100644 src/DebugBar/DataCollector/Renderable.php rename src/DebugBar/DataCollector/{TimeCollector.php => TimeDataCollector.php} (53%) create mode 100644 src/DebugBar/JavascriptRenderer.php delete mode 100644 src/DebugBar/Renderer/JavascriptRenderer.php create mode 100644 tests/DebugBar/Tests/DebugBarTestCase.php delete mode 100644 web/standard-debugbar.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1344c98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2013 Maxime Bouroumeau-Fuseau + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f7dbcbe..d3aaba7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,83 @@ -# Debug bar for PHP +# PHP Debug Bar -Displays a debug bar in the browser with information from php \ No newline at end of file +Displays a debug bar in the browser with information from php. +No more `var_dump()` in your code! + +![Screenshot](docs/screenshot.png) + +**Features:** + + - Generic debug bar with no other dependencies + - Easy to integrate with any project + - Clean, fast and easy to use interface + - Handles AJAX request + - Includes generic data collectors and collectors for well known libraries + - The client side bar is 100% coded in javascript + - Easily create your own collectors and their associated view in the bar + + +## Installation + +The easiest way to install DebugBar is using [Composer](https://github.com/composer/composer) +with the following requirement: + + { + "require": { + "maximebf/debugbar": ">=1.0.0" + } + } + +Alternatively, you can [download the archive](https://github.com/maximebf/php-debugbar/zipball/master) +and add the src/ folder to PHP's include path: + + set_include_path('/path/to/src' . PATH_SEPARATOR . get_include_path()); + +DebugBar does not provide an autoloader but follows the [PSR-0 convention](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md). +You can use the following snippet to autoload ConsoleKit classes: + + spl_autoload_register(function($className) { + if (substr($className, 0, 8) === 'DebugBar') { + $filename = str_replace('\\', DIRECTORY_SEPARATOR, trim($className, '\\')) . '.php'; + require_once $filename; + } + }); + +## Quick start + +DebugBar is very easy to use and you can add it to any of your projets in no time. +The easiest way is using the `render()` functions + + getJavascriptRenderer(); + + $debugbar["messages"]->addMessage("hello world!"); + ?> + + + renderHead() ?> + + + ... + render() ?> + + + +The DebugBar uses DataCollectors to collect data from your PHP code. Some of them are +automated but others are manual. Use the `DebugBar` like an array where keys are the +collector names. In our previous example, we add a message to the `MessagesCollector`: + + $debugbar["messages"]->addMessage("hello world!"); + +`StandardDebugBar` activates all bundled collectors: + + - `MemoryCollector` (*memory*) + - `MessagesCollector` (*messages*) + - `PhpInfoCollector` (*php*) + - `RequestDataCollector` (*request*) + - `TimeDataCollector` (*time*) + +Learn more about DebugBar in the [docs](http://maximebf.github.io/php-debugbar). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..30bbadc --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "maximebf/debugbar", + "description": "Debug bar in the browser for php application", + "keywords": ["debug"], + "homepage": "https://github.com/maximebf/php-debugbar", + "type": "library", + "license": "MIT", + "authors": [{ + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": {"DebugBar": "src/"} + } +} diff --git a/demo/bootstrap.php b/demo/bootstrap.php index dd18b36..771a0c8 100644 --- a/demo/bootstrap.php +++ b/demo/bootstrap.php @@ -3,7 +3,6 @@ include '../tests/bootstrap.php'; use DebugBar\StandardDebugBar; -use DebugBar\Renderer\JavascriptRenderer; $debugbar = new StandardDebugBar(); -$debugbarRenderer = new JavascriptRenderer($debugbar, '../web/'); +$debugbarRenderer = $debugbar->getJavascriptRenderer()->setBaseUrl('../web'); diff --git a/demo/demo.php b/demo/demo.php index 97cd42e..3c637ce 100644 --- a/demo/demo.php +++ b/demo/demo.php @@ -20,7 +20,7 @@ $debugbar['time']->startMeasure('render'); ?> - renderIncludes() ?> + renderHead() ?> + +Using `setInitialization(0)` will only render the addDataSet part. + +### Defining controls + +Controls can be manually added to the debug bar using `addControl($name, $options)`. You should read +the Javascript bar chapter before this section. + +`$name` will be the name of your control and `$options` is a key/value pair array with these +possible values: + +- *icon*: icon name +- *tooltip*: string +- *widget*: widget class name +- *map*: a property name from the data to map the control to +- *default*: a js string, default value of the data map + +At least *icon* or *widget* are needed. If *widget* is specified, a tab will be created, otherwise +an indicator. + + $renderer->addControl('messages', array( + "widget" => "PhpDebugBar.Widgets.MessagesWidget", + "map" => "messages", + "default" => "[]" + )); diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..adddef1936c3fdbc83b2ef3ce33e7fbbc2c0e4cc GIT binary patch literal 15430 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 } + /** + * Updates the peak memory usage value + */ public function updatePeakUsage() { $this->peakUsage = memory_get_peak_usage(true); } + /** + * Transforms a size in bytes to a human readable string + * + * @param string $size + * @param integer $precision + * @return string + */ public function toReadableString($size, $precision = 2) { $base = log($size) / log(1024); @@ -28,6 +49,9 @@ class MemoryCollector extends DataCollector return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)]; } + /** + * {@inheritDoc} + */ public function collect() { $this->updatePeakUsage(); @@ -36,4 +60,27 @@ class MemoryCollector extends DataCollector 'peak_usage_str' => $this->toReadableString($this->peakUsage) ); } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'memory'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + return array( + "memory" => array( + "icon" => "cogs", + "tooltip" => "Memory Usage", + "map" => "peak_usage_str", + "default" => "'0B'" + ) + ); + } } diff --git a/src/DebugBar/DataCollector/MessagesCollector.php b/src/DebugBar/DataCollector/MessagesCollector.php index 9880529..92b88d4 100644 --- a/src/DebugBar/DataCollector/MessagesCollector.php +++ b/src/DebugBar/DataCollector/MessagesCollector.php @@ -1,11 +1,30 @@ messages[] = array( @@ -18,18 +37,43 @@ class MessagesCollector extends DataCollector ); } + /** + * Returns all messages + * + * @return array + */ public function getMessages() { return $this->messages; } + /** + * {@inheritDoc} + */ + public function collect() + { + return $this->messages; + } + + /** + * {@inheritDoc} + */ public function getName() { return 'messages'; } - public function collect() + /** + * {@inheritDoc} + */ + public function getWidgets() { - return $this->messages; + return array( + "messages" => array( + "widget" => "PhpDebugBar.Widgets.MessagesWidget", + "map" => "messages", + "default" => "[]" + ) + ); } } diff --git a/src/DebugBar/DataCollector/PhpInfoCollector.php b/src/DebugBar/DataCollector/PhpInfoCollector.php index d81ff66..a88a966 100644 --- a/src/DebugBar/DataCollector/PhpInfoCollector.php +++ b/src/DebugBar/DataCollector/PhpInfoCollector.php @@ -1,18 +1,35 @@ PHP_VERSION ); } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'php'; + } } diff --git a/src/DebugBar/DataCollector/Renderable.php b/src/DebugBar/DataCollector/Renderable.php new file mode 100644 index 0000000..9b2bbc5 --- /dev/null +++ b/src/DebugBar/DataCollector/Renderable.php @@ -0,0 +1,25 @@ + array( + "widget" => "PhpDebugBar.Widgets.VariableListWidget", + "map" => "request", + "default" => "{}" + ) + ); + } } diff --git a/src/DebugBar/DataCollector/TimeCollector.php b/src/DebugBar/DataCollector/TimeDataCollector.php similarity index 53% rename from src/DebugBar/DataCollector/TimeCollector.php rename to src/DebugBar/DataCollector/TimeDataCollector.php index d12da39..93e8a5c 100644 --- a/src/DebugBar/DataCollector/TimeCollector.php +++ b/src/DebugBar/DataCollector/TimeDataCollector.php @@ -1,16 +1,30 @@ requestStartTime = $requestStartTime; } + /** + * Starts a measure + * + * @param string $name Internal name, used to stop the measure + * @param string $label Public name + */ public function startMeasure($name, $label = null) { $start = microtime(true); @@ -33,6 +53,11 @@ class TimeCollector extends DataCollector ); } + /** + * Stops a measure + * + * @param string $name + */ public function stopMeasure($name) { $end = microtime(true); @@ -42,16 +67,54 @@ class TimeCollector extends DataCollector $this->measures[$name]['duration_str'] = $this->toReadableString($this->measures[$name]['duration']); } + /** + * Utility function to measure the execution of a Closure + * + * @param Closure $closure + */ + public function measure(\Closure $closure) + { + $name = spl_object_hash($closure); + $this->startMeasure($name, $closure); + $closure(); + $this->stopMeasure($name); + } + + /** + * Returns an array of all measures + * + * @return array + */ + public function getMeasures() + { + return $this->measures; + } + + /** + * Returns the request start time + * + * @return float + */ public function getRequestStartTime() { return $this->requestStartTime; } + /** + * Returns the request end time + * + * @return float + */ public function getRequestEndTime() { return $this->requestEndTime; } + /** + * Returns the duration of a request + * + * @return float + */ public function getRequestDuration() { if ($this->requestEndTime !== null) { @@ -60,21 +123,20 @@ class TimeCollector extends DataCollector return microtime(true) - $this->requestStartTime; } - public function getMeasures() - { - return $this->measures; - } - + /** + * Transforms a duration in seconds in a readable string + * + * @param float $value + * @return string + */ public function toReadableString($value) { - return round($value / 1000) . 'ms'; - } - - public function getName() - { - return 'time'; + return round($value * 1000) . 'ms'; } + /** + * {@inheritDoc} + */ public function collect() { $this->requestEndTime = microtime(true); @@ -92,4 +154,32 @@ class TimeCollector extends DataCollector 'measures' => array_values($this->measures) ); } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'time'; + } + + /** + * {@inheritDoc} + */ + public function getWidgets() + { + return array( + "time" => array( + "icon" => "time", + "tooltip" => "Request Duration", + "map" => "time.duration_str", + "default" => "'0ms'" + ), + "timeline" => array( + "widget" => "PhpDebugBar.Widgets.TimelineWidget", + "map" => "time", + "default" => "{}" + ) + ); + } } diff --git a/src/DebugBar/DebugBar.php b/src/DebugBar/DebugBar.php index 6d462e8..08cc789 100644 --- a/src/DebugBar/DebugBar.php +++ b/src/DebugBar/DebugBar.php @@ -1,28 +1,69 @@ + * $debugbar = new DebugBar(); + * $debugbar->addCollector(new DataCollector\MessagesCollector()); + * $debugbar['messages']->addMessage("foobar"); + * + */ class DebugBar implements ArrayAccess { protected $collectors = array(); protected $data; + + protected $jsRenderer; + /** + * Adds a data collector + * + * @param DataCollector $collector + */ public function addCollector(DataCollector $collector) { + if (isset($this->collectors[$collector->getName()])) { + throw new DebugBarException("'$name' is already a registered collector"); + } $this->collectors[$collector->getName()] = $collector; + return $this; } + /** + * Checks if a data collector has been added + * + * @param string $name + * @return boolean + */ public function hasCollector($name) { return isset($this->collectors[$name]); } + /** + * Returns a data collector + * + * @param string $name + * @return DataCollector + */ public function getCollector($name) { if (!isset($this->collectors[$name])) { @@ -31,11 +72,61 @@ class DebugBar implements ArrayAccess return $this->collectors[$name]; } + /** + * Returns an array of all data collectors + * + * @return array[DataCollector] + */ public function getCollectors() { return $this->collectors; } + /** + * Collects the data from the collectors + * + * @return array + */ + public function collect() + { + $this->data = array(); + foreach ($this->collectors as $name => $collector) { + $this->data[$name] = $collector->collect(); + } + return $this->data; + } + + /** + * Returns collected data + * + * Will collect the data if none have been collected yet + * + * @return array + */ + public function getData() + { + if ($this->data === null) { + $this->collect(); + } + return $this->data; + } + + /** + * Returns a JavascriptRenderer for this instance + * + * @return JavascriptRenderer + */ + public function getJavascriptRenderer() + { + if ($this->jsRenderer === null) { + $this->jsRenderer = new JavascriptRenderer($this); + } + return $this->jsRenderer; + } + + // -------------------------------------------- + // ArrayAccess implementation + public function offsetSet($key, $value) { throw new DebugBarException("DebugBar[] is read-only"); @@ -55,18 +146,4 @@ class DebugBar implements ArrayAccess { throw new DebugBarException("DebugBar[] is read-only"); } - - public function collect() - { - $this->data = array(); - foreach ($this->collectors as $name => $collector) { - $this->data[$name] = $collector->collect(); - } - return $this->data; - } - - public function getData() - { - return $this->data; - } } diff --git a/src/DebugBar/DebugBarException.php b/src/DebugBar/DebugBarException.php index 3f2a904..f887a08 100644 --- a/src/DebugBar/DebugBarException.php +++ b/src/DebugBar/DebugBarException.php @@ -1,4 +1,12 @@ debugBar = $debugBar; + $this->baseUrl = $baseUrl; + + if ($basePath === null) { + $basePath = __DIR__ . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, array('..', '..', '..', 'web')); + } + $this->basePath = $basePath; + + // bitwise operations cannot be done in class definition :( + $this->initialization = self::INITIALIZE_CONSTRUCTOR | self::INITIALIZE_CONTROLS; + } + + /** + * Sets the path which assets are relative to + * + * @param string $path + */ + public function setBasePath($path) + { + $this->basePath = $path; + return $this; + } + + /** + * Returns the path which assets are relative to + * + * @return string + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * Sets the base URL from which assets will be served + * + * @param string $url + */ + public function setBaseUrl($url) + { + $this->baseUrl = $url; + return $this; + } + + /** + * Returns the base URL from which assets will be served + * + * @return string + */ + public function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * Whether to include vendor assets + * + * @param boolean $enabled + */ + public function setIncludeVendors($enabled = true) + { + $this->includeVendors = $enabled; + return $this; + } + + /** + * Checks if vendors assets are included + * + * @return boolean + */ + public function areVendorsIncluded() + { + return $this->includeVendors; + } + + /** + * Sets the javascript class name + * + * @param string $className + */ + public function setJavascriptClass($className) + { + $this->javascriptClass = $className; + return $this; + } + + /** + * Returns the javascript class name + * + * @return string + */ + public function getJavascriptClass() + { + return $this->javascriptClass; + } + + /** + * Sets the variable name of the class instance + * + * @param string $name + */ + public function setVariableName($name) + { + $this->variableName = $name; + return $this; + } + + /** + * Returns the variable name of the class instance + * + * @return string + */ + public function getVariableName() + { + return $this->variableName; + } + + /** + * Sets what should be initialized + * + * - INITIALIZE_CONSTRUCTOR: only initializes the instance + * - INITIALIZE_CONTROLS: initializes the controls and data mapping + * - INITIALIZE_CONSTRUCTOR | INITIALIZE_CONTROLS: initialize everything (default) + * + * @param integer $init + */ + public function setInitialization($init) + { + $this->initialization = $init; + return $this; + } + + /** + * Returns what should be initialized + * + * @return integer + */ + public function getInitialization() + { + return $this->initialization; + } + + /** + * Adds a control to initialize + * + * Possible options: + * - icon: icon name + * - tooltip: string + * - widget: widget class name + * - map: a property name from the data to map the control to + * - default: a js string, default value of the data map + * + * "icon" or "widget" are at least needed + * + * @param string $name + * @param array $options + */ + public function addControl($name, $options) + { + if (!isset($options['icon']) || !isset($options['widget'])) { + throw new DebugBarException("Missing 'icon' or 'widget' option for control '$name'"); + } + $this->controls[$name] = $options; + return $this; + } + + /** + * Returns the list of asset files + * + * @return array + */ + protected function getAssetFiles() + { + $cssFiles = $this->cssFiles; + $jsFiles = $this->jsFiles; + + if ($this->includeVendors) { + $cssFiles = array_merge($this->cssVendors, $cssFiles); + $jsFiles = array_merge($this->jsVendors, $jsFiles); + } + + return array($cssFiles, $jsFiles); + } + + /** + * Returns a tuple where the both items are Assetic AssetCollection, + * the first one being css files and the second js files + * + * @return array or \Assetic\Asset\AssetCollection + */ + public function getAsseticCollection() + { + list($cssFiles, $jsFiles) = $this->getAssetFiles(); + return array( + $this->createAsseticCollection($cssFiles), + $this->createAsseticCollection($jsFiles) + ); + } + + /** + * Create an Assetic AssetCollection with the given files. + * Filenames will be converted to absolute path using + * the base path. + * + * @param array $files + * @return \Assetic\Asset\AssetCollection + */ + protected function createAsseticCollection($files) + { + $assets = array(); + foreach ($files as $file) { + $assets[] = new \Assetic\Asset\FileAsset($this->makeUriRelativeTo($file, $this->basePath)); + } + return new \Assetic\Asset\AssetCollection($assets); + } + + /** + * Renders the html to include needed assets + * + * Only useful if Assetic is not used + * + * @return string + */ + public function renderHead() + { + list($cssFiles, $jsFiles) = $this->getAssetFiles(); + $html = ''; + + foreach ($cssFiles as $file) { + $html .= sprintf('' . "\n", + $this->makeUriRelativeTo($file, $this->baseUrl)); + } + + foreach ($jsFiles as $file) { + $html .= sprintf('' . "\n", + $this->makeUriRelativeTo($file, $this->baseUrl)); + } + + return $html; + } + + /** + * Makes a URI relative to another + * + * @param string $uri + * @param string $root + * @return string + */ + protected function makeUriRelativeTo($uri, $root) + { + if (substr($uri, 0, 1) === '/' || preg_match('/^([a-z]+:\/\/|[a-zA-Z]:\/)/', $uri)) { + return $uri; + } + return rtrim($root, '/') . "/$uri"; + } + + /** + * Returns the code needed to display the debug bar + * + * AJAX request should not render the initialization code. + * + * @param boolean $initialize Whether to render the de bug bar initialization code + * @return string + */ + public function render($initialize = true) + { + $js = ''; + + if ($initialize) { + $js = $this->getJsInitializationCode(); + } + + $js .= sprintf("%s.addDataSet(%s);\n", $this->variableName, json_encode($this->debugBar->getData())); + return "\n"; + } + + /** + * Returns the js code needed to initialize the debug bar + * + * @return string + */ + protected function getJsInitializationCode() + { + $js = ''; + + if (($this->initialization & self::INITIALIZE_CONSTRUCTOR) === self::INITIALIZE_CONSTRUCTOR) { + $js = sprintf("var %s = new %s();\n", $this->variableName, $this->javascriptClass); + } + + if (($this->initialization & self::INITIALIZE_CONTROLS) === self::INITIALIZE_CONTROLS) { + $js .= $this->getJsControlsDefinitionCode($this->variableName); + } + + return $js; + } + + /** + * Returns the js code needed to initialized the controls and data mapping of the debug bar + * + * Controls can be defined by collectors themselves or using {@see addControl()} + * + * @param string $varname Debug bar's variable name + * @return string + */ + protected function getJsControlsDefinitionCode($varname) + { + $js = ''; + $dataMap = array(); + $controls = $this->controls; + + // finds controls provided by collectors + foreach ($this->debugBar->getCollectors() as $collector) { + if ($collector instanceof Renderable) { + $controls = array_merge($controls, $collector->getWidgets()); + } + } + + foreach ($controls as $name => $options) { + if (isset($options['widget'])) { + $js .= sprintf("%s.createTab(\"%s\", new %s());\n", + $varname, + $name, + $options['widget'] + ); + } else { + $js .= sprintf("%s.createIndicator(\"%s\", \"%s\", \"%s\");\n", + $varname, + $name, + isset($options['icon']) ? $options['icon'] : 'null', + isset($options['tooltip']) ? $options['tooltip'] : 'null' + ); + } + + if (isset($options['map']) && isset($options['default'])) { + $dataMap[$name] = array($options['map'], $options['default']); + } + } + + // creates the data mapping object + $mapJson = array(); + foreach ($dataMap as $name => $values) { + $mapJson[] = sprintf('"%s": ["%s", %s]', $name, $values[0], $values[1]); + } + $js .= sprintf("%s.setDataMap({\n%s\n});\n", $varname, implode(",\n", $mapJson)); + + // activate state restauration + $js .= sprintf("%s.restoreState();\n", $varname); + + return $js; + } +} diff --git a/src/DebugBar/Renderer/JavascriptRenderer.php b/src/DebugBar/Renderer/JavascriptRenderer.php deleted file mode 100644 index 07e3556..0000000 --- a/src/DebugBar/Renderer/JavascriptRenderer.php +++ /dev/null @@ -1,159 +0,0 @@ -debugBar = $debugBar; - $this->setBaseUrl($baseUrl); - } - - public function setBaseUrl($url) - { - $this->baseUrl = $url; - return $this; - } - - public function getBaseUrl() - { - return $this->baseUrl; - } - - public function setIncludeVendors($enabled = true) - { - $this->includeVendors = $enabled; - return $this; - } - - public function areVendorsIncluded() - { - return $this->includeVendors; - } - - public function setIncludeFiles($enabled = true) - { - $this->includeFiles = $enabled; - return $this; - } - - public function areFilesIncluded() - { - return $this->includeFiles; - } - - public function setToolbarFile($file) - { - $this->toolbarFile = $file; - return $this; - } - - public function getToolbarFile() - { - return $this->toolbarFile; - } - - public function setToolbarClass($className) - { - $this->toolbarClass = $className; - return $this; - } - - public function getToolbarClass() - { - return $this->toolbarClass; - } - - public function setToolbarVariableName($name) - { - $this->toolbarVariableName = $name; - } - - public function getToolbarVariableName() - { - return $this->toolbarVariableName; - } - - public function renderIncludes() - { - $cssFiles = array(); - $jsFiles = array(); - - if ($this->includeVendors) { - $cssFiles = array_merge($cssFiles, $this->cssVendors); - $jsFiles = array_merge($jsFiles, $this->jsVendors); - } - - if ($this->includeFiles) { - $cssFiles = array_merge($cssFiles, $this->cssFiles); - $jsFiles = array_merge($jsFiles, $this->jsFiles); - } - - $jsFiles[] = $this->toolbarFile; - - $html = ''; - foreach ($cssFiles as $file) { - $html .= sprintf('' . "\n", - $this->makeUrlRelativeTo($file, $this->baseUrl)); - } - foreach ($jsFiles as $file) { - $html .= sprintf('' . "\n", - $this->makeUrlRelativeTo($file, $this->baseUrl)); - } - return $html; - } - - public function renderToolbar() - { - return sprintf('' . "\n", - $this->toolbarVariableName, $this->toolbarClass, json_encode($this->debugBar->getData())); - } - - public function renderAjaxToolbar() - { - return sprintf('' . "\n", - $this->toolbarVariableName, json_encode($this->debugBar->getData())); - } - - public function renderAll() - { - return $this->renderIncludes() . $this->renderToolbar(); - } - - protected function makeUrlRelativeTo($url, $root) - { - if (substr($url, 0, 1) === '/' || preg_match('/^[a-z]+:\/\//', $url)) { - return $url; - } - return rtrim($root, '/') . "/$url"; - } -} diff --git a/src/DebugBar/StandardDebugBar.php b/src/DebugBar/StandardDebugBar.php index 4172a9a..d697b1e 100644 --- a/src/DebugBar/StandardDebugBar.php +++ b/src/DebugBar/StandardDebugBar.php @@ -1,21 +1,35 @@ addCollector(new PhpInfoCollector()); $this->addCollector(new MessagesCollector()); - $this->addCollector(new TimeCollector()); $this->addCollector(new RequestDataCollector()); + $this->addCollector(new TimeDataCollector()); $this->addCollector(new MemoryCollector()); } } diff --git a/tests/DebugBar/Tests/DebugBarTestCase.php b/tests/DebugBar/Tests/DebugBarTestCase.php new file mode 100644 index 0000000..491d6c2 --- /dev/null +++ b/tests/DebugBar/Tests/DebugBarTestCase.php @@ -0,0 +1,8 @@ +').text(title); + this.panel = $('
'); + this.replaceWidget(widget); + }; + + /** + * Sets the title of the tab + * + * @this {Tab} + * @param {String} text + */ + Tab.prototype.setTitle = function(text) { + this.tab.text(text); + }; + + /** + * Replaces the widget inside the panel + * + * @this {Tab} + * @param {Object} new_widget + */ + Tab.prototype.replaceWidget = function(new_widget) { + this.panel.empty().append(new_widget.element); + this.widget = new_widget; + }; + + // ------------------------------------------------------------------ + + /** + * Indicator + * + * An indicator is a text and an icon to display single value information + * right inside the always visible part of the debug bar + * + * @this {Indicator} + * @constructor + * @param {String} icon + * @param {String} tooltip + * @param {String} position "right" or "left", default is "right" + */ + var Indicator = function(icon, tooltip, position) { + if (!position) { + position = 'right' + } + + this.position = position; + this.element = $('').css('float', position); + this.label = $('').appendTo(this.element); + + if (icon) { + $('').insertBefore(this.label); + } + if (tooltip) { + this.element.append($('').text(tooltip)); + } + }; + + /** + * Sets the text of the indicator + * + * @this {Indicator} + * @param {String} text + */ + Indicator.prototype.setText = function(text) { + this.element.find('.text').text(text); + }; + + /** + * Sets the tooltip of the indicator + * + * @this {Indicator} + * @param {String} text + */ + Indicator.prototype.setTooltip = function(text) { + this.element.find('.tooltip').text(text); + }; + + // ------------------------------------------------------------------ + + + /** + * DebugBar + * + * @this {DebugBar} + * @constructor + */ var DebugBar = function() { this.controls = {}; this.dataMap = {}; + this.datasets = {}; this.initUI(); this.init(); - this.restoreState(); }; + /** + * Initialiazes the UI + * + * @this {DebugBar} + */ DebugBar.prototype.initUI = function() { + var self = this; this.element = $('
').appendTo('body'); this.header = $('
').appendTo(this.element); this.body = $('
').appendTo(this.element); this.resizeHandle = $('
').appendTo(this.body); + this.firstPanelName = null; + this.activePanelName = null; + // allow resizing by dragging handle this.body.drag('start', function(e, dd) { dd.height = $(this).height(); }).drag(function(e, dd) { @@ -44,21 +179,35 @@ PhpDebugBar.DebugBar = (function($) { localStorage.setItem('phpdebugbar-height', h); }, {handle: '.phpdebugbar-resize-handle'}); + // close button this.closeButton = $('').appendTo(this.header); - var self = this; this.closeButton.click(function() { self.hidePanels(); }); - this.stackSelectBox = $('').appendTo(this.header); + this.datasetSelectBox.change(function() { + self.dataChangeHandler(self.datasets[this.value]); }); }; + /** + * Custom initialiaze function for subsclasses + * + * @this {DebugBar} + */ DebugBar.prototype.init = function() {}; + /** + * Restores the state of the DebugBar using localStorage + * This is not called by default in the constructor and + * needs to be called by subclasses in their init() method + * + * @this {DebugBar} + */ DebugBar.prototype.restoreState = function() { + // bar height var height = localStorage.getItem('phpdebugbar-height'); if (height) { this.body.css('height', height); @@ -66,86 +215,171 @@ PhpDebugBar.DebugBar = (function($) { localStorage.setItem('phpdebugbar-height', this.body.height()); } + // bar visibility var visible = localStorage.getItem('phpdebugbar-visible'); if (visible && visible == '1') { this.showPanel(localStorage.getItem('phpdebugbar-panel')); } }; + /** + * Creates and adds a new tab + * + * @this {DebugBar} + * @param {String} name Internal name + * @param {Object} widget A widget object with an element property + * @param {String} title The text in the tab, if not specified, name will be used + * @return {Tab} + */ DebugBar.prototype.createTab = function(name, widget, title) { + var tab = new Tab(title || (name.replace(/[_\-]/g, ' ').charAt(0).toUpperCase() + name.slice(1)), widget); + return this.addTab(name, tab); + }; + + /** + * Adds a new tab + * + * @this {DebugBar} + * @param {String} name Internal name + * @param {Tab} tab Tab object + * @return {Tab} + */ + DebugBar.prototype.addTab = function(name, tab) { if (this.isControl(name)) { throw new Exception(name + ' already exists'); } - var tab = $('').text(title || (name.charAt(0).toUpperCase() + name.slice(1))), - panel = $('
'), - self = this; + var self = this; + tab.tab.appendTo(this.header).click(function() { self.showPanel(name); }); + tab.panel.appendTo(this.body); - tab.appendTo(this.header).click(function() { self.showPanel(name); }); - panel.appendTo(this.body).append(widget.element); - - this.controls[name] = {type: "tab", tab: tab, panel: panel, widget: widget}; - return widget; + this.controls[name] = tab; + if (this.firstPanelName == null) { + this.firstPanelName = name; + } + return tab; }; - DebugBar.prototype.setTabTitle = function(name, title) { + /** + * Returns a Tab object + * + * @this {DebugBar} + * @param {String} name + * @return {Tab} + */ + DebugBar.prototype.getTab = function(name) { if (this.isTab(name)) { - this.controls[name].tab.text(title); - } - }; - - DebugBar.prototype.getTabWidget = function(name) { - if (this.isTab(name)) { - return this.controls[name].widget; + return this.controls[name]; } }; + /** + * Creates and adds an indicator + * + * @this {DebugBar} + * @param {String} name Internal name + * @param {String} icon + * @param {String} tooltip + * @param {String} position "right" or "left", default is "right" + * @return {Indicator} + */ DebugBar.prototype.createIndicator = function(name, icon, tooltip, position) { + var indicator = new Indicator(icon, tooltip, position); + return this.addIndicator(name, indicator); + }; + + /** + * Adds an indicator + * + * @this {DebugBar} + * @param {String} name Internal name + * @param {Indicator} indicator Indicator object + * @return {Indicator} + */ + DebugBar.prototype.addIndicator = function(name, indicator) { if (this.isControl(name)) { throw new Exception(name + ' already exists'); } - if (!position) { - position = 'right' - } - var indicator = $('').css('float', position), - text = $('').appendTo(indicator); - - if (icon) { - $('').insertBefore(text); - } - if (tooltip) { - indicator.append($('').text(tooltip)); - } - - if (position == 'right') { - indicator.appendTo(this.header); + if (indicator.position == 'right') { + indicator.element.appendTo(this.header); } else { - indicator.insertBefore(this.header.children().first()) + indicator.element.insertBefore(this.header.children().first()) } - this.controls[name] = {type: "indicator", indicator: indicator}; - return text; + this.controls[name] = indicator; + return indicator; }; - DebugBar.prototype.setIndicatorText = function(name, text) { + /** + * Returns an Indicator object + * + * @this {DebugBar} + * @param {String} name + * @return {Indicator} + */ + DebugBar.prototype.getIndicator = function(name) { if (this.isIndicator(name)) { - this.controls[name].indicator.find('.text').text(text); + return this.controls[name]; } }; + /** + * Adds a control + * + * @param {String} name + * @param {Object} control + * @return {Object} + */ + DebugBar.prototype.addControl = function(name, control) { + if (control instanceof Tab) { + this.addTab(name, control); + } else if (control instanceof Indicator) { + this.addIndicator(name, control); + } else { + throw new Exception("Unknown type of control"); + } + return control; + }; + + /** + * Checks if there's a control under the specified name + * + * @this {DebugBar} + * @param {String} name + * @return {Boolean} + */ DebugBar.prototype.isControl = function(name) { return typeof(this.controls[name]) != 'undefined'; }; + /** + * Checks if a tab with the specified name exists + * + * @this {DebugBar} + * @param {String} name + * @return {Boolean} + */ DebugBar.prototype.isTab = function(name) { - return typeof(this.controls[name]) != 'undefined' && this.controls[name].type === 'tab'; + return this.isControl(name) && this.controls[name] instanceof Tab; }; + /** + * Checks if an indicator with the specified name exists + * + * @this {DebugBar} + * @param {String} name + * @return {Boolean} + */ DebugBar.prototype.isIndicator = function(name) { - return this.isControl(name) && this.controls[name].type === 'indicator'; + return this.isControl(name) && this.controls[name] instanceof Indicator; }; + /** + * Removes all tabs and indicators from the debug bar and hides it + * + * @this {DebugBar} + */ DebugBar.prototype.reset = function() { this.hidePanels(); this.body.find('.phpdebugbar-panel').remove(); @@ -153,17 +387,22 @@ PhpDebugBar.DebugBar = (function($) { this.controls = {}; }; + /** + * Open the debug bar and display a specified panel + * + * @this {DebugBar} + * @param {String} name If not specified, display the first panel + */ DebugBar.prototype.showPanel = function(name) { this.resizeHandle.show(); this.body.show(); this.closeButton.show(); if (!name) { - activePanel = this.body.find('.phpdebugbar-panel.active'); - if (activePanel.length > 0) { - name = activePanel.data('id'); + if (this.activePanelName) { + name = this.activePanelName; } else { - name = this.body.find('.phpdebugbar-panel').first().data('id'); + name = this.firstPanelName; } } @@ -173,15 +412,26 @@ PhpDebugBar.DebugBar = (function($) { if (this.isTab(name)) { this.controls[name].tab.addClass('active'); this.controls[name].panel.addClass('active').show(); + this.activePanelName = name; } localStorage.setItem('phpdebugbar-visible', '1'); localStorage.setItem('phpdebugbar-panel', name); }; + /** + * Shows the first panel + * + * @this {DebugBar} + */ DebugBar.prototype.showFirstPanel = function() { - this.showPanel(this.body.find('.phpdebugbar-panel').first().data('id')); + this.showPanel(this.firstPanelName); }; + /** + * Hide panels and "close" the debug bar + * + * @this {DebugBar} + */ DebugBar.prototype.hidePanels = function() { this.header.find('.phpdebugbar-tab.active').removeClass('active'); this.body.hide(); @@ -190,40 +440,121 @@ PhpDebugBar.DebugBar = (function($) { localStorage.setItem('phpdebugbar-visible', '0'); }; - DebugBar.prototype.setData = function(data) { - this.stacks = {}; - this.addDataStack(data); + /** + * Sets the data map used by dataChangeHandler to populate + * indicators and widgets + * + * A data map is an object where properties are control names. + * The value of each property should be an array where the first + * item is the name of a property from the data object (nested properties + * can be specified) and the second item the default value. + * + * Example: + * {"memory": ["memory.peak_usage_str", "0B"]} + * + * @this {DebugBar} + * @param {Object} map + */ + DebugBar.prototype.setDataMap = function(map) { + this.dataMap = map; }; - DebugBar.prototype.addDataStack = function(data, id) { - id = id || ("Request #" + (Object.keys(this.stacks).length + 1)); - this.stacks[id] = data; + /** + * Same as setDataMap() but appends to the existing map + * rather than replacing it + * + * @this {DebugBar} + * @param {Object} map + */ + DebugBar.prototype.addDataMap = function(map) { + $.extend(this.dataMap, map); + }; - this.stackSelectBox.append($('')); - if (Object.keys(this.stacks).length > 1) { - this.stackSelectBox.show(); + /** + * Resets datasets and add one set of data + * + * For this method to be usefull, you need to specify + * a dataMap using setDataMap() + * + * @this {DebugBar} + * @param {Object} data + * @return {String} Dataset's id + */ + DebugBar.prototype.setData = function(data) { + this.datasets = {}; + return this.addDataSet(data); + }; + + /** + * Adds a dataset + * + * If more than one dataset are added, the dataset selector + * will be displayed. + * + * For this method to be usefull, you need to specify + * a dataMap using setDataMap() + * + * @this {DebugBar} + * @param {Object} data + * @param {String} id The name of this set, optional + * @return {String} Dataset's id + */ + DebugBar.prototype.addDataSet = function(data, id) { + id = id || ("Request #" + (Object.keys(this.datasets).length + 1)); + this.datasets[id] = data; + + this.datasetSelectBox.append($('')); + if (Object.keys(this.datasets).length > 1) { + this.datasetSelectBox.show(); } - this.switchDataStack(id); + this.showDataSet(id); + return id; }; - DebugBar.prototype.switchDataStack = function(id) { - this.dataChangeHandler(this.stacks[id]); - this.stackSelectBox.val(id); + /** + * Returns the data from a dataset + * + * @this {DebugBar} + * @param {String} id + * @return {Object} + */ + DebugBar.prototype.getDataSet = function(id) { + return this.datasets[id]; }; + /** + * Switch the currently displayed dataset + * + * @this {DebugBar} + * @param {String} id + */ + DebugBar.prototype.showDataSet = function(id) { + this.dataChangeHandler(this.datasets[id]); + this.datasetSelectBox.val(id); + }; + + /** + * Called when the current dataset is modified. + * + * @this {DebugBar} + * @param {Object} data + */ DebugBar.prototype.dataChangeHandler = function(data) { var self = this; $.each(this.dataMap, function(key, def) { var d = getDictValue(data, def[0], def[1]); if (self.isIndicator(key)) { - self.setIndicatorText(key, d); + self.getIndicator(key).setText(d); } else { - self.getTabWidget(key).setData(d); + self.getTab(key).widget.setData(d); } }); }; + DebugBar.Tab = Tab; + DebugBar.Indicator = Indicator; + return DebugBar; })(jQuery); diff --git a/web/standard-debugbar.js b/web/standard-debugbar.js deleted file mode 100644 index e74ff04..0000000 --- a/web/standard-debugbar.js +++ /dev/null @@ -1,30 +0,0 @@ - -var StandardPhpDebugBar = (function() { - - var DebugBar = function(data) { - PhpDebugBar.DebugBar.apply(this); - this.setData(data); - }; - - DebugBar.prototype = new PhpDebugBar.DebugBar(); - - DebugBar.prototype.init = function() { - this.createIndicator('memory', 'cogs', 'Memory Usage'); - this.createIndicator('time', 'time', 'Request duration'); - - this.createTab('messages', new PhpDebugBar.Widgets.MessagesWidget()); - this.createTab('request', new PhpDebugBar.Widgets.VariableListWidget()); - this.createTab('timeline', new PhpDebugBar.Widgets.TimelineWidget()); - - this.dataMap = { - "memory": ["memory.peak_usage_str", "0B"], - "time": ["time.duration_str", "0ms"], - "messages": ["messages", []], - "request": ["request", {}], - "timeline": ["time", {}] - }; - }; - - return DebugBar; - -})(); diff --git a/web/widgets.js b/web/widgets.js index abcd6ae..523e6ae 100644 --- a/web/widgets.js +++ b/web/widgets.js @@ -1,15 +1,35 @@ if (typeof(PhpDebugBar) == 'undefined') { + // namespace var PhpDebugBar = {}; } +/** + * @namespace + */ PhpDebugBar.Widgets = (function($) { var widgets = {}; + /** + * Replaces spaces with   and line breaks with
+ * + * @param {String} text + * @return {String} + */ var htmlize = function(text) { return text.replace(/\n/g, '
').replace(/\s/g, " ") }; + widgets.htmlize = htmlize; + + /** + * Returns a string representation of value, using JSON.stringify + * if it's an object. + * + * @param {Object} value + * @param {Boolean} prettify Uses htmlize() if true + * @return {String} + */ var renderValue = function(value, prettify) { if (typeof(value) !== 'string') { if (prettify) { @@ -20,8 +40,21 @@ PhpDebugBar.Widgets = (function($) { return value; }; - // ------------------------------------------ + widgets.renderValue = renderValue; + + // ------------------------------------------------------------------ + // Generic widgets + // ------------------------------------------------------------------ + + /** + * Displays array element in a
    list + * + * @this {ListWidget} + * @constructor + * @param {Array} data + * @param {Function} itemRenderer Optional + */ var ListWidget = function(data, itemRenderer) { this.element = $('
      '); if (itemRenderer) { @@ -32,6 +65,12 @@ PhpDebugBar.Widgets = (function($) { } }; + /** + * Sets the data and updates the list + * + * @this {ListWidget} + * @param {Array} data + */ ListWidget.prototype.setData = function(data) { this.element.empty(); for (var i = 0; i < data.length; i++) { @@ -40,14 +79,154 @@ PhpDebugBar.Widgets = (function($) { } }; + /** + * Renders the content of a
    • element + * + * @this {ListWidget} + * @param {jQuery} li The
    • element as a jQuery Object + * @param {Object} value An item from the data array + */ ListWidget.prototype.itemRenderer = function(li, value) { li.html(renderValue(value)); }; widgets.ListWidget = ListWidget; - // ------------------------------------------ + // ------------------------------------------------------------------ + /** + * Displays object property/value paris in a
      list + * + * @this {KVListWidget} + * @constructor + * @param {Object} data + * @param {Function} itemRenderer Optional + */ + var KVListWidget = function(data, itemRenderer) { + this.element = $('
      '); + if (itemRenderer) { + this.itemRenderer = itemRenderer; + } + if (data) { + this.setData(data); + } + }; + + /** + * Sets the data and updates the list + * + * @this {KVListWidget} + * @param {Object} data + */ + KVListWidget.prototype.setData = function(data) { + var self = this; + this.element.empty(); + $.each(data, function(key, value) { + var dt = $('
      ').appendTo(self.element); + var dd = $('
      ').appendTo(self.element); + self.itemRenderer(dt, dd, key, value); + }); + }; + + /** + * Renders the content of the
      and
      elements + * + * @this {KVListWidget} + * @param {jQuery} dt The
      element as a jQuery Object + * @param {jQuery} dd The
      element as a jQuery Object + * @param {String} key Property name + * @param {Object} value Property value + */ + KVListWidget.prototype.itemRenderer = function(dt, dd, key, value) { + dt.text(key); + dd.html(htmlize(value)); + }; + + widgets.KVListWidget = KVListWidget; + + // ------------------------------------------------------------------ + + /** + * An extension of KVListWidget where the data represents a list + * of variables + * + * @this {VariableListWidget} + * @constructor + * @param {Object} data + */ + var VariableListWidget = function(data) { + KVListWidget.apply(this, [data]); + this.element.addClass('phpdebugbar-widgets-varlist'); + }; + + VariableListWidget.prototype = new KVListWidget(); + VariableListWidget.constructor = VariableListWidget; + + VariableListWidget.prototype.itemRenderer = function(dt, dd, key, value) { + dt.text(key); + + var v = value; + if (v.length > 100) { + v = v.substr(0, 100) + "..."; + } + dd.text(v).click(function() { + if (dd.hasClass('pretty')) { + dd.text(v).removeClass('pretty'); + } else { + dd.html(htmlize(value)).addClass('pretty'); + } + }); + }; + + widgets.VariableListWidget = VariableListWidget; + + // ------------------------------------------------------------------ + + /** + * Iframe widget + * + * @this {IFrameWidget} + * @constructor + * @param {String} url + */ + var IFrameWidget = function(url) { + this.element = $('