475 lines
12 KiB
PHP
475 lines
12 KiB
PHP
<?php
|
|
/*
|
|
* This file is part of the DebugBar package.
|
|
*
|
|
* (c) 2013 Maxime Bouroumeau-Fuseau
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace DebugBar;
|
|
|
|
use ArrayAccess;
|
|
use DebugBar\DataCollector\DataCollectorInterface;
|
|
use DebugBar\Storage\StorageInterface;
|
|
|
|
/**
|
|
* Main DebugBar object
|
|
*
|
|
* Manages data collectors. DebugBar provides an array-like access
|
|
* to collectors by name.
|
|
*
|
|
* <code>
|
|
* $debugbar = new DebugBar();
|
|
* $debugbar->addCollector(new DataCollector\MessagesCollector());
|
|
* $debugbar['messages']->addMessage("foobar");
|
|
* </code>
|
|
*/
|
|
class DebugBar implements ArrayAccess
|
|
{
|
|
public static $useOpenHandlerWhenSendingDataHeaders = false;
|
|
|
|
protected $collectors = array();
|
|
|
|
protected $data;
|
|
|
|
protected $jsRenderer;
|
|
|
|
protected $requestIdGenerator;
|
|
|
|
protected $requestId;
|
|
|
|
protected $storage;
|
|
|
|
protected $httpDriver;
|
|
|
|
protected $stackSessionNamespace = 'PHPDEBUGBAR_STACK_DATA';
|
|
|
|
protected $stackAlwaysUseSessionStorage = false;
|
|
|
|
/**
|
|
* Adds a data collector
|
|
*
|
|
* @param DataCollectorInterface $collector
|
|
*
|
|
* @throws DebugBarException
|
|
* @return $this
|
|
*/
|
|
public function addCollector(DataCollectorInterface $collector)
|
|
{
|
|
if ($collector->getName() === '__meta') {
|
|
throw new DebugBarException("'__meta' is a reserved name and cannot be used as a collector name");
|
|
}
|
|
if (isset($this->collectors[$collector->getName()])) {
|
|
throw new DebugBarException("'{$collector->getName()}' 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 DataCollectorInterface
|
|
* @throws DebugBarException
|
|
*/
|
|
public function getCollector($name)
|
|
{
|
|
if (!isset($this->collectors[$name])) {
|
|
throw new DebugBarException("'$name' is not a registered collector");
|
|
}
|
|
return $this->collectors[$name];
|
|
}
|
|
|
|
/**
|
|
* Returns an array of all data collectors
|
|
*
|
|
* @return array[DataCollectorInterface]
|
|
*/
|
|
public function getCollectors()
|
|
{
|
|
return $this->collectors;
|
|
}
|
|
|
|
/**
|
|
* Sets the request id generator
|
|
*
|
|
* @param RequestIdGeneratorInterface $generator
|
|
* @return $this
|
|
*/
|
|
public function setRequestIdGenerator(RequestIdGeneratorInterface $generator)
|
|
{
|
|
$this->requestIdGenerator = $generator;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return RequestIdGeneratorInterface
|
|
*/
|
|
public function getRequestIdGenerator()
|
|
{
|
|
if ($this->requestIdGenerator === null) {
|
|
$this->requestIdGenerator = new RequestIdGenerator();
|
|
}
|
|
return $this->requestIdGenerator;
|
|
}
|
|
|
|
/**
|
|
* Returns the id of the current request
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCurrentRequestId()
|
|
{
|
|
if ($this->requestId === null) {
|
|
$this->requestId = $this->getRequestIdGenerator()->generate();
|
|
}
|
|
return $this->requestId;
|
|
}
|
|
|
|
/**
|
|
* Sets the storage backend to use to store the collected data
|
|
*
|
|
* @param StorageInterface $storage
|
|
* @return $this
|
|
*/
|
|
public function setStorage(StorageInterface $storage = null)
|
|
{
|
|
$this->storage = $storage;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return StorageInterface
|
|
*/
|
|
public function getStorage()
|
|
{
|
|
return $this->storage;
|
|
}
|
|
|
|
/**
|
|
* Checks if the data will be persisted
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isDataPersisted()
|
|
{
|
|
return $this->storage !== null;
|
|
}
|
|
|
|
/**
|
|
* Sets the HTTP driver
|
|
*
|
|
* @param HttpDriverInterface $driver
|
|
* @return $this
|
|
*/
|
|
public function setHttpDriver(HttpDriverInterface $driver)
|
|
{
|
|
$this->httpDriver = $driver;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the HTTP driver
|
|
*
|
|
* If no http driver where defined, a PhpHttpDriver is automatically created
|
|
*
|
|
* @return HttpDriverInterface
|
|
*/
|
|
public function getHttpDriver()
|
|
{
|
|
if ($this->httpDriver === null) {
|
|
$this->httpDriver = new PhpHttpDriver();
|
|
}
|
|
return $this->httpDriver;
|
|
}
|
|
|
|
/**
|
|
* Collects the data from the collectors
|
|
*
|
|
* @return array
|
|
*/
|
|
public function collect()
|
|
{
|
|
$this->data = array(
|
|
'__meta' => array(
|
|
'id' => $this->getCurrentRequestId(),
|
|
'datetime' => date('Y-m-d H:i:s'),
|
|
'utime' => microtime(true),
|
|
'method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null,
|
|
'uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null,
|
|
'ip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null
|
|
)
|
|
);
|
|
|
|
foreach ($this->collectors as $name => $collector) {
|
|
$this->data[$name] = $collector->collect();
|
|
}
|
|
|
|
// Remove all invalid (non UTF-8) characters
|
|
array_walk_recursive($this->data, function (&$item) {
|
|
if (is_string($item) && !mb_check_encoding($item, 'UTF-8')) {
|
|
$item = mb_convert_encoding($item, 'UTF-8', 'UTF-8');
|
|
}
|
|
});
|
|
|
|
if ($this->storage !== null) {
|
|
$this->storage->save($this->getCurrentRequestId(), $this->data);
|
|
}
|
|
|
|
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 an array of HTTP headers containing the data
|
|
*
|
|
* @param string $headerName
|
|
* @param integer $maxHeaderLength
|
|
* @return array
|
|
*/
|
|
public function getDataAsHeaders($headerName = 'phpdebugbar', $maxHeaderLength = 4096, $maxTotalHeaderLength = 250000)
|
|
{
|
|
$data = rawurlencode(json_encode(array(
|
|
'id' => $this->getCurrentRequestId(),
|
|
'data' => $this->getData()
|
|
)));
|
|
|
|
if (strlen($data) > $maxTotalHeaderLength) {
|
|
$data = rawurlencode(json_encode(array(
|
|
'error' => 'Maximum header size exceeded'
|
|
)));
|
|
}
|
|
|
|
$chunks = array();
|
|
|
|
while (strlen($data) > $maxHeaderLength) {
|
|
$chunks[] = substr($data, 0, $maxHeaderLength);
|
|
$data = substr($data, $maxHeaderLength);
|
|
}
|
|
$chunks[] = $data;
|
|
|
|
$headers = array();
|
|
for ($i = 0, $c = count($chunks); $i < $c; $i++) {
|
|
$name = $headerName . ($i > 0 ? "-$i" : '');
|
|
$headers[$name] = $chunks[$i];
|
|
}
|
|
|
|
return $headers;
|
|
}
|
|
|
|
/**
|
|
* Sends the data through the HTTP headers
|
|
*
|
|
* @param bool $useOpenHandler
|
|
* @param string $headerName
|
|
* @param integer $maxHeaderLength
|
|
* @return $this
|
|
*/
|
|
public function sendDataInHeaders($useOpenHandler = null, $headerName = 'phpdebugbar', $maxHeaderLength = 4096)
|
|
{
|
|
if ($useOpenHandler === null) {
|
|
$useOpenHandler = self::$useOpenHandlerWhenSendingDataHeaders;
|
|
}
|
|
if ($useOpenHandler && $this->storage !== null) {
|
|
$this->getData();
|
|
$headerName .= '-id';
|
|
$headers = array($headerName => $this->getCurrentRequestId());
|
|
} else {
|
|
$headers = $this->getDataAsHeaders($headerName, $maxHeaderLength);
|
|
}
|
|
$this->getHttpDriver()->setHeaders($headers);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Stacks the data in the session for later rendering
|
|
*/
|
|
public function stackData()
|
|
{
|
|
$http = $this->initStackSession();
|
|
|
|
$data = null;
|
|
if (!$this->isDataPersisted() || $this->stackAlwaysUseSessionStorage) {
|
|
$data = $this->getData();
|
|
} elseif ($this->data === null) {
|
|
$this->collect();
|
|
}
|
|
|
|
$stack = $http->getSessionValue($this->stackSessionNamespace);
|
|
$stack[$this->getCurrentRequestId()] = $data;
|
|
$http->setSessionValue($this->stackSessionNamespace, $stack);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Checks if there is stacked data in the session
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function hasStackedData()
|
|
{
|
|
try {
|
|
$http = $this->initStackSession();
|
|
} catch (DebugBarException $e) {
|
|
return false;
|
|
}
|
|
return count($http->getSessionValue($this->stackSessionNamespace)) > 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the data stacked in the session
|
|
*
|
|
* @param boolean $delete Whether to delete the data in the session
|
|
* @return array
|
|
*/
|
|
public function getStackedData($delete = true)
|
|
{
|
|
$http = $this->initStackSession();
|
|
$stackedData = $http->getSessionValue($this->stackSessionNamespace);
|
|
if ($delete) {
|
|
$http->deleteSessionValue($this->stackSessionNamespace);
|
|
}
|
|
|
|
$datasets = array();
|
|
if ($this->isDataPersisted() && !$this->stackAlwaysUseSessionStorage) {
|
|
foreach ($stackedData as $id => $data) {
|
|
$datasets[$id] = $this->getStorage()->get($id);
|
|
}
|
|
} else {
|
|
$datasets = $stackedData;
|
|
}
|
|
|
|
return $datasets;
|
|
}
|
|
|
|
/**
|
|
* Sets the key to use in the $_SESSION array
|
|
*
|
|
* @param string $ns
|
|
* @return $this
|
|
*/
|
|
public function setStackDataSessionNamespace($ns)
|
|
{
|
|
$this->stackSessionNamespace = $ns;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the key used in the $_SESSION array
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getStackDataSessionNamespace()
|
|
{
|
|
return $this->stackSessionNamespace;
|
|
}
|
|
|
|
/**
|
|
* Sets whether to only use the session to store stacked data even
|
|
* if a storage is enabled
|
|
*
|
|
* @param boolean $enabled
|
|
* @return $this
|
|
*/
|
|
public function setStackAlwaysUseSessionStorage($enabled = true)
|
|
{
|
|
$this->stackAlwaysUseSessionStorage = $enabled;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Checks if the session is always used to store stacked data
|
|
* even if a storage is enabled
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isStackAlwaysUseSessionStorage()
|
|
{
|
|
return $this->stackAlwaysUseSessionStorage;
|
|
}
|
|
|
|
/**
|
|
* Initializes the session for stacked data
|
|
* @return HttpDriverInterface
|
|
* @throws DebugBarException
|
|
*/
|
|
protected function initStackSession()
|
|
{
|
|
$http = $this->getHttpDriver();
|
|
if (!$http->isSessionStarted()) {
|
|
throw new DebugBarException("Session must be started before using stack data in the debug bar");
|
|
}
|
|
|
|
if (!$http->hasSessionValue($this->stackSessionNamespace)) {
|
|
$http->setSessionValue($this->stackSessionNamespace, array());
|
|
}
|
|
|
|
return $http;
|
|
}
|
|
|
|
/**
|
|
* Returns a JavascriptRenderer for this instance
|
|
* @param string $baseUrl
|
|
* @param string $basePath
|
|
* @return JavascriptRenderer
|
|
*/
|
|
public function getJavascriptRenderer($baseUrl = null, $basePath = null)
|
|
{
|
|
if ($this->jsRenderer === null) {
|
|
$this->jsRenderer = new JavascriptRenderer($this, $baseUrl, $basePath);
|
|
}
|
|
return $this->jsRenderer;
|
|
}
|
|
|
|
// --------------------------------------------
|
|
// ArrayAccess implementation
|
|
|
|
public function offsetSet($key, $value)
|
|
{
|
|
throw new DebugBarException("DebugBar[] is read-only");
|
|
}
|
|
|
|
public function offsetGet($key)
|
|
{
|
|
return $this->getCollector($key);
|
|
}
|
|
|
|
public function offsetExists($key)
|
|
{
|
|
return $this->hasCollector($key);
|
|
}
|
|
|
|
public function offsetUnset($key)
|
|
{
|
|
throw new DebugBarException("DebugBar[] is read-only");
|
|
}
|
|
}
|