This commit is contained in:
2022-12-24 22:10:40 +08:00
parent 84fc1030a7
commit 2a00928da5
4898 changed files with 429855 additions and 77 deletions
+6
View File
@@ -0,0 +1,6 @@
build
vendor
.idea
.vscode
.phpunit*
composer.lock
+23
View File
@@ -0,0 +1,23 @@
# webman-tech/debugbar
[PHP Debugbar](http://phpdebugbar.com/) for webman
## 安装
```bash
composer require webman-tech/debugbar --dev
```
## 使用
安装默认开启(仅 debug 模式下启用),直接访问路由 `{Host}/_debugbar/sample` 即可看到信息
如果是 api 应用,其他接口调用后,可以在 debugbar 的历史记录下看到接口相关的请求
## 效果图
![截图](https://www.workerman.net/upload/img/20220717/1762d40f967190.png)
![截图](https://www.workerman.net/upload/img/20220717/1762d40f897d23.png)
![截图](https://www.workerman.net/upload/img/20220717/1762d40f5fce87.png)
+22
View File
@@ -0,0 +1,22 @@
{
"name": "webman-tech/debugbar",
"type": "library",
"license": "MIT",
"description": "Webman plugin webman-tech/debugbar",
"require": {
"php": ">=7.2",
"maximebf/debugbar": "^1.18"
},
"autoload": {
"psr-4": {
"WebmanTech\\Debugbar\\": "src"
}
},
"require-dev": {
"illuminate/database": "^8.83",
"illuminate/events": "^8.83",
"illuminate/redis": "^8.83",
"symfony/finder": "^5.4",
"workerman/webman-framework": "^1.4"
}
}
@@ -0,0 +1,39 @@
<?php
namespace WebmanTech\Debugbar\Bootstrap;
use Illuminate\Database\Capsule\Manager as Capsule;
use WebmanTech\Debugbar\DataCollector\LaravelQueryCollector;
use WebmanTech\Debugbar\DebugBar;
use support\Db;
use Webman\Bootstrap;
class LaravelQuery implements Bootstrap
{
/**
* @inheritDoc
*/
public static function start($worker)
{
if (!class_exists(Capsule::class)) {
return;
}
$connections = array_keys(config('database.connections'));
if (!$connections) {
return;
}
$collectorName = (new LaravelQueryCollector())->getName();
$debugBar = DebugBar::instance();
$debugBar->boot();
if (!$debugBar->hasCollector($collectorName)) {
return;
}
/** @var LaravelQueryCollector $collector */
$collector = $debugBar->getCollector($collectorName);
foreach ($connections as $connection) {
$collector->addListener(Db::connection($connection));
}
}
}
@@ -0,0 +1,44 @@
<?php
namespace WebmanTech\Debugbar\Bootstrap;
use Illuminate\Redis\RedisManager;
use WebmanTech\Debugbar\DataCollector\LaravelRedisCollector;
use WebmanTech\Debugbar\DebugBar;
use support\Redis;
use Webman\Bootstrap;
class LaravelRedisExec implements Bootstrap
{
/**
* @inheritDoc
*/
public static function start($worker)
{
if (!class_exists(RedisManager::class)) {
return;
}
$connections = array_keys(config('redis'));
if (!$connections) {
return;
}
$collectorName = (new LaravelRedisCollector())->getName();
$debugBar = DebugBar::instance();
$debugBar->boot();
if (!$debugBar->hasCollector($collectorName)) {
return;
}
/** @var LaravelRedisCollector $collector */
$collector = $debugBar->getCollector($collectorName);
foreach ($connections as $connection) {
try {
$connection = Redis::connection($connection);
$collector->addRedisListener($connection);
} catch (\Throwable $e) {
// 忽略错误的 redis connection
}
}
}
}
@@ -0,0 +1,231 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\TimeDataCollector as DebugBarTimeDataCollector;
use Illuminate\Database\Connection;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\Events\TransactionBeginning;
use Illuminate\Database\Events\TransactionCommitted;
use Illuminate\Database\Events\TransactionRolledBack;
use WebmanTech\Debugbar\DebugBar;
use WebmanTech\Debugbar\Helper\ArrayHelper;
use WebmanTech\Debugbar\Laravel\DataCollector\QueryCollector;
use WebmanTech\Debugbar\Laravel\DataFormatter\QueryFormatter;
class LaravelQueryCollector extends QueryCollector
{
/**
* @var array
*/
protected $config = [
'with_params' => true, // 将参数绑定上
'backtrace' => true, // 显示 sql 来源
'backtrace_exclude_paths' => [], // 排除 sql 来源
'timeline' => true, // 将 sql 显示到 timeline 上
'duration_background' => true, // 相对于执行所需的时间,在每个查询上显示阴影背景
'explain' => [
'enabled' => false,
'types' => ['SELECT'], // 废弃的设置,目前只支持 SELECT
],
'hints' => false, // 显示常见错误提示
'show_copy' => true, // 显示复制
'slow_threshold' => false, // 仅显示慢sql,设置毫秒时间来启用
];
public function __construct(array $config = [], DebugBarTimeDataCollector $timeCollector = null)
{
$this->config = ArrayHelper::merge($this->config, $config);
if (!$this->config['timeline']) {
$timeCollector = null;
}
parent::__construct($timeCollector);
$this->setConfig();
}
/**
* @inheritDoc
*/
public function getAssets()
{
$assets = parent::getAssets();
$assets['js'] = 'laravel/sqlqueries/widget.js';
return $assets;
}
/**
* @inheritDoc
*/
public function getName()
{
return 'laravelDB';
}
/**
* @inheritDoc
*/
function getWidgets()
{
$name = $this->getName();
return [
$name => [
"icon" => "database",
"widget" => "PhpDebugBar.Widgets.LaravelSQLQueriesWidget",
"map" => $name,
"default" => "[]"
],
"{$name}:badge" => [
"map" => "{$name}.nb_statements",
"default" => 0,
],
];
}
protected function setConfig()
{
$this->mergeBacktraceExcludePaths([
'/vendor/webman-tech/debugbar',
'/vendor/illuminate/support',
'/vendor/illuminate/database',
'/vendor/illuminate/events',
'/vendor/jenssegers/mongodb', // jenssegers/mongodb
]);
$this->setDataFormatter(new QueryFormatter());
if ($this->config['with_params']) {
$this->setRenderSqlWithParams(true);
}
if ($this->config['backtrace']) {
$middleware = [];
$this->setFindSource(true, $middleware);
}
if ($this->config['backtrace_exclude_paths']) {
$excludePaths = $this->config['backtrace_exclude_paths'];
$this->mergeBacktraceExcludePaths($excludePaths);
}
$this->setDurationBackground($this->config['duration_background']);
if ($this->config['explain']['enabled']) {
$types = $this->config['explain']['types'];
$this->setExplainSource(true, $types);
}
if ($this->config['hints']) {
$this->setShowHints(true);
}
if ($this->config['show_copy']) {
$this->setShowCopyButton(true);
}
}
public function addQuery($query, $bindings, $time, $connection)
{
parent::addQuery($query, $bindings, $time, $connection);
// 给 connection 添加 connectionName
$lastIndex = count($this->queries) - 1;
$lastQuery = $this->queries[$lastIndex];
$lastQuery['connection'] = $connection->getName() . '__' . $lastQuery['connection']; // 目前只能用 __ 分隔,其他字符比如冒号、斜杠等会导致js错误
$this->queries[$lastIndex] = $lastQuery;
}
public function addListener(Connection $db)
{
$db->listen(function (QueryExecuted $event) use ($db) {
$connection = $event->connection;
if (!$this->isEventConnectionEqual($db, $connection)) {
return;
}
$bindings = $event->bindings;
$time = $event->time;
$query = $event->sql;
$threshold = $this->config['slow_threshold'];
if (!$threshold || $time > $threshold) {
if ($collector = $this->getRequestThisCollector()) {
$collector->addQuery($query, $bindings, $time, $connection);
}
}
});
$db->getEventDispatcher()->listen(TransactionBeginning::class, function (TransactionBeginning $transaction) use ($db) {
if (!$this->isEventConnectionEqual($db, $transaction->connection)) {
return;
}
if ($collector = $this->getRequestThisCollector()) {
$collector->collectTransactionEvent('Begin Transaction', $transaction->connection);
}
});
$db->getEventDispatcher()->listen(TransactionCommitted::class, function (TransactionCommitted $transaction) use ($db) {
if (!$this->isEventConnectionEqual($db, $transaction->connection)) {
return;
}
if ($collector = $this->getRequestThisCollector()) {
$collector->collectTransactionEvent('Commit Transaction', $transaction->connection);
}
});
$db->getEventDispatcher()->listen(TransactionRolledBack::class, function (TransactionRolledBack $transaction) use ($db) {
if (!$this->isEventConnectionEqual($db, $transaction->connection)) {
return;
}
if ($collector = $this->getRequestThisCollector()) {
$collector->collectTransactionEvent('Rollback Transaction', $transaction->connection);
}
});
$db->getEventDispatcher()->listen('connection.*.beganTransaction', function ($event, $params) {
if ($collector = $this->getRequestThisCollector()) {
$collector->collectTransactionEvent('Begin Transaction', $params[0]);
}
});
$db->getEventDispatcher()->listen('connection.*.committed', function ($event, $params) {
if ($collector = $this->getRequestThisCollector()) {
$collector->collectTransactionEvent('Commit Transaction', $params[0]);
}
});
$db->getEventDispatcher()->listen('connection.*.rollingBack', function ($event, $params) {
if ($collector = $this->getRequestThisCollector()) {
$collector->collectTransactionEvent('Rollback Transaction', $params[0]);
}
});
}
/**
* 获取 request 下每次新的当前 collector 对象
* @return $this|null
*/
protected function getRequestThisCollector(): ?self
{
$debugBar = DebugBar::instance();
if (!$debugBar->hasCollector($this->getName())) {
return null;
}
/** @var static $collector */
$collector = $debugBar->getCollector($this->getName());
return $collector;
}
/**
* 由于 event 可能是 Container 下的,则事件会重复绑定
* 因此粗腰判断事件的 connection 是否是当前 DB 的
* @param Connection $connection
* @param Connection $eventConnection
* @return bool
*/
protected function isEventConnectionEqual(Connection $connection, Connection $eventConnection): bool
{
return $connection->getName() === $eventConnection->getName();
}
}
@@ -0,0 +1,135 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use Illuminate\Redis\Connections\Connection;
use Illuminate\Redis\Events\CommandExecuted;
use Illuminate\Support\Str;
use WebmanTech\Debugbar\Laravel\DataFormatter\QueryFormatter;
class LaravelRedisCollector extends LaravelQueryCollector
{
/**
* @var array
*/
protected $config = [
'with_params' => true, // 将参数绑定上
'backtrace' => true, // 显示 sql 来源
'backtrace_exclude_paths' => [], // 排除 sql 来源
'timeline' => true, // 将 sql 显示到 timeline 上
'duration_background' => true, // 相对于执行所需的时间,在每个查询上显示阴影背景
'show_copy' => true, // 显示复制
'slow_threshold' => false, // 仅显示慢执行,设置毫秒时间来启用
];
/**
* @inheritDoc
*/
function getName()
{
return 'laravelRedis';
}
protected function setConfig()
{
$this->mergeBacktraceExcludePaths([
'/webman-debugbar/',
'/vendor/webman-tech/debugbar',
'/vendor/illuminate/support',
'/vendor/illuminate/redis',
'/vendor/illuminate/events',
'/support/Redis.php',
]);
$this->setDataFormatter(new QueryFormatter());
if ($this->config['with_params']) {
$this->setRenderSqlWithParams(true);
}
if ($this->config['backtrace']) {
$middleware = [];
$this->setFindSource(true, $middleware);
}
if ($this->config['backtrace_exclude_paths']) {
$excludePaths = $this->config['backtrace_exclude_paths'];
$this->mergeBacktraceExcludePaths($excludePaths);
}
$this->setDurationBackground($this->config['duration_background']);
$this->setExplainSource(false, null);
$this->setShowHints(false);
if ($this->config['show_copy']) {
$this->setShowCopyButton(true);
}
}
public function addRedisListener(Connection $connection): void
{
$connection->listen(function (CommandExecuted $event) {
$command = $event->command;
$connection = $event->connection;
$parameters = $event->parameters;
$time = $event->time;
if ($collector = $this->getRequestThisCollector()) {
$collector->addExec($command, $parameters, $time, $connection);
}
});
}
/**
* @param string $command
* @param array $parameters
* @param float $time
* @param Connection $connection
* @see addQuery
*/
protected function addExec(string $command, array $parameters, float $time, Connection $connection)
{
$explainResults = [];
$time = $time / 1000;
$endTime = microtime(true);
$startTime = $endTime - $time;
if (!empty($parameters) && $this->renderSqlWithParams) {
foreach ($parameters as &$item) {
if (is_array($item)) {
$item = implode('\', \'', $item);
}
}
unset($item);
$command .= '(' . implode(', ', $parameters) . ')';
}
$source = [];
if ($this->findSource) {
try {
$source = array_slice($this->findSource(), 0, 5);
} catch (\Exception $e) {
}
}
$this->queries[] = [
'query' => $command,
'type' => 'query',
'bindings' => $this->getDataFormatter()->escapeBindings($parameters),
'time' => $time,
'source' => $source,
'explain' => $explainResults,
'connection' => $connection->getName(),
'driver' => get_class($connection->client()),
'hints' => null,
'show_copy' => $this->showCopyButton,
];
if ($this->timeCollector !== null) {
$this->timeCollector->addMeasure(Str::limit($command, 100), $startTime, $endTime);
}
}
}
@@ -0,0 +1,23 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\MemoryCollector as OriginMemoryCollector;
class MemoryCollector extends OriginMemoryCollector
{
/**
* @inheritdoc
*/
public function getWidgets()
{
return array(
"memory" => array(
"icon" => "cogs",
"tooltip" => "内存使用: 仅表示本次启动后的最大内存",
"map" => "memory.peak_usage_str",
"default" => "'0B'"
)
);
}
}
@@ -0,0 +1,18 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\PhpInfoCollector as OriginPhpInfoCollector;
class PhpInfoCollector extends OriginPhpInfoCollector
{
/**
* @inheritDoc
*/
public function getWidgets()
{
$widgets = parent::getWidgets();
$widgets['php_version']['tooltip'] = 'PHP Version';
return $widgets;
}
}
@@ -0,0 +1,60 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\RequestDataCollector as OriginRequestDataCollector;
use Webman\Http\Request;
use Webman\Http\Response;
class RequestDataCollector extends OriginRequestDataCollector
{
/**
* @var Request
*/
protected $request;
/**
* @var Response
*/
protected $response;
public function __construct(Request $request, Response $response)
{
$this->request = $request;
$this->response = $response;
}
/**
* @inheritdoc
*/
public function collect()
{
$request = $this->request;
$response = $this->response;
$data = [
'app' => $request->app,
'controller' => $request->controller,
'action' => $request->action,
'method' => $request->method(),
'host' => $request->host(),
'uri' => $request->uri(),
'path' => $request->path(),
'get' => $request->get(),
'post' => $request->post(),
'header' => $request->header(),
'cookie' => $request->cookie(),
'session' => $request->session()->all(),
'server' => $_SERVER,
'response_status_code' => $response->getStatusCode(),
'response_reason_phrase' => $response->getReasonPhrase(),
'response_headers' => $response->getHeaders(),
];
return array_map(function ($item) {
if ($this->isHtmlVarDumperUsed()) {
$item = $this->getVarDumper()->renderVar($item);
} else {
$item = $this->getDataFormatter()->formatVar($item);
}
return $item;
}, $data);
}
}
@@ -0,0 +1,66 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\DataCollector;
use DebugBar\DataCollector\DataCollectorInterface;
use DebugBar\DataCollector\Renderable;
class RouteCollector extends DataCollector implements DataCollectorInterface, Renderable
{
/**
* @inheritDoc
*/
function collect()
{
$request = request();
$data = [];
if ($route = $request->route) {
$data = [
'uri' => $request->method() . ' ' . $request->uri(),
'controller' => $request->controller,
'action' => $request->action,
'name' => $route->getName(),
'path' => $route->getPath(),
'methods' => $route->getMethods(),
'middleware' => $route->getMiddleware(),
];
}
foreach ($data as &$value) {
if (!is_string($value)) {
$value = $this->getDataFormatter()->formatVar($value);
}
}
unset($value);
return $data;
}
/**
* @inheritDoc
*/
function getName()
{
return 'route';
}
/**
* @inheritDoc
*/
function getWidgets()
{
return [
"route" => [
"icon" => "share",
"widget" => "PhpDebugBar.Widgets.VariableListWidget",
"map" => "route",
"default" => "{}"
],
"currentRoute" => [
"icon" => "share",
"tooltip" => "Route",
"map" => "route.uri",
"default" => ""
],
];
}
}
@@ -0,0 +1,56 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\DataCollector;
use DebugBar\DataCollector\DataCollectorInterface;
use DebugBar\DataCollector\Renderable;
use Webman\Http\Request;
class SessionCollector extends DataCollector implements DataCollectorInterface, Renderable
{
/**
* @var Request
*/
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* @inheritDoc
*/
function collect()
{
$data = [];
foreach ($this->request->session()->all() as $key => $value) {
$data[$key] = is_string($value) ? $value : $this->getDataFormatter()->formatVar($value);
}
return $data;
}
/**
* @inheritDoc
*/
function getName()
{
return 'session';
}
/**
* @inheritDoc
*/
function getWidgets()
{
return [
"session" => [
"icon" => "archive",
"widget" => "PhpDebugBar.Widgets.VariableListWidget",
"map" => "session",
"default" => "{}"
]
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\TimeDataCollector as OriginTimeDataCollector;
class TimeDataCollector extends OriginTimeDataCollector
{
public const REQUEST_KEY = '_debugbar_request_time';
public function __construct($requestStartTime = null)
{
if ($requestStartTime === null) {
if ($request = request()) {
if (!$request->{static::REQUEST_KEY}) {
$request->{static::REQUEST_KEY} = microtime(true);
}
$requestStartTime = $request->{static::REQUEST_KEY};
}
}
parent::__construct($requestStartTime);
}
}
@@ -0,0 +1,49 @@
<?php
namespace WebmanTech\Debugbar\DataCollector;
use DebugBar\DataCollector\DataCollector;
use DebugBar\DataCollector\Renderable;
class WebmanCollector extends DataCollector implements Renderable
{
/**
* @inheritDoc
*/
public function collect()
{
return [
"version" => defined('WEBMAN_VERSION') ? WEBMAN_VERSION : 'Undefined',
"environment" => config('app.debug', false) ? 'debug' : 'prod',
];
}
/**
* @inheritDoc
*/
public function getName()
{
return 'webman';
}
/**
* @inheritDoc
*/
public function getWidgets()
{
return [
"version" => [
"icon" => "github",
"tooltip" => "Webman Version",
"map" => "webman.version",
"default" => ""
],
"environment" => [
"icon" => "desktop",
"tooltip" => "Environment",
"map" => "webman.environment",
"default" => ""
],
];
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace WebmanTech\Debugbar;
use Closure;
use Throwable;
/**
* @method static void addThrowable(Throwable $e)
* @method static void addMessage($message, string $type = 'info')
* @method static void startMeasure(string $name, string $label = null)
* @method static void stopMeasure(string $name)
* @method static void addMeasure(string $label, float $start, float $end)
* @method static mixed measure(string $label, Closure $closure)
*/
class DebugBar
{
public const REQUEST_KEY = '_debugbar_request_instance';
/**
* @var null|WebmanDebugBar
*/
protected static $_instance = null;
public static function instance(): WebmanDebugBar
{
$request = request();
// 非 request 请求使用一个实例
if (!$request) {
if (!static::$_instance) {
$config = config('plugin.webman-tech.debugbar.app.debugbar', []);
static::$_instance = static::createDebugBar($config);
}
return static::$_instance;
}
// 每个 request 请求单独创建一个实例
if (!$request->{static::REQUEST_KEY}) {
$config = config('plugin.webman-tech.debugbar.app.debugbar', []);
$request->{static::REQUEST_KEY} = static::createDebugBar($config);
}
return $request->{static::REQUEST_KEY};
}
protected static function createDebugBar(array $config): WebmanDebugBar
{
return new WebmanDebugBar($config);
}
/**
* @param $name
* @param $arguments
* @return mixed
*/
public static function __callStatic($name, $arguments)
{
return static::instance()->{$name}(... $arguments);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace WebmanTech\Debugbar\Ext;
use Webman\Http\Request;
use Webman\Http\Response;
class HttpExt
{
/**
* @var Request
*/
protected $request;
/**
* @var Response
*/
protected $response;
public function __construct(Request $request, Response $response)
{
$this->request = $request;
$this->response = $response;
}
/**
* 是否是重定向
* @return bool
*/
public function isRedirection(): bool
{
return $this->response->getStatusCode() >= 300 && $this->response->getStatusCode() < 400;
}
/**
* 是否 accept html
* @return bool
*/
public function isHtmlAccepted(): bool
{
if ($this->request->isAjax()) {
return false;
}
$header = $this->request->header('Accept');
if (is_array($header)) {
$header = implode(', ', $header);
}
$is = strpos($header, 'text/html') !== false;
if ($is) {
return true;
}
return false;
}
/**
* 响应的是否是 html
* @return bool
*/
public function isHtmlResponse(): bool
{
if ($header = $this->response->getHeader('Content-Type')) {
if (is_array($header)) {
$header = implode(';', $header);
}
$is = strpos($header, 'text/html') !== false;
if ($is) {
return true;
}
}
if (strpos($this->response->rawBody(), '<html') === 0 || strpos($this->response->rawBody(), '<body') !== false) {
return true;
}
return false;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace WebmanTech\Debugbar\Helper;
class ArrayHelper
{
/**
* 合并数组
* @link https://github.com/yiisoft/arrays/blob/master/src/ArrayHelper.php::merge
* @param ...$arrays
* @return array
*/
public static function merge(...$arrays): array
{
$result = array_shift($arrays) ?: [];
while (!empty($arrays)) {
/** @var mixed $value */
foreach (array_shift($arrays) as $key => $value) {
if (is_int($key)) {
if (array_key_exists($key, $result)) {
if ($result[$key] !== $value) {
/** @var mixed */
$result[] = $value;
}
} else {
/** @var mixed */
$result[$key] = $value;
}
} elseif (isset($result[$key]) && is_array($value) && is_array($result[$key])) {
$result[$key] = static::merge($result[$key], $value);
} else {
/** @var mixed */
$result[$key] = $value;
}
}
}
return $result;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace WebmanTech\Debugbar\Helper;
class StringHelper
{
/**
* 通配符匹配
*
* - `\` 用于转译
* - `*` 匹配所有字符,包括空格
* - `?` 匹配任意单个字符
* - `[seq]` 匹配所有在中括号内的字符
* - `[a-z]` 匹配从 a 到 z
* - `[!seq]` 匹配所有不在中括号内的字符
*
* @param string $pattern 匹配规则
* @param string $string 匹配内容
* @param array $options 选项
*
* - caseSensitive: bool, 大小写敏感,默认为 true
* - escape: bool, 允许 \ 转译,默认为 true
*
* @return bool 是否匹配成功
*/
public static function matchWildcard(string $pattern, string $string, array $options = []): bool
{
if ($pattern === '*') {
return true;
}
$replacements = [
'\\\\\\\\' => '\\\\',
'\\\\\\*' => '[*]',
'\\\\\\?' => '[?]',
'\*' => '.*',
'\?' => '.',
'\[\!' => '[^',
'\[' => '[',
'\]' => ']',
'\-' => '-',
];
if (isset($options['escape']) && !$options['escape']) {
unset($replacements['\\\\\\\\'], $replacements['\\\\\\*'], $replacements['\\\\\\?']);
}
$pattern = strtr(preg_quote($pattern, '#'), $replacements);
$pattern = '#^' . $pattern . '$#us';
if (isset($options['caseSensitive']) && !$options['caseSensitive']) {
$pattern .= 'i';
}
return preg_match($pattern, $string) === 1;
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace WebmanTech\Debugbar;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static $pathRelation = array (
'config/plugin/webman-tech/debugbar' => 'config/plugin/webman-tech/debugbar',
);
/**
* Install
* @return void
*/
public static function install()
{
static::installByRelation();
}
/**
* Uninstall
* @return void
*/
public static function uninstall()
{
self::uninstallByRelation();
}
/**
* installByRelation
* @return void
*/
public static function installByRelation()
{
foreach (static::$pathRelation as $source => $dest) {
if ($pos = strrpos($dest, '/')) {
$parent_dir = base_path().'/'.substr($dest, 0, $pos);
if (!is_dir($parent_dir)) {
mkdir($parent_dir, 0777, true);
}
}
//symlink(__DIR__ . "/$source", base_path()."/$dest");
copy_dir(__DIR__ . "/$source", base_path()."/$dest");
echo "Create $dest
";
}
}
/**
* uninstallByRelation
* @return void
*/
public static function uninstallByRelation()
{
foreach (static::$pathRelation as $source => $dest) {
$path = base_path()."/$dest";
if (!is_dir($path) && !is_file($path)) {
continue;
}
echo "Remove $dest
";
if (is_file($path) || is_link($path)) {
unlink($path);
continue;
}
remove_dir($path);
}
}
}
@@ -0,0 +1,593 @@
<?php
namespace WebmanTech\Debugbar\Laravel\DataCollector;
use DebugBar\DataCollector\PDO\PDOCollector;
use DebugBar\DataCollector\TimeDataCollector;
use Illuminate\Support\Str;
/**
* @link https://github.com/barryvdh/laravel-debugbar/blob/master/src/DataCollector/QueryCollector.php
*
* Collects data about SQL statements executed with PDO
*/
class QueryCollector extends PDOCollector
{
protected $timeCollector;
protected $queries = [];
protected $renderSqlWithParams = false;
protected $findSource = false;
protected $middleware = [];
protected $durationBackground = true;
protected $explainQuery = false;
protected $explainTypes = ['SELECT']; // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
protected $showHints = false;
protected $showCopyButton = false;
protected $reflection = [];
protected $backtraceExcludePaths = [
'/vendor/laravel/framework/src/Illuminate/Support',
'/vendor/laravel/framework/src/Illuminate/Database',
'/vendor/laravel/framework/src/Illuminate/Events',
'/vendor/october/rain',
'/vendor/barryvdh/laravel-debugbar',
];
/**
* @param TimeDataCollector $timeCollector
*/
public function __construct(TimeDataCollector $timeCollector = null)
{
$this->timeCollector = $timeCollector;
}
/**
* Renders the SQL of traced statements with params embedded
*
* @param boolean $enabled
* @param string $quotationChar NOT USED
*/
public function setRenderSqlWithParams($enabled = true, $quotationChar = "'")
{
$this->renderSqlWithParams = $enabled;
}
/**
* Show or hide the hints in the parameters
*
* @param boolean $enabled
*/
public function setShowHints($enabled = true)
{
$this->showHints = $enabled;
}
/**
* Show or hide copy button next to the queries
*
* @param boolean $enabled
*/
public function setShowCopyButton($enabled = true)
{
$this->showCopyButton = $enabled;
}
/**
* Enable/disable finding the source
*
* @param bool $value
* @param array $middleware
*/
public function setFindSource($value, array $middleware)
{
$this->findSource = (bool) $value;
$this->middleware = $middleware;
}
/**
* Set additional paths to exclude from the backtrace
*
* @param array $excludePaths Array of file paths to exclude from backtrace
*/
public function mergeBacktraceExcludePaths(array $excludePaths)
{
$this->backtraceExcludePaths = array_merge($this->backtraceExcludePaths, $excludePaths);
}
/**
* Enable/disable the shaded duration background on queries
*
* @param bool $enabled
*/
public function setDurationBackground($enabled)
{
$this->durationBackground = $enabled;
}
/**
* Enable/disable the EXPLAIN queries
*
* @param bool $enabled
* @param array|null $types Array of types to explain queries (select/insert/update/delete)
*/
public function setExplainSource($enabled, $types)
{
$this->explainQuery = $enabled;
// workaround ['SELECT'] only. https://github.com/barryvdh/laravel-debugbar/issues/888
// if($types){
// $this->explainTypes = $types;
// }
}
/**
*
* @param string $query
* @param array $bindings
* @param float $time
* @param \Illuminate\Database\Connection $connection
*/
public function addQuery($query, $bindings, $time, $connection)
{
$explainResults = [];
$time = $time / 1000;
$endTime = microtime(true);
$startTime = $endTime - $time;
$hints = $this->performQueryAnalysis($query);
$pdo = null;
try {
$pdo = $connection->getPdo();
} catch (\Exception $e) {
// ignore error for non-pdo laravel drivers
}
$bindings = $connection->prepareBindings($bindings);
// Run EXPLAIN on this query (if needed)
if ($this->explainQuery && $pdo && preg_match('/^\s*(' . implode('|', $this->explainTypes) . ') /i', $query)) {
$statement = $pdo->prepare('EXPLAIN ' . $query);
$statement->execute($bindings);
$explainResults = $statement->fetchAll(\PDO::FETCH_CLASS);
}
$bindings = $this->getDataFormatter()->checkBindings($bindings);
if (!empty($bindings) && $this->renderSqlWithParams) {
foreach ($bindings as $key => $binding) {
// This regex matches placeholders only, not the question marks,
// nested in quotes, while we iterate through the bindings
// and substitute placeholders by suitable values.
$regex = is_numeric($key)
? "/(?<!\?)\?(?=(?:[^'\\\']*'[^'\\']*')*[^'\\\']*$)(?!\?)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
// Mimic bindValue and only quote non-integer and non-float data types
if (!is_int($binding) && !is_float($binding)) {
if ($pdo) {
try {
$binding = $pdo->quote((string) $binding);
} catch (\Exception $e) {
$binding = $this->emulateQuote($binding);
}
} else {
$binding = $this->emulateQuote($binding);
}
}
$query = preg_replace($regex, addcslashes($binding, '$'), $query, 1);
}
}
$source = [];
if ($this->findSource) {
try {
$source = array_slice($this->findSource(), 0, 5);
} catch (\Exception $e) {
}
}
$this->queries[] = [
'query' => $query,
'type' => 'query',
'bindings' => $this->getDataFormatter()->escapeBindings($bindings),
'time' => $time,
'source' => $source,
'explain' => $explainResults,
'connection' => $connection->getDatabaseName(),
'driver' => $connection->getConfig('driver'),
'hints' => $this->showHints ? $hints : null,
'show_copy' => $this->showCopyButton,
];
if ($this->timeCollector !== null) {
$this->timeCollector->addMeasure(Str::limit($query, 100), $startTime, $endTime);
}
}
/**
* Mimic mysql_real_escape_string
*
* @param string $value
* @return string
*/
protected function emulateQuote($value)
{
$search = ["\\", "\x00", "\n", "\r", "'", '"', "\x1a"];
$replace = ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"];
return "'" . str_replace($search, $replace, (string) $value) . "'";
}
/**
* Explainer::performQueryAnalysis()
*
* Perform simple regex analysis on the code
*
* @package xplain (https://github.com/rap2hpoutre/mysql-xplain-xplain)
* @author e-doceo
* @copyright 2014
* @version $Id$
* @access public
* @param string $query
* @return string[]
*/
protected function performQueryAnalysis($query)
{
// @codingStandardsIgnoreStart
$hints = [];
if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $query)) {
$hints[] = 'Use <code>SELECT *</code> only if you need all columns from table';
}
if (preg_match('/ORDER BY RAND()/i', $query)) {
$hints[] = '<code>ORDER BY RAND()</code> is slow, try to avoid if you can.
You can <a href="http://stackoverflow.com/questions/2663710/how-does-mysqls-order-by-rand-work" target="_blank">read this</a>
or <a href="http://stackoverflow.com/questions/1244555/how-can-i-optimize-mysqls-order-by-rand-function" target="_blank">this</a>';
}
if (strpos($query, '!=') !== false) {
$hints[] = 'The <code>!=</code> operator is not standard. Use the <code>&lt;&gt;</code> operator to test for inequality instead.';
}
if (stripos($query, 'WHERE') === false && preg_match('/^(SELECT) /i', $query)) {
$hints[] = 'The <code>SELECT</code> statement has no <code>WHERE</code> clause and could examine many more rows than intended';
}
if (preg_match('/LIMIT\\s/i', $query) && stripos($query, 'ORDER BY') === false) {
$hints[] = '<code>LIMIT</code> without <code>ORDER BY</code> causes non-deterministic results, depending on the query execution plan';
}
if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $query, $matches)) {
$hints[] = 'An argument has a leading wildcard character: <code>' . $matches[1] . '</code>.
The predicate with this argument is not sargable and cannot use an index if one exists.';
}
return $hints;
// @codingStandardsIgnoreEnd
}
/**
* Use a backtrace to search for the origins of the query.
*
* @return array
*/
protected function findSource()
{
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, 50);
$sources = [];
foreach ($stack as $index => $trace) {
$sources[] = $this->parseTrace($index, $trace);
}
return array_filter($sources);
}
/**
* Parse a trace element from the backtrace stack.
*
* @param int $index
* @param array $trace
* @return object|bool
*/
protected function parseTrace($index, array $trace)
{
$frame = (object) [
'index' => $index,
'namespace' => null,
'name' => null,
'line' => isset($trace['line']) ? $trace['line'] : '?',
];
if (isset($trace['function']) && $trace['function'] == 'substituteBindings') {
$frame->name = 'Route binding';
return $frame;
}
if (
isset($trace['class']) &&
isset($trace['file']) &&
!$this->fileIsInExcludedPath($trace['file'])
) {
$file = $trace['file'];
if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
list($file, $frame->line) = $this->getTwigInfo($trace);
} elseif (strpos($file, base_path() . '/storage') !== false) {
$hash = pathinfo($file, PATHINFO_FILENAME);
if (! $frame->name = $this->findViewFromHash($hash)) {
$frame->name = $hash;
}
$frame->namespace = 'view';
return $frame;
} elseif (strpos($file, 'Middleware') !== false) {
$frame->name = $this->findMiddlewareFromFile($file);
if ($frame->name) {
$frame->namespace = 'middleware';
} else {
$frame->name = $this->normalizeFilename($file);
}
return $frame;
}
$frame->name = $this->normalizeFilename($file);
return $frame;
}
return false;
}
/**
* Check if the given file is to be excluded from analysis
*
* @param string $file
* @return bool
*/
protected function fileIsInExcludedPath($file)
{
$normalizedPath = str_replace('\\', '/', $file);
foreach ($this->backtraceExcludePaths as $excludedPath) {
if (strpos($normalizedPath, $excludedPath) !== false) {
return true;
}
}
return false;
}
/**
* Find the middleware alias from the file.
*
* @param string $file
* @return string|null
*/
protected function findMiddlewareFromFile($file)
{
$filename = pathinfo($file, PATHINFO_FILENAME);
foreach ($this->middleware as $alias => $class) {
if (strpos($class, $filename) !== false) {
return $alias;
}
}
}
/**
* Find the template name from the hash.
*
* @param string $hash
* @return null|string
*/
protected function findViewFromHash($hash)
{
return null;
$finder = app('view')->getFinder();
if (isset($this->reflection['viewfinderViews'])) {
$property = $this->reflection['viewfinderViews'];
} else {
$reflection = new \ReflectionClass($finder);
$property = $reflection->getProperty('views');
$property->setAccessible(true);
$this->reflection['viewfinderViews'] = $property;
}
foreach ($property->getValue($finder) as $name => $path) {
if (sha1($path) == $hash || md5($path) == $hash) {
return $name;
}
}
}
/**
* Get the filename/line from a Twig template trace
*
* @param array $trace
* @return array The file and line
*/
protected function getTwigInfo($trace)
{
$file = $trace['object']->getTemplateName();
if (isset($trace['line'])) {
foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) {
if ($codeLine <= $trace['line']) {
return [$file, $templateLine];
}
}
}
return [$file, -1];
}
/**
* Shorten the path by removing the relative links and base dir
*
* @param string $path
* @return string
*/
protected function normalizeFilename($path)
{
if (file_exists($path)) {
$path = realpath($path);
}
return str_replace(base_path(), '', $path);
}
/**
* Collect a database transaction event.
* @param string $event
* @param \Illuminate\Database\Connection $connection
* @return array
*/
public function collectTransactionEvent($event, $connection)
{
$source = [];
if ($this->findSource) {
try {
$source = $this->findSource();
} catch (\Exception $e) {
}
}
$this->queries[] = [
'query' => $event,
'type' => 'transaction',
'bindings' => [],
'time' => 0,
'source' => $source,
'explain' => [],
'connection' => $connection->getDatabaseName(),
'driver' => $connection->getConfig('driver'),
'hints' => null,
'show_copy' => false,
];
}
/**
* Reset the queries.
*/
public function reset()
{
$this->queries = [];
}
/**
* {@inheritDoc}
*/
public function collect()
{
$totalTime = 0;
$queries = $this->queries;
$statements = [];
foreach ($queries as $query) {
$totalTime += $query['time'];
$statements[] = [
'sql' => $this->getDataFormatter()->formatSql($query['query']),
'type' => $query['type'],
'params' => [],
'bindings' => $query['bindings'],
'hints' => $query['hints'],
'show_copy' => $query['show_copy'],
'backtrace' => array_values($query['source']),
'duration' => $query['time'],
'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']),
'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])),
'connection' => $query['connection'],
];
// Add the results from the explain as new rows
if ($query['driver'] === 'pgsql') {
$explainer = trim(implode("\n", array_map(function ($explain) {
return $explain->{'QUERY PLAN'};
}, $query['explain'])));
if ($explainer) {
$statements[] = [
'sql' => " - EXPLAIN: {$explainer}",
'type' => 'explain',
];
}
} else {
foreach ($query['explain'] as $explain) {
$statements[] = [
'sql' => " - EXPLAIN # {$explain->id}: `{$explain->table}` ({$explain->select_type})",
'type' => 'explain',
'params' => $explain,
'row_count' => $explain->rows,
'stmt_id' => $explain->id,
];
}
}
}
if ($this->durationBackground) {
if ($totalTime > 0) {
// For showing background measure on Queries tab
$start_percent = 0;
foreach ($statements as $i => $statement) {
if (!isset($statement['duration'])) {
continue;
}
$width_percent = $statement['duration'] / $totalTime * 100;
$statements[$i] = array_merge($statement, [
'start_percent' => round($start_percent, 3),
'width_percent' => round($width_percent, 3),
]);
$start_percent += $width_percent;
}
}
}
$nb_statements = array_filter($queries, function ($query) {
return $query['type'] === 'query';
});
$data = [
'nb_statements' => count($nb_statements),
'nb_failed_statements' => 0,
'accumulated_duration' => $totalTime,
'accumulated_duration_str' => $this->formatDuration($totalTime),
'statements' => $statements
];
return $data;
}
/**
* {@inheritDoc}
*/
public function getName()
{
return 'queries';
}
/**
* {@inheritDoc}
*/
public function getWidgets()
{
return [
"queries" => [
"icon" => "database",
"widget" => "PhpDebugBar.Widgets.LaravelSQLQueriesWidget",
"map" => "queries",
"default" => "[]"
],
"queries:badge" => [
"map" => "queries.nb_statements",
"default" => 0
]
];
}
}
@@ -0,0 +1,90 @@
<?php
namespace WebmanTech\Debugbar\Laravel\DataFormatter;
use DebugBar\DataFormatter\DataFormatter;
/**
* @link https://github.com/barryvdh/laravel-debugbar/blob/master/src/DataFormatter/QueryFormatter.php
*/
class QueryFormatter extends DataFormatter
{
/**
* Removes extra spaces at the beginning and end of the SQL query and its lines.
*
* @param string $sql
* @return string
*/
public function formatSql($sql)
{
$sql = preg_replace("/\?(?=(?:[^'\\\']*'[^'\\']*')*[^'\\\']*$)(?:\?)/", '?', $sql);
$sql = trim(preg_replace("/\s*\n\s*/", "\n", $sql));
return $sql;
}
/**
* Check bindings for illegal (non UTF-8) strings, like Binary data.
*
* @param $bindings
* @return mixed
*/
public function checkBindings($bindings)
{
foreach ($bindings as &$binding) {
if (is_string($binding) && !mb_check_encoding($binding, 'UTF-8')) {
$binding = '[BINARY DATA]';
}
if (is_array($binding)) {
$binding = $this->checkBindings($binding);
$binding = '[' . implode(',', $binding) . ']';
}
if (is_object($binding)) {
$binding = json_encode($binding);
}
}
return $bindings;
}
/**
* Make the bindings safe for outputting.
*
* @param array $bindings
* @return array
*/
public function escapeBindings($bindings)
{
foreach ($bindings as &$binding) {
$binding = htmlentities((string) $binding, ENT_QUOTES, 'UTF-8', false);
}
return $bindings;
}
/**
* Format a source object.
*
* @param object|null $source If the backtrace is disabled, the $source will be null.
* @return string
*/
public function formatSource($source)
{
if (! is_object($source)) {
return '';
}
$parts = [];
if ($source->namespace) {
$parts['namespace'] = $source->namespace . '::';
}
$parts['name'] = $source->name;
$parts['line'] = ':' . $source->line;
return implode($parts);
}
}
+2
View File
@@ -0,0 +1,2 @@
该目录下的内容来自于 https://github.com/barryvdh/laravel-debugbar
并针对性的做一些调整(不适用于 webman 的地方)
@@ -0,0 +1,274 @@
(function ($) {
var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-');
/**
* Widget for the displaying sql queries
*
* Options:
* - data
*/
var LaravelSQLQueriesWidget = PhpDebugBar.Widgets.LaravelSQLQueriesWidget = PhpDebugBar.Widget.extend({
className: csscls('sqlqueries'),
onFilterClick: function (el) {
$(el).toggleClass(csscls('excluded'));
var excludedLabels = [];
this.$toolbar.find(csscls('.filter') + csscls('.excluded')).each(function () {
excludedLabels.push(this.rel);
});
this.$list.$el.find("li[connection=" + $(el).attr("rel") + "]").toggle();
this.set('exclude', excludedLabels);
},
onCopyToClipboard: function (el) {
var code = $(el).parent('li').find('code').get(0);
var copy = function () {
try {
document.execCommand('copy');
alert('Query copied to the clipboard');
} catch (err) {
console.log('Oops, unable to copy');
}
};
var select = function (node) {
if (document.selection) {
var range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
} else if (window.getSelection) {
var range = document.createRange();
range.selectNodeContents(node);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
}
copy();
window.getSelection().removeAllRanges();
};
select(code);
},
render: function () {
this.$status = $('<div />').addClass(csscls('status')).appendTo(this.$el);
this.$toolbar = $('<div></div>').addClass(csscls('toolbar')).appendTo(this.$el);
var filters = [], self = this;
this.$list = new PhpDebugBar.Widgets.ListWidget({ itemRenderer: function (li, stmt) {
if (stmt.type === 'transaction') {
$('<strong />').addClass(csscls('sql')).addClass(csscls('name')).text(stmt.sql).appendTo(li);
} else {
$('<code />').addClass(csscls('sql')).html(PhpDebugBar.Widgets.highlight(stmt.sql, 'sql')).appendTo(li);
}
if (stmt.width_percent) {
$('<div></div>').addClass(csscls('bg-measure')).append(
$('<div></div>').addClass(csscls('value')).css({
left: stmt.start_percent + '%',
width: Math.max(stmt.width_percent, 0.01) + '%',
})
).appendTo(li);
}
if (stmt.duration_str) {
$('<span title="Duration" />').addClass(csscls('duration')).text(stmt.duration_str).appendTo(li);
}
if (stmt.memory_str) {
$('<span title="Memory usage" />').addClass(csscls('memory')).text(stmt.memory_str).appendTo(li);
}
if (typeof(stmt.row_count) != 'undefined') {
$('<span title="Row count" />').addClass(csscls('row-count')).text(stmt.row_count).appendTo(li);
}
if (typeof(stmt.stmt_id) != 'undefined' && stmt.stmt_id) {
$('<span title="Prepared statement ID" />').addClass(csscls('stmt-id')).text(stmt.stmt_id).appendTo(li);
}
if (stmt.connection) {
$('<span title="Connection" />').addClass(csscls('database')).text(stmt.connection).appendTo(li);
li.attr("connection",stmt.connection);
if ( $.inArray(stmt.connection, filters) == -1 ) {
filters.push(stmt.connection);
$('<a />')
.addClass(csscls('filter'))
.text(stmt.connection)
.attr('rel', stmt.connection)
.on('click', function () {
self.onFilterClick(this); })
.appendTo(self.$toolbar);
if (filters.length > 1) {
self.$toolbar.show();
self.$list.$el.css("margin-bottom","20px");
}
}
}
if (typeof(stmt.is_success) != 'undefined' && !stmt.is_success) {
li.addClass(csscls('error'));
li.append($('<span />').addClass(csscls('error')).text("[" + stmt.error_code + "] " + stmt.error_message));
}
if (stmt.show_copy) {
$('<span title="Copy to clipboard" />')
.addClass(csscls('copy-clipboard'))
.css('cursor', 'pointer')
.on('click', function (event) {
self.onCopyToClipboard(this);
event.stopPropagation();
})
.appendTo(li);
}
var table = $('<table><tr><th colspan="2">Metadata</th></tr></table>').addClass(csscls('params')).appendTo(li);
if (stmt.bindings && stmt.bindings.length) {
table.append(function () {
var icon = 'thumb-tack';
var $icon = '<i class="phpdebugbar-fa phpdebugbar-fa-' + icon + ' phpdebugbar-text-muted"></i>';
var $name = $('<td />').addClass(csscls('name')).html('Bindings ' + $icon);
var $value = $('<td />').addClass(csscls('value'));
var $span = $('<span />').addClass('phpdebugbar-text-muted');
var index = 0;
var $bindings = new PhpDebugBar.Widgets.ListWidget({ itemRenderer: function (li, binding) {
var $index = $span.clone().text(index++ + '.');
li.append($index, '&nbsp;', binding).removeClass(csscls('list-item')).addClass(csscls('table-list-item'));
}});
$bindings.set('data', stmt.bindings);
$bindings.$el
.removeClass(csscls('list'))
.addClass(csscls('table-list'))
.appendTo($value);
return $('<tr />').append($name, $value);
});
}
if (stmt.hints && stmt.hints.length) {
table.append(function () {
var icon = 'question-circle';
var $icon = '<i class="phpdebugbar-fa phpdebugbar-fa-' + icon + ' phpdebugbar-text-muted"></i>';
var $name = $('<td />').addClass(csscls('name')).html('Hints ' + $icon);
var $value = $('<td />').addClass(csscls('value'));
var $hints = new PhpDebugBar.Widgets.ListWidget({ itemRenderer: function (li, hint) {
li.append(hint).removeClass(csscls('list-item')).addClass(csscls('table-list-item'));
}});
$hints.set('data', stmt.hints);
$hints.$el
.removeClass(csscls('list'))
.addClass(csscls('table-list'))
.appendTo($value);
return $('<tr />').append($name, $value);
});
}
if (stmt.backtrace && stmt.backtrace.length) {
table.append(function () {
var icon = 'list-ul';
var $icon = '<i class="phpdebugbar-fa phpdebugbar-fa-' + icon + ' phpdebugbar-text-muted"></i>';
var $name = $('<td />').addClass(csscls('name')).html('Backtrace ' + $icon);
var $value = $('<td />').addClass(csscls('value'));
var $span = $('<span />').addClass('phpdebugbar-text-muted');
var $backtrace = new PhpDebugBar.Widgets.ListWidget({ itemRenderer: function (li, source) {
var $parts = [
$span.clone().text(source.index + '.'),
'&nbsp;',
];
if (source.namespace) {
$parts.push(source.namespace + '::');
}
$parts.push(source.name);
$parts.push($span.clone().text(':' + source.line));
li.append($parts).removeClass(csscls('list-item')).addClass(csscls('table-list-item'));
}});
$backtrace.set('data', stmt.backtrace);
$backtrace.$el
.removeClass(csscls('list'))
.addClass(csscls('table-list'))
.appendTo($value);
return $('<tr />').append($name, $value);
});
}
if (stmt.params && !$.isEmptyObject(stmt.params)) {
for (var key in stmt.params) {
if (typeof stmt.params[key] !== 'function') {
table.append('<tr><td class="' + csscls('name') + '">' + key + '</td><td class="' + csscls('value') +
'">' + stmt.params[key] + '</td></tr>');
}
}
}
li.css('cursor', 'pointer').click(function () {
if (table.is(':visible')) {
table.hide();
} else {
table.show();
}
});
}});
this.$list.$el.appendTo(this.$el);
this.bindAttr('data', function (data) {
this.$list.set('data', data.statements);
this.$status.empty();
var stmt;
// Search for duplicate statements.
for (var sql = {}, duplicate = 0, i = 0; i < data.statements.length; i++) {
if (data.statements[i].type === 'query') {
stmt = data.statements[i].sql;
if (data.statements[i].bindings && data.statements[i].bindings.length) {
stmt += JSON.stringify(data.statements[i].bindings);
}
if (data.statements[i].connection) {
stmt += '@' + data.statements[i].connection;
}
sql[stmt] = sql[stmt] || { keys: [] };
sql[stmt].keys.push(i);
}
}
// Add classes to all duplicate SQL statements.
for (stmt in sql) {
if (sql[stmt].keys.length > 1) {
duplicate += sql[stmt].keys.length;
for (i = 0; i < sql[stmt].keys.length; i++) {
this.$list.$el.find('.' + csscls('list-item')).eq(sql[stmt].keys[i])
.addClass(csscls('sql-duplicate'))
.addClass(csscls('sql-duplicate-' + duplicate));
}
}
}
var t = $('<span />').text(data.nb_statements + " statements were executed").appendTo(this.$status);
if (data.nb_failed_statements) {
t.append(", " + data.nb_failed_statements + " of which failed");
}
if (duplicate) {
t.append(", " + duplicate + " of which were duplicated");
t.append(", " + (data.nb_statements - duplicate) + " unique");
}
if (data.accumulated_duration_str) {
this.$status.append($('<span title="Accumulated duration" />').addClass(csscls('duration')).text(data.accumulated_duration_str));
}
if (data.memory_usage_str) {
this.$status.append($('<span title="Memory usage" />').addClass(csscls('memory')).text(data.memory_usage_str));
}
});
}
});
})(PhpDebugBar.$);
@@ -0,0 +1,32 @@
<?php
namespace WebmanTech\Debugbar\Middleware;
use WebmanTech\Debugbar\DebugBar;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
class DebugBarMiddleware implements MiddlewareInterface
{
/**
* @inheritDoc
*/
public function process(Request $request, callable $handler): Response
{
$debugBar = DebugBar::instance();
if ($debugBar->isSkipRequest($request)) {
return $handler($request);
}
$debugBar->boot();
$debugBar->startMeasure(static::class, 'Application');
/** @var Response $response */
$response = $handler($request);
$debugBar->stopMeasure(static::class);
return $debugBar->modifyResponse($request, $response);
}
}
@@ -0,0 +1,796 @@
/** 参考:https://github.com/barryvdh/laravel-debugbar/blob/master/src/Resources/laravel-debugbar.css */
/* Force Laravel Whoops exception handler to be displayed under the debug bar */
.Whoops.container {
z-index: 5999999;
}
div.phpdebugbar {
font-size: 13px;
font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
direction: ltr;
text-align: left;
z-index: 6000000;
}
div.phpdebugbar * {
direction: ltr;
text-align: left;
}
div.phpdebugbar-openhandler-overlay {
z-index: 6000001;
cursor: pointer;
}
div.phpdebugbar-openhandler {
border: 1px solid #aaa;
border-top: 3px solid #2468f2;
width: 80%;
height: 70%;
padding: 10px;
border-radius: 5px;
overflow-y: scroll;
z-index: 6000002;
cursor: default;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > a,
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions button {
display: inline-block;
cursor: pointer;
margin: 5px;
padding: 0px 12px 2px;
border-radius: 3px;
border: 1px solid #ddd;
background-color: #f5f5f5;
color: #000;
text-shadow: 1px 1px #fff;
font-size: 13px;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form {
margin: 15px 0px 5px;
text-transform: uppercase;
font-size: 13px;
font-weight: bold;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form br {
display: none;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form > b {
display: none;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form input,
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form select {
margin: 0px 10px 10px 2px;
border: 1px solid #aaa;
border-radius: 3px;
padding: 3px 6px 2px;
line-height: 16px;
}
@media (max-width: 720px) {
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form select + br,
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form input[name="uri"] + br {
display: block;
}
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-actions > form select {
padding: 2px 5px 1px;
}
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 .phpdebugbar-openhandler-actions button {
outline: none;
margin: 0px 0px 10px 2px;
padding: 5px 15px 4px;
line-height: 12px;
}
div.phpdebugbar-openhandler table {
margin: 15px 0px 10px;
table-layout: auto;
border-collapse: collapse;
width: 100%;
}
div.phpdebugbar-openhandler table td,
div.phpdebugbar-openhandler table th {
width: auto!important;
text-align: left;
border: 0px solid #bbb;
padding: 2px 8px;
font-size: 14px;
}
div.phpdebugbar-openhandler table th {
text-shadow: 1px 1px #fff;
font-size: 12px;
text-transform: uppercase;
padding: 5px 8px;
}
div.phpdebugbar-openhandler table th,
div.phpdebugbar-openhandler table tr:nth-child(2n) {
background-color: #efefef;
}
div.phpdebugbar-openhandler table th:nth-child(1), div.phpdebugbar-openhandler table td:nth-child(1), /* Date */
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 */
width: 5%!important;
white-space: nowrap;
}
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 th:nth-child(3) { /* URL */
width: calc(100vw - 100% - 196px)!important;
}
div.phpdebugbar-openhandler table td a {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
div.phpdebugbar-resize-handle {
display: block!important;
height: 3px;
margin-top: -4px;
width: 100%;
background: none;
cursor: ns-resize;
border-top: none;
border-bottom: 0px;
background-color: #2468f2;
}
.phpdebugbar.phpdebugbar-minimized div.phpdebugbar-resize-handle {
cursor: default!important;
}
div.phpdebugbar-closed,
div.phpdebugbar-minimized {
border-top-color: #ddd;
}
div.phpdebugbar code, div.phpdebugbar pre, div.phpdebugbar samp {
background: none;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 1em;
border: 0;
padding: 0;
}
div.phpdebugbar .hljs {
padding: 0;
}
div.phpdebugbar .phpdebugbar-widgets-messages .hljs > code {
padding-bottom: 3px;
}
div.phpdebugbar code, div.phpdebugbar pre {
color: #000;
}
div.phpdebugbar-widgets-exceptions .phpdebugbar-widgets-filename {
margin-top: 4px;
}
div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item pre.phpdebugbar-widgets-file {
border: 1px solid #d2d2d2;
border-left: 2px solid #d2d2d2;
}
div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item pre.phpdebugbar-widgets-file[style="display: block;"] ~ div {
display: block;
}
div.phpdebugbar pre.sf-dump {
color: #000;
outline: none;
padding: 0px!important;
}
div.phpdebugbar-body {
border-top: 1px solid #ddd;
}
div.phpdebugbar-header {
min-height: 30px;
line-height: 20px;
padding-left: 39px;
text-shadow: 1px 1px #FFF;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-header {
background-size: 20px;
padding: 2px 0px 3px 36px;
background-position: 9px 5px;
border-radius: 3px;
}
div.phpdebugbar-openhandler .phpdebugbar-openhandler-header a {
cursor: pointer;
}
a.phpdebugbar-close-btn {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuM4zml1AAAADDSURBVDhPxZCxCsIwFEUzuGdwCvQD7BIIcehUXDqVfGM/wsG/iG4ifkzMlRuSPLo4eeFBue8c6Iv6b4wxW557Hs0KnWa3seqDxTiOyVqbhmF4UND4Rofdruyce3rvE6bIRSo9GOI1McbLPM/vVm4l7MAQr0kpHaQsJTDE+6zrepym6SVFdNgR69M+hBTLzWCI10gJvydvBkO8ZlmWayvhJnkzGOI1+fBTCOHWPkT7YNiBId4HizxnCKy+r81uX/otSn0A7dioI/vYX+8AAAAASUVORK5CYII=) no-repeat 9px 6px;
color : #555;
border-right: none;
}
a.phpdebugbar-open-btn {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAOCAYAAADJ7fe0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAHdElNRQfdCgYULwwNKp3GAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4wLjOM5pdQAAAA1UlEQVQ4T2OgKpCUlOQH4vdA/B8Jv4dKEwYgDdLS0v8NDQ3/GxsbwzGIj2YoGEO1oQJkjcRgqDZUAJKwsrJ6/v//fwdiMFQbKgAZkpGR0QR0ajy60wlgRJhBXSGhpqb2CNnZhHBkZORcqBEMDFBX2BsYGGBVjAv39vZaQ41gYIC6Ygs2hbiwr6/vdqA+DqgR4CiW19bWxqoYF87Ly4uFaocAZWXlydgU4sJ2dna3ga4QgGqHAC0trY/YFOPCKSkpDVCtCAA01QaIsaYJHFgCqpVagIEBACGlF2c3r4ViAAAAAElFTkSuQmCC) no-repeat 8px 6px;
}
div.phpdebugbar-header,
div.phpdebugbar-openhandler-header {
background-size: 21px auto;
background-position: 9px center;
}
a.phpdebugbar-restore-btn {
border-right-color: #ddd!important;
height: 20px;
width: 23px;
background-position: center;
background-size: 21px;
}
div.phpdebugbar-header > div > * {
font-size: 13px;
padding: 5px;
}
div.phpdebugbar-header .phpdebugbar-tab {
padding: 5px 8px;
border-left: 1px solid #ddd;
}
div.phpdebugbar-header .phpdebugbar-header-left {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div.phpdebugbar .phpdebugbar-header select {
margin: 4px 0px 0px 8px;
padding: 2px 3px 3px 3px;
border-radius: 3px;
width: auto;
cursor: pointer;
}
dl.phpdebugbar-widgets-kvlist dt,
dl.phpdebugbar-widgets-kvlist dd {
min-height: 20px;
line-height: 20px;
padding: 4px 5px 5px;
border-top: 0px;
}
dl.phpdebugbar-widgets-kvlist dd.phpdebugbar-widgets-value.phpdebugbar-widgets-pretty .phpdebugbar-widgets-code-block {
padding: 0px 0px;
background: transparent;
}
dl.phpdebugbar-widgets-kvlist dt {
width: 25%;
}
dl.phpdebugbar-widgets-kvlist dd {
margin-left: 25%;
}
ul.phpdebugbar-widgets-timeline .phpdebugbar-widgets-measure {
height: 28px;
line-height: 28px;
border: none;
}
ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-value {
height: 16px;
background-color: #63abca;
border-bottom: 2px solid #477e96;
}
ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label,
ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-collector {
top: 0px;
color: #000;
font-size: 11px;
}
ul.phpdebugbar-widgets-timeline li .phpdebugbar-widgets-value span.phpdebugbar-widgets-label {
color: #fff;
text-shadow: 1px 1px #000;
}
ul.phpdebugbar-widgets-timeline table.phpdebugbar-widgets-params {
font-size: 11px;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar {
width: calc(100% - 20px);
padding: 4px 0px 4px;
height: 20px;
border: 1px solid #ddd;
border-bottom: 0px;
background-color: #e8e8e8;
border-radius: 5px 5px 0px 0px;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar input {
width: calc(100% - 48px);
margin-left: 0px;
border-radius: 3px;
padding: 2px 6px;
height: 15px;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-label,
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-collector {
padding: 1px 0px 0px 10px;
margin: 0px;
text-transform: uppercase;
font-style: normal;
color: #333;
}
.phpdebugbar-widgets-toolbar i.phpdebugbar-fa.phpdebugbar-fa-search {
position: relative;
top: -1px;
padding: 0px 10px;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter,
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded {
position: relative;
top: -48px;
display: inline-block;
background-color: #6d6d6d;
margin-left: 3px;
border-radius: 3px;
padding: 5px 8px 4px;
text-transform: uppercase;
font-size: 10px;
text-shadow: 1px 1px #585858;
transition: background-color .25s linear 0s, color .25s linear 0s;
color: #FFF;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="info"],
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="info"] {
background-color: #5896e2;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="error"],
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="error"] {
background-color: #2468f2;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter[rel="warning"],
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded[rel="warning"] {
background-color: #f99400;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter:hover {
color: #FFF;
opacity: 0.85;
}
div.phpdebugbar-widgets-messages div.phpdebugbar-widgets-toolbar a.phpdebugbar-widgets-filter.phpdebugbar-widgets-excluded {
opacity: 0.45;
}
a.phpdebugbar-tab:hover,
span.phpdebugbar-indicator:hover,
a.phpdebugbar-indicator:hover,
a.phpdebugbar-close-btn:hover,
a.phpdebugbar-minimize-btn:hover,
a.phpdebugbar-maximize-btn:hover,
a.phpdebugbar-open-btn:hover {
background-color: #ebebeb;
/* transition: background-color .25s linear 0s, color .25s linear 0s; */
}
a.phpdebugbar-minimize-btn,
a.phpdebugbar-maximize-btn {
width: 28px!important;
}
a.phpdebugbar-tab.phpdebugbar-active {
background: #242ef2;
background-image: none;
color: #fff !important;
text-shadow: 1px 1px #3124f2;
}
a.phpdebugbar-tab.phpdebugbar-active span.phpdebugbar-badge {
background-color: white;
color: #2468f2;
text-shadow: 1px 1px #ebebeb;
}
a.phpdebugbar-tab span.phpdebugbar-badge {
vertical-align: 0px;
padding: 2px 8px 3px 8px;
text-align: center;
background: #2468f2;
font-size: 11px;
font-family: monospace;
color: #fff;
text-shadow: 1px 1px #3124f2;
border-radius: 10px;
top: -1px;
position: relative;
}
.phpdebugbar-indicator {
cursor: text;
}
.phpdebugbar-indicator span.phpdebugbar-tooltip,
div.phpdebugbar-mini-design a.phpdebugbar-tab:hover span.phpdebugbar-text {
border: none;
border-radius: 5px;
background: #f5f5f5;
font-size: 12px;
width: auto;
white-space: nowrap;
padding: 2px 18px;
text-shadow: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div.phpdebugbar-mini-design a.phpdebugbar-tab:hover span.phpdebugbar-text {
left: 0px;
right: auto;
}
.phpdebugbar-widgets-toolbar > .fa {
width: 25px;
font-size: 15px;
color: #555;
text-align: center;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item {
padding: 7px 10px;
border: none;
font-family: inherit;
overflow: visible;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:hover,
ul.phpdebugbar-widgets-timeline li:hover {
background-color: initial;
}
.phpdebugbar-widgets-sqlqueries ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item {
display: flex;
flex-wrap: wrap;
}
.phpdebugbar-widgets-templates ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item {
display: block;
}
.phpdebugbar-widgets-templates ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item,
.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item {
line-height: 15px;
}
.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item {
cursor: pointer;
display: block;
}
.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-subject {
display: inline-block;
margin-right: 15px;
}
.phpdebugbar-widgets-mails ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-headers {
margin: 10px 0px;
padding: 7px 10px;
border-left: 2px solid #ddd;
line-height: 17px;
}
.phpdebugbar-widgets-list .phpdebugbar-widgets-list-item .phpdebugbar-widgets-name {
height: 15px;
}
.phpdebugbar-widgets-sql.phpdebugbar-widgets-name {
font-weight: bold;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-sql {
flex: 1;
margin-right: 5px;
max-width: 100%;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-duration {
margin-left: auto;
margin-right: 5px;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-database {
margin-left: auto;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item .phpdebugbar-widgets-stmt-id {
margin-left: auto;
margin-right: 5px;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item table.phpdebugbar-widgets-params {
background-color: #fdfdfd;
margin: 10px 0px;
font-size: 12px;
border-left: 2px solid #cecece;
}
div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params th,
div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params td {
padding: 1px 10px!important;
}
div.phpdebugbar-widgets-templates table.phpdebugbar-widgets-params th {
padding: 2px 10px!important;
background-color: #efefef;
}
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name {
width: auto;
}
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name .phpdebugbar-fa {
position: relative;
top: 1px;
margin-left: 3px;
}
ul.phpdebugbar-widgets-list li.phpdebugbar-widgets-list-item:nth-child(even) {
background-color: #f5f5f5;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value {
display: inline-flex;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value:before {
font-family: PhpDebugbarFontAwesome;
content: "\f005";
color: #333;
font-size: 13px;
margin-right: 8px;
float: left;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-info {
color: #1299DA;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-info:before {
font-family: PhpDebugbarFontAwesome;
content: "\f05a";
color: #5896e2;
font-size: 15px;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-error {
color: #e74c3c;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-error:before {
color: #2468f2;
font-size: 15px;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-value.phpdebugbar-widgets-warning:before {
color: #FF9800;
font-size: 13px;
}
div.phpdebugbar-widgets-messages .phpdebugbar-widgets-value.phpdebugbar-widgets-warning {
color: #FF9800;
}
div.phpdebugbar-widgets-messages li.phpdebugbar-widgets-list-item pre.sf-dump {
display: inline-block;
position: relative;
top: -1px;
}
div.phpdebugbar-widgets-sqlqueries {
line-height: 20px;
}
div.phpdebugbar-panel div.phpdebugbar-widgets-status {
padding: 9px 20px!important;
width: calc(100% - 20px);
margin-left: -10px;
margin-top: -10px;
line-height: 11px!important;
font-weight: bold!important;
background: #f5f5f5!important;
border-bottom: 1px solid #cecece!important;
}
div.phpdebugbar-panel div.phpdebugbar-widgets-status > * {
color: #383838!important;
}
div.phpdebugbar-panel div.phpdebugbar-widgets-status > span:first-child:before {
font-family: PhpDebugbarFontAwesome;
content: "\f05a";
color: #737373;
text-shadow: 1px 1px #fff;
font-size: 14px;
position: relative;
top: 1px;
margin-right: 8px;
}
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params th,
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td {
padding: 4px 10px;
}
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params th {
background-color: #efefef;
}
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-name {
text-align: right;
vertical-align: top;
white-space: nowrap;
}
div.phpdebugbar-widgets-sqlqueries table.phpdebugbar-widgets-params td.phpdebugbar-widgets-value {
text-align: left;
}
div.phpdebugbar-widgets-templates .phpdebugbar-widgets-list-item table.phpdebugbar-widgets-params {
width: auto!important;
}
ul.phpdebugbar-widgets-list ul.phpdebugbar-widgets-table-list {
text-align: left;
}
.phpdebugbar-text-muted {
color: #888;
}
ul.phpdebugbar-widgets-cache a.phpdebugbar-widgets-forget {
float: right;
font-size: 12px;
padding: 0 4px;
background: #2468f2;
margin: 0 2px;
border-radius: 4px;
color: #fff;
text-decoration: none;
line-height: 1.5rem;
}
a.phpdebugbar-tab i {
line-height: 20px;
}
div.phpdebugbar-mini-design a.phpdebugbar-tab {
border-right: none;
}
div.phpdebugbar-header-right > a {
height: 20px;
width: 20px;
background-position: center;
}
div.phpdebugbar-header-right .phpdebugbar-indicator > i.phpdebugbar-fa {
vertical-align: baseline;
margin-top: 2px;
}
div.phpdebugbar-panel {
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
}
div.phpdebugbar-panel table {
margin: 10px 0px!important;
width: 100%!important;
}
div.phpdebugbar-panel table .phpdebugbar-widgets-name {
font-size: 13px;
}
dl.phpdebugbar-widgets-kvlist > :nth-child(4n-1),
dl.phpdebugbar-widgets-kvlist > :nth-child(4n) {
background-color: #f5f5f5;
}
.phpdebugbar pre.sf-dump:after {
clear: none!important;
}
div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item span.phpdebugbar-widgets-message {
color: #dd1044;
}
div.phpdebugbar-widgets-exceptions li.phpdebugbar-widgets-list-item > div {
display: none;
}
div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-database:before,
div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-duration:before,
div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-memory:before,
div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-row-count:before,
div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-copy-clipboard:before,
div.phpdebugbar-widgets-sqlqueries span.phpdebugbar-widgets-stmt-id:before,
div.phpdebugbar-widgets-templates span.phpdebugbar-widgets-param-count:before {
margin-right: 6px!important;
}
.phpdebugbar-widgets-list-item .phpdebugbar-widgets-bg-measure {
position: absolute;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
.phpdebugbar-widgets-bg-measure .phpdebugbar-widgets-value {
position: absolute;
height: 100%;
opacity: 0.2;
background: #242ef2;
}
@@ -0,0 +1,77 @@
<?php
namespace WebmanTech\Debugbar\Storage;
use DebugBar\Storage\FileStorage;
use Symfony\Component\Finder\Finder;
class AutoCleanFileStorage extends FileStorage
{
/**
* @var array
*/
protected $config = [
'gc_percent_by_files_count' => 50,
'files_count_max' => 1000,
'files_count_keep' => 400,
'gc_percent_by_hours' => 10,
'hours_keep' => 24,
];
public function __construct($dirname, array $config = [])
{
if (!class_exists(Finder::class)) {
throw new \InvalidArgumentException('必须先安装 symfony/finder');
}
parent::__construct($dirname);
$this->config = array_merge($this->config, $config);
}
/**
* @inheritDoc
*/
public function save($id, $data)
{
$this->autoClean();
parent::save($id, $data);
}
private function autoClean()
{
if (!is_dir($this->dirname)) {
return;
}
$gcPercent = random_int(0, 100);
if ($gcPercent < $this->config['gc_percent_by_files_count']) {
$this->cleanByDirMaxSize($this->config['files_count_max'], $this->config['files_count_keep']);
}
if ($gcPercent < $this->config['gc_percent_by_hours']) {
$this->cleanByKeepHours($this->config['hours_keep']);
}
}
private function cleanByDirMaxSize(int $filesCountMax, int $filesCountKeep)
{
$finder = Finder::create()->files()->name('*.json')->in($this->dirname);
if (($totalCount = $finder->count()) > $filesCountMax) {
foreach ($finder->sortByModifiedTime() as $file) {
unlink($file->getRealPath());
$totalCount--;
if ($totalCount <= $filesCountKeep) {
break;
}
}
}
}
private function cleanByKeepHours(int $hoursKeep)
{
foreach (Finder::create()->files()->name('*.json')->date('< ' . $hoursKeep . ' hour ago')->in(
$this->dirname
) as $file) {
unlink($file->getRealPath());
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace WebmanTech\Debugbar\Traits;
trait DebugBarOverwrite
{
/**
* 修改 $request_variables
* @inheritdoc
*/
public function collect()
{
$request = request();
$request_variables = array(
'method' => $request->method(),
'uri' => $request->uri(),
'ip' => $request->getRealIp(),
);
// 以下未修改
$this->data = array(
'__meta' => array_merge(
array(
'id' => $this->getCurrentRequestId(),
'datetime' => date('Y-m-d H:i:s'),
'utime' => microtime(true)
),
$request_variables
)
);
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;
}
}
+442
View File
@@ -0,0 +1,442 @@
<?php
namespace WebmanTech\Debugbar;
use Closure;
use DebugBar\DataCollector\DataCollectorInterface;
use DebugBar\DataCollector\ExceptionsCollector;
use DebugBar\DataCollector\MessagesCollector;
use DebugBar\DebugBar;
use DebugBar\OpenHandler;
use DebugBar\Storage\FileStorage;
use WebmanTech\Debugbar\DataCollector\LaravelQueryCollector;
use WebmanTech\Debugbar\DataCollector\LaravelRedisCollector;
use WebmanTech\Debugbar\DataCollector\MemoryCollector;
use WebmanTech\Debugbar\DataCollector\PhpInfoCollector;
use WebmanTech\Debugbar\DataCollector\RequestDataCollector;
use WebmanTech\Debugbar\DataCollector\RouteCollector;
use WebmanTech\Debugbar\DataCollector\SessionCollector;
use WebmanTech\Debugbar\DataCollector\TimeDataCollector;
use WebmanTech\Debugbar\DataCollector\WebmanCollector;
use WebmanTech\Debugbar\Ext\HttpExt;
use WebmanTech\Debugbar\Helper\ArrayHelper;
use WebmanTech\Debugbar\Helper\StringHelper;
use WebmanTech\Debugbar\Storage\AutoCleanFileStorage;
use WebmanTech\Debugbar\Traits\DebugBarOverwrite;
use support\Container;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\Route;
class WebmanDebugBar extends DebugBar
{
use DebugBarOverwrite;
/**
* @var array
*/
protected $config = [
'enabled' => true, // 弃用
'storage' => true, // 定义 storage
'http_driver' => true, // 定义 http_driver
'open_handler_url' => '/_debugbar/open', // storage 启用时打开历史的路由
'asset_base_url' => '/_debugbar/assets', // 静态资源的路由
'sample_url' => '/_debugbar/sample', // 示例页面,可用于查看 debugbar 信息,设为 null 关闭
'javascript_renderer_options' => [], // 其他 javascriptRenderer 参数
/**
* 需要忽略的请求路由
*/
'skip_request_path' => [
'/_debugbar/open',
'/_debugbar/assets/*',
'*.css',
'*.js',
],
/**
* 需要忽略的请求 callback 处理, function(Request $request): bool {}
*/
'skip_request_callback' => null,
/**
* 支持的 collectors,可以配置成 class 或者 callback
*/
'collectors_boot' => [
'phpinfo' => true,
'webman' => true,
'messages' => true,
'exceptions' => true,
'time' => true,
'memory' => true,
'route' => true,
'laravelDB' => true,
'laravelRedis' => true,
],
'collectors_response' => [
'request' => true,
'session' => true,
],
'options' => [
/**
* @see LaravelQueryCollector::$config
*/
'db' => [],
/**
* @see LaravelRedisCollector::$config
*/
'redis' => [],
],
];
public function __construct(array $config = [])
{
$this->config = ArrayHelper::merge($this->config, $config);
}
/**
* @var bool
*/
protected $booted = false;
public function boot(): void
{
if ($this->booted) {
return;
}
// 存储
if ($this->config['storage']) {
if ($this->config['storage'] === true) {
$this->config['storage'] = function () {
$path = runtime_path() . '/debugbar';
if (class_exists('Symfony\Component\Finder\Finder')) {
return new AutoCleanFileStorage($path);
}
// 使用该存储形式会导致文件数量极多
return new FileStorage($path);
};
}
$storage = call_user_func($this->config['storage']);
$this->setStorage($storage);
}
// Collector
$collectorMaps = $this->collectorMaps();
foreach ($this->config['collectors_boot'] as $name => $builder) {
if ($builder === false) {
continue;
}
if ($builder === true && isset($collectorMaps[$name])) {
$builder = $collectorMaps[$name];
}
if ($collector = $this->buildCollector($builder)) {
$this->addCollector($collector);
}
}
// javascriptRenderer
$this->bootJavascriptRenderer();
$this->booted = true;
}
/**
* 默认的 collector 配置
* @return array
*/
protected function collectorMaps(): array
{
return [
'webman' => WebmanCollector::class,
'phpinfo' => PhpInfoCollector::class,
'messages' => MessagesCollector::class,
'time' => TimeDataCollector::class,
'memory' => MemoryCollector::class,
'exceptions' => ExceptionsCollector::class,
'route' => RouteCollector::class,
'laravelDB' => function () {
if (class_exists('Illuminate\Database\Capsule\Manager')) {
$timeDataCollector = null;
if ($this->hasCollector('time')) {
/** @var \DebugBar\DataCollector\TimeDataCollector $timeDataCollector */
$timeDataCollector = $this->getCollector('time');
}
return new LaravelQueryCollector($this->config['options']['db'] ?? [], $timeDataCollector);
}
return null;
},
'laravelRedis' => function () {
if (class_exists('Illuminate\Redis\RedisManager')) {
$timeDataCollector = null;
if ($this->hasCollector('time')) {
/** @var \DebugBar\DataCollector\TimeDataCollector $timeDataCollector */
$timeDataCollector = $this->getCollector('time');
}
return new LaravelRedisCollector($this->config['options']['redis'] ?? [], $timeDataCollector);
}
return null;
},
'request' => function (Request $request, Response $response) {
return new RequestDataCollector($request, $response);
},
'session' => function (Request $request) {
if (request() && request()->session()) {
return new SessionCollector($request);
}
return null;
},
];
}
/**
* @param string|callable $collector
* @param mixed ...$params
* @return DataCollectorInterface|null
*/
protected function buildCollector($collector, ...$params): ?DataCollectorInterface
{
if (is_string($collector) && strpos($collector, '\\') !== false && class_exists($collector)) {
$collector = new $collector;
}
if (is_callable($collector)) {
$collector = call_user_func($collector, ...$params);
}
if (!$collector instanceof DataCollectorInterface) {
return null;
}
return $collector;
}
/**
* @inheritDoc
*/
public function getJavascriptRenderer($baseUrl = null, $basePath = null)
{
if ($this->jsRenderer === null) {
$this->jsRenderer = new WebmanJavascriptRenderer($this, $baseUrl, $basePath);
}
return $this->jsRenderer;
}
/**
* boot javascriptRenderer
*/
protected function bootJavascriptRenderer(): void
{
$renderer = $this->getJavascriptRenderer($this->config['asset_base_url']);
// 历史访问
if ($this->getStorage() && $this->config['open_handler_url']) {
$renderer->setOpenHandlerUrl($this->config['open_handler_url']);
}
// 其他配置参数
$renderer->setOptions($this->config['javascript_renderer_options']);
}
/**
* 注册必要路由
*/
public function registerRoute(): void
{
$this->boot();
// 示例的路由
if ($this->config['sample_url']) {
Route::get($this->config['sample_url'], function () {
return response("<html lang='en'><body><h1>DebugBar Sample</h1></body></html>");
});
}
// 历史记录的路由
if ($this->config['open_handler_url']) {
Route::get($this->config['open_handler_url'], function () {
$openHandler = new OpenHandler($this);
$data = $openHandler->handle(request()->get(), false, true);
return response($data);
});
}
// 静态资源路由
$renderer = $this->getJavascriptRenderer();
if ($renderer instanceof WebmanJavascriptRenderer) {
$renderer->registerAssetRoute();
}
}
/**
* 是否是需要忽略的请求
* @param Request $request
* @return bool
*/
public function isSkipRequest(Request $request): bool
{
if ($this->config['skip_request_path']) {
$path = $request->path();
foreach ($this->config['skip_request_path'] as $pathPattern) {
if (StringHelper::matchWildcard($pathPattern, $path)) {
return true;
}
}
}
if ($this->config['skip_request_callback']) {
if (call_user_func($this->config['skip_request_callback'], $request)) {
return true;
}
}
return false;
}
public function modifyResponse(Request $request, Response $response): Response
{
// handle exception
if ($exception = $response->exception()) {
$this->addThrowable($exception);
}
// 启用 session 支持
if ($this->config['http_driver']) {
if ($this->config['http_driver'] === true) {
$this->config['http_driver'] = new WebmanHttpDriver($request, $response);
}
if (is_callable($this->config['http_driver'])) {
$this->config['http_driver'] = call_user_func($this->config['http_driver']);
}
$this->setHttpDriver($this->config['http_driver']);
}
// 添加 collectors
$collectorMaps = $this->collectorMaps();
foreach ($this->config['collectors_response'] as $name => $builder) {
if ($builder === false) {
continue;
}
if ($builder === true) {
$builder = $collectorMaps[$name];
}
if ($collector = $this->buildCollector($builder, $request, $response)) {
$this->addCollector($collector);
}
}
// 处理 response
/** @var HttpExt $httpExt */
$httpExt = Container::make(HttpExt::class, ['request' => $request, 'response' => $response]);
if ($httpExt->isRedirection()) {
$this->stackData();
} elseif (!$httpExt->isHtmlResponse()) {
$this->sendDataInHeaders(true);
} elseif ($httpExt->isHtmlAccepted()) {
$response = $this->attachDebugBarToHtmlResponse($response);
}
return $response;
}
/**
* @param Response $response
* @return Response
*/
protected function attachDebugBarToHtmlResponse(Response $response): Response
{
$renderer = $this->getJavascriptRenderer();
$head = $renderer->renderHead();
$foot = $renderer->render();
$content = $response->rawBody();
$pos = strripos($content, '</head>');
if (false !== $pos) {
$content = substr($content, 0, $pos) . $head . substr($content, $pos);
} else {
$foot = $head . $foot;
}
$pos = strripos($content, '</body>');
if (false !== $pos) {
$content = substr($content, 0, $pos) . $foot . substr($content, $pos);
} else {
$content = $content . $foot;
}
$response->withBody($content);
$response->withoutHeader('Content-Length');
return $response;
}
/**
* @param Throwable $e
*/
public function addThrowable(Throwable $e)
{
if ($this->hasCollector('exceptions')) {
/** @var ExceptionsCollector $collector */
$collector = $this->getCollector('exceptions');
$collector->addThrowable($e);
}
}
/**
* @param $message
* @param string $type
*/
public function addMessage($message, string $type = 'info')
{
if ($this->hasCollector('messages')) {
/** @var MessagesCollector $collector */
$collector = $this->getCollector('messages');
$collector->addMessage($message, $type);
}
}
/**
* @param string $name
* @param string|null $label
*/
public function startMeasure(string $name, string $label = null)
{
if ($this->hasCollector('time')) {
/** @var \DebugBar\DataCollector\TimeDataCollector $collector */
$collector = $this->getCollector('time');
$collector->startMeasure($name, $label);
}
}
/**
* @param string $name
*/
public function stopMeasure(string $name)
{
if ($this->hasCollector('time')) {
/** @var \DebugBar\DataCollector\TimeDataCollector $collector */
$collector = $this->getCollector('time');
try {
$collector->stopMeasure($name);
} catch (\Exception $e) {
$this->addThrowable($e);
}
}
}
/**
* @param string $label
* @param float $start
* @param float $end
*/
public function addMeasure(string $label, float $start, float $end)
{
if ($this->hasCollector('time')) {
/** @var \DebugBar\DataCollector\TimeDataCollector $collector */
$collector = $this->getCollector('time');
$collector->addMeasure($label, $start, $end);
}
}
/**
* @param string $label
* @param Closure $closure
* @return mixed
*/
public function measure(string $label, Closure $closure)
{
if ($this->hasCollector('time')) {
/** @var \DebugBar\DataCollector\TimeDataCollector $collector */
$collector = $this->getCollector('time');
$result = $collector->measure($label, $closure);
} else {
$result = $closure();
}
return $result;
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace WebmanTech\Debugbar;
use DebugBar\HttpDriverInterface;
use Webman\Http\Request;
use Webman\Http\Response;
class WebmanHttpDriver implements HttpDriverInterface
{
/**
* @var Request
*/
protected $request;
/**
* @var Response
*/
protected $response;
public function __construct(Request $request, Response $response)
{
$this->request = $request;
$this->response = $response;
}
/**
* @inheritDoc
*/
function setHeaders(array $headers)
{
foreach ($headers as $key => $value) {
$this->response->header($key, $value);
}
}
/**
* @inheritDoc
*/
function isSessionStarted()
{
return !!$this->request->session();
}
/**
* @inheritDoc
*/
function setSessionValue($name, $value)
{
$this->request->session()->set($name, $value);
}
/**
* @inheritDoc
*/
function hasSessionValue($name)
{
return $this->request->session()->has($name);
}
/**
* @inheritDoc
*/
function getSessionValue($name)
{
return $this->request->session()->get($name);
}
/**
* @inheritDoc
*/
function deleteSessionValue($name)
{
$this->request->session()->delete($name);
}
}
@@ -0,0 +1,73 @@
<?php
namespace WebmanTech\Debugbar;
use DebugBar\DebugBar;
use DebugBar\JavascriptRenderer;
use Webman\Route;
class WebmanJavascriptRenderer extends JavascriptRenderer
{
protected $ajaxHandlerBindToJquery = true;
protected $ajaxHandlerBindToXHR = true;
protected $ajaxHandlerBindToFetch = true;
/**
* @var array
*/
protected $assetsInfo = [
'webman' => [
'path' => __DIR__ . '/Resources',
'css' => [
'webman-debugbar.css',
],
'js' => [],
],
'laravel' => [
'path' => __DIR__ . '/Laravel/Resources',
'css' => [],
'js' => [
'sqlqueries/widget.js',
],
]
];
public function __construct(DebugBar $debugBar, $baseUrl = null, $basePath = null)
{
parent::__construct($debugBar, $baseUrl, $basePath);
foreach ($this->assetsInfo as $name => $item) {
$this->addAssets($item['css'], $item['js'], $item['path'], $baseUrl . '/' . $name);
}
}
/**
* 注册静态资源路由
*/
public function registerAssetRoute(): void
{
Route::get($this->getBaseUrl() . '/[{path:.+}]', function ($request, $path = '') {
// 安全检查,避免url里 /../../../password 这样的非法访问
if (strpos($path, '..') !== false) {
return response('<h1>400 Bad Request</h1>', 400);
}
// debugbar 的静态文件目录
$staticBasePath = $this->getBasePath();
// 其他文件
foreach ($this->assetsInfo as $name => $item) {
if (strpos($path, $name . '/') === 0) {
$staticBasePath = $item['path'];
$path = substr($path, strlen($name) + 1);
break;
}
}
// 文件
$file = "$staticBasePath/$path";
if (!is_file($file)) {
return response('<h1>404 Not Found</h1>', 404);
}
return response()->withFile($file);
});
}
}
@@ -0,0 +1,13 @@
<?php
$enable = config('app.debug', false) && class_exists('WebmanTech\Debugbar\WebmanDebugBar');
return [
'enable' => $enable,
/**
* @see \WebmanTech\Debugbar\WebmanDebugBar::$config
*/
'debugbar' => [
'enable' => $enable,
],
];
@@ -0,0 +1,9 @@
<?php
use WebmanTech\Debugbar\Bootstrap\LaravelQuery;
use WebmanTech\Debugbar\Bootstrap\LaravelRedisExec;
return [
LaravelQuery::class,
LaravelRedisExec::class,
];
@@ -0,0 +1,9 @@
<?php
use WebmanTech\Debugbar\Middleware\DebugBarMiddleware;
return [
'' => [
DebugBarMiddleware::class,
],
];
@@ -0,0 +1,5 @@
<?php
use WebmanTech\Debugbar\DebugBar;
DebugBar::instance()->registerRoute();