diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8a67a7..1d18a89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## 3.2.2 under development
+- Enh #117: Show arguments table by click (@xepozz)
- Enh #116: Remove @anonymous postfix (@xepozz)
- Bug #114: Stop `click` event on text selection (@xepozz)
- Enh #114: Show full argument by click (@xepozz)
diff --git a/src/Middleware/ErrorCatcher.php b/src/Middleware/ErrorCatcher.php
index bbfd8ac..fbaa85c 100644
--- a/src/Middleware/ErrorCatcher.php
+++ b/src/Middleware/ErrorCatcher.php
@@ -41,6 +41,8 @@
*/
final class ErrorCatcher implements MiddlewareInterface
{
+ public const REQUEST_ATTRIBUTE_NAME_TIMER = self::class . 'timer';
+
private HeadersProvider $headersProvider;
/**
@@ -129,6 +131,7 @@ public function forceContentType(string $contentType): self
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
+ $startTime = microtime(true);
try {
return $handler->handle($request);
} catch (Throwable $t) {
@@ -137,7 +140,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
} catch (Throwable $e) {
$t = new CompositeException($e, $t);
}
- return $this->generateErrorResponse($t, $request);
+ return $this->generateErrorResponse(
+ $t,
+ $request->withAttribute(
+ self::REQUEST_ATTRIBUTE_NAME_TIMER,
+ microtime(true) - $startTime,
+ ),
+ );
}
}
diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php
index 0081f0c..304b58b 100644
--- a/src/Renderer/HtmlRenderer.php
+++ b/src/Renderer/HtmlRenderer.php
@@ -245,7 +245,16 @@ public function renderPreviousExceptions(Throwable $t): string
public function renderCallStack(Throwable $t, array $trace = []): string
{
$application = $vendor = [];
- $application[1] = $this->renderCallStackItem($t->getFile(), $t->getLine(), null, null, [], 1, false);
+ $application[1] = $this->renderCallStackItem(
+ $t->getFile(),
+ $t->getLine(),
+ null,
+ null,
+ [],
+ 1,
+ false,
+ [],
+ );
$length = count($trace);
for ($i = 0; $i < $length; ++$i) {
@@ -254,18 +263,39 @@ public function renderCallStack(Throwable $t, array $trace = []): string
$class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null;
$args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : [];
+ $parameters = [];
$function = null;
if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') {
$function = $trace[$i]['function'];
+ if ($class !== null) {
+ $parameters = (new \ReflectionMethod($class, $function))->getParameters();
+ }
}
$index = $i + 2;
- if ($isVendor = $this->isVendorFile($file)) {
- $vendor[$index] = $this->renderCallStackItem($file, $line, $class, $function, $args, $index, $isVendor);
- continue;
+ if ($this->isVendorFile($file)) {
+ $vendor[$index] = $this->renderCallStackItem(
+ $file,
+ $line,
+ $class,
+ $function,
+ $args,
+ $index,
+ true,
+ $parameters,
+ );
+ } else {
+ $application[$index] = $this->renderCallStackItem(
+ $file,
+ $line,
+ $class,
+ $function,
+ $args,
+ $index,
+ false,
+ $parameters,
+ );
}
-
- $application[$index] = $this->renderCallStackItem($file, $line, $class, $function, $args, $index, $isVendor);
}
return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-items.php', [
@@ -302,7 +332,7 @@ public function argumentsToString(array $args, bool $truncate = true): string
}
if (is_object($value)) {
- $args[$key] = '' . $this->htmlEncode($this->removeAnonymous($value::class)) . '';
+ $args[$key] = '' . $this->htmlEncode($this->removeAnonymous($value::class) . '#' . spl_object_id($value)) . '';
} elseif (is_bool($value)) {
$args[$key] = '' . ($value ? 'true' : 'false') . '';
} elseif (is_string($value)) {
@@ -498,7 +528,8 @@ private function renderCallStackItem(
?string $function,
array $args,
int $index,
- bool $isVendorFile
+ bool $isVendorFile,
+ array $reflectionParameters,
): string {
$lines = [];
$begin = $end = 0;
@@ -525,6 +556,7 @@ private function renderCallStackItem(
'end' => $end,
'args' => $args,
'isVendorFile' => $isVendorFile,
+ 'reflectionParameters' => $reflectionParameters,
]);
}
diff --git a/templates/_call-stack-item.php b/templates/_call-stack-item.php
index 426d06e..bd4b0ab 100644
--- a/templates/_call-stack-item.php
+++ b/templates/_call-stack-item.php
@@ -1,15 +1,23 @@
Open the target page
@@ -32,7 +40,7 @@
-
+
removeAnonymous($class)}::$function";
@@ -48,9 +56,38 @@
- = sprintf('at line %d', $line + 1) ?>
+
+ = sprintf('at line %d', $line + 1) ?>
+
+
+
+
+
+ ';
+ foreach ($args as $key => $argument) {
+ echo '
';
+ $key = is_int($key) && isset($reflectionParameters[$key]) ? $reflectionParameters[$key]->getName() : $key;
+ echo '';
+ echo '$' . $this->htmlEncode($key) . '';
+ echo ' | ';
+ echo '';
+ echo '';
+ echo $this->argumentsToString(is_array($argument) ? $argument : [$argument]);
+ echo '';
+ echo '';
+ echo gettype($argument);
+ echo '';
+ echo ' | ';
+ echo '
';
+ }
+ echo '';
+ }
+ ?>
+
diff --git a/templates/development.css b/templates/development.css
index b9969bf..7c8ab72 100644
--- a/templates/development.css
+++ b/templates/development.css
@@ -40,6 +40,70 @@ ul {
--page-text-color: #505050;
--icon-color: #505050;
--icon-hover-color: #000;
+ --table-line-even-bg: #fff;
+ --table-line-even-color: #141414;
+ --table-line-odd-bg: #eee;
+ --table-line-odd-color: #141414;
+ --table-line-hover: #ccc;
+ --button-bg: #eee;
+ --button-color: #000;
+ --button-bg-hover: #d4d4d4;
+ --button-color-hover: #333;
+}
+
+.table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.table td, .table th {
+ border: 1px solid #ddd;
+ padding: 8px;
+}
+
+.table tr:nth-child(odd) {
+ border-color: var(--table-line-odd-bg);
+ background-color: var(--table-line-odd-bg);
+ color: var(--table-line-odd-color);
+}
+
+.table tr:nth-child(even) {
+ border-color: var(--table-line-even-bg);
+ background-color: var(--table-line-even-bg);
+ color: var(--table-line-even-color);
+}
+
+.table tr:hover {
+ background-color: var(--table-line-hover);
+}
+
+.argument-type {
+ padding-left: 4px;
+ opacity: 0.5;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ display: none;
+}
+
+.argument:hover .argument-type {
+ display: inline;
+}
+
+button.show-arguments-toggle {
+ padding: 4px;
+ font-size: 14px;
+ box-sizing: border-box;
+ border: 1px solid #ddd;
+ background: var(--button-bg);
+ color: var(--button-color)
+}
+
+button.show-arguments-toggle:hover {
+ border-color: var(--button-bg-hover);
+ background: var(--button-bg-hover);
+ color: var(--button-color-hover);
}
header {
@@ -137,13 +201,16 @@ header .exception-card {
word-break: break-word;
}
-header .exception-class {
- padding-right: 114px;
+header .exception-card-heading {
+ justify-content: space-between;
margin-bottom: 30px;
+ color: var(--exception-class-friendly-text-color);
+}
+
+header .exception-card-heading .exception-class {
font-weight: 500;
font-size: 36px;
line-height: 42px;
- color: var(--exception-class-friendly-text-color);
}
header .exception-class a {
@@ -330,7 +397,7 @@ header .previous h3 {
margin: 10px 0;
}
-#clipboard {
+.clipboard-content {
position: absolute;
top: -500px;
right: 300px;
@@ -338,21 +405,21 @@ header .previous h3 {
height: 150px;
}
-.copy-clipboard {
- position: absolute;
- right: 40px;
- top: 44px;
+.copy-clipboard svg path {
+ fill: var(--icon-color);
}
.copy-clipboard:hover svg path {
fill: var(--icon-hover-color);
}
-#copied {
- display: none;
- position: absolute;
- right: 76px;
- top: 51px;
+.copy-clipboard.copied svg path {
+ fill: green;
+}
+
+.copy-clipboard:hover svg,
+.copy-clipboard.copied svg {
+ zoom: 1.2;
}
#light-mode {
@@ -381,14 +448,56 @@ main {
display: none;
}
+.flex {
+ display: flex;
+}
+
+.flex-column {
+ flex-direction: column;
+}
+
.flex-1 {
flex: 1;
}
+.items-center {
+ align-items: center;
+}
+
.mw-100 {
max-width: 100%;
}
+.w-100 {
+ width: 100%;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.word-break {
+ overflow-wrap: break-word;
+ word-break: break-word;
+}
+
+.badges {
+ display: flex;
+ position: absolute;
+ right: 0;
+ bottom: 10px;
+ justify-content: end;
+}
+
+.badges .badge {
+ border: 1px solid var(--exception-card-border-color);
+ margin: auto 4px;
+ color: var(--exception-message-text-color);
+ padding: 4px 8px;
+ text-align: center;
+ border-radius: 5px;
+}
+
/* call stack */
.call-stack ul li,
.request {
@@ -467,8 +576,6 @@ main {
.call-stack ul li .element-wrap .function-info {
display: inline-block;
line-break: normal;
- overflow-wrap: break-word;
- word-break: break-word;
}
.call-stack ul li.application .element-wrap {
@@ -681,10 +788,21 @@ main {
/* start dark-theme */
.dark-theme {
- --page-bg-color: rgba(46,46,46, 0.9);
+ --page-bg-color: rgba(46, 46, 46, 0.9);
--page-text-color: #fff;
--icon-color: #989898;
--icon-hover-color: #fff;
+
+ --table-line-even-bg: #555;
+ --table-line-even-color: #eee;
+ --table-line-odd-bg: #999;
+ --table-line-odd-color: #eee;
+ --table-line-hover: #141414;
+
+ --button-bg: #666;
+ --button-color: #fff;
+ --button-bg-hover: #aaa;
+ --button-color-hover: #333;
}
.dark-theme header {
diff --git a/templates/development.php b/templates/development.php
index 38ae759..e6522c0 100644
--- a/templates/development.php
+++ b/templates/development.php
@@ -1,13 +1,16 @@
getMessage();
+function formatBytes(int $size, int $precision): string
+{
+ if ($size < 1024) {
+ return $size . ' B';
+ }
+
+ $factor = floor(log($size, 1024));
+ return sprintf("%.{$precision}f ", (float)($size / pow(1024, $factor))) . ['B', 'KB', 'MB', 'GB', 'TB', 'PB'][$factor];
+}
+function formatSeconds(float $time): string
+{
+ $hours = (int)($time/60/60);
+ $minutes = (int)($time/60)-$hours*60;
+ $seconds = $time-$hours*60*60-$minutes*60;
+ return number_format((float)$seconds, 4, '.', '') . ' sec';
+}
+$copyIcon = <<
+
+
+HTML;
+
?>
@@ -64,15 +89,36 @@
-
-
-
= $this->htmlEncode($throwable->getName())?>
- —
- = $exceptionClass ?>
-
-
= $exceptionClass ?>
-
+
+
+
+ = $this->htmlEncode($throwable->getName())?>
+ —
+ = $exceptionClass ?>
+
+ = $exceptionClass ?>
+
+
+
+
+
+
+ = formatSeconds($request->getAttribute(ErrorCatcher::REQUEST_ATTRIBUTE_NAME_TIMER)) ?>
+
+
+ = formatBytes(memory_get_peak_usage(true), 2) ?>
+
+
+
+
+ = $copyIcon ?>
+
+
@@ -85,19 +131,9 @@
= $this->renderPreviousExceptions($originalException) ?>
-
-
Copied!
-
-
-
-
+
+
@@ -119,19 +155,17 @@ class="copy-clipboard"
renderCurl($request)) !== 'curl'): ?>
-
-
Copied!
-
cURL
-
-
-
+
+
= $this->htmlEncode($curlInfo) ?>
@@ -193,11 +227,11 @@ class="copy-clipboard"
const callStackItems = document.getElementsByClassName('call-stack-item');
// If there are grouped vendor package files
- var vendorCollapse = document.getElementsByClassName('call-stack-vendor-collapse');
- for (var i = 0, imax = vendorCollapse.length; i < imax; ++i) {
+ const vendorCollapse = document.getElementsByClassName('call-stack-vendor-collapse');
+ for (let i = 0, imax = vendorCollapse.length; i < imax; ++i) {
vendorCollapse[i].addEventListener('click', function (event) {
- var vendorCollapseState = this.getElementsByClassName('call-stack-vendor-state')[0];
- var vendorCollapseItems = this.parentElement.getElementsByClassName('call-stack-vendor-items')[0];
+ const vendorCollapseState = this.getElementsByClassName('call-stack-vendor-state')[0];
+ const vendorCollapseItems = this.parentElement.getElementsByClassName('call-stack-vendor-items')[0];
if (vendorCollapseItems.style.display === 'block') {
vendorCollapseItems.style.display = 'none';
@@ -216,21 +250,21 @@ class="copy-clipboard"
hljs.listLanguages().forEach(function(language) {
hljs.getLanguage(language).disableAutodetect = true;
});
- for (var i = 0, imax = codeBlocks.length; i < imax; ++i) {
+ for (let i = 0, imax = codeBlocks.length; i < imax; ++i) {
hljs.highlightElement(codeBlocks[i]);
}
- var refreshCallStackItemCode = function(callStackItem) {
+ const refreshCallStackItemCode = function(callStackItem) {
if (!callStackItem.getElementsByTagName('pre')[0]) {
return;
}
- var top = callStackItem.getElementsByClassName('code-wrap')[0].offsetTop - window.pageYOffset + 3,
+ const top = callStackItem.getElementsByClassName('code-wrap')[0].offsetTop - window.pageYOffset + 3,
lines = callStackItem.getElementsByTagName('pre')[0].getClientRects(),
lineNumbers = callStackItem.getElementsByClassName('lines-item'),
errorLine = callStackItem.getElementsByClassName('error-line')[0],
hoverLines = callStackItem.getElementsByClassName('hover-line');
- for (var i = 0, imax = lines.length; i < imax; ++i) {
+ for (let i = 0, imax = lines.length; i < imax; ++i) {
if (!lineNumbers[i]) {
continue;
}
@@ -240,7 +274,7 @@ class="copy-clipboard"
hoverLines[i].style.height = parseInt(lines[i].bottom - lines[i].top + 6) + 'px';
hoverLines[i].style.width = hoverLines[i].parentElement.parentElement.scrollWidth + 'px'
- if (parseInt(callStackItem.getAttribute('data-line')) == i) {
+ if (parseInt(callStackItem.getAttribute('data-line')) === i) {
errorLine.style.top = parseInt(lines[i].top - top) + 'px';
errorLine.style.height = parseInt(lines[i].bottom - lines[i].top + 6) + 'px';
errorLine.style.width = errorLine.parentElement.parentElement.scrollWidth + 'px';
@@ -254,30 +288,29 @@ class="copy-clipboard"
// toggle code block visibility
stackItem.querySelector('.show-arguments-toggle')?.addEventListener('click', function (e) {
- e.stopPropagation()
-
- stackItem.getElementsByClassName('function-arguments-wrap')[0].classList.toggle('hidden')
+ e.stopPropagation();
+ stackItem.getElementsByClassName('function-arguments-wrap')[0].classList.toggle('hidden');
});
// toggle code block visibility
const arguments = stackItem.querySelector('.arguments');
arguments?.addEventListener('select', function (e) {
- e.stopPropagation()
- e.stopImmediatePropagation()
+ e.stopPropagation();
+ e.stopImmediatePropagation();
})
arguments?.addEventListener('click', function (e) {
- e.stopPropagation()
+ e.stopPropagation();
// stop click event on selecting text
if (document.getSelection()?.type === 'Range') {
- return
+ return;
}
const fullArguments = stackItem.querySelector('.full-arguments');
const shortArguments = stackItem.querySelector('.short-arguments');
if (fullArguments) {
- fullArguments.classList.toggle('hidden')
- shortArguments.classList.toggle('hidden')
+ fullArguments.classList.toggle('hidden');
+ shortArguments.classList.toggle('hidden');
}
});
@@ -288,7 +321,7 @@ class="copy-clipboard"
}
// stop click event on selecting text
if (document.getSelection()?.type === 'Range') {
- return
+ return;
}
var callStackItem = this.parentNode,
@@ -307,14 +340,13 @@ class="copy-clipboard"
refreshCallStackItemCode(callStackItem);
}
});
-
}
// handle copy stacktrace action on clipboard button
const copyIntoClipboard = function(e) {
e.preventDefault();
- const parentContainer = e.currentTarget.parentElement;
- const textarea = parentContainer.querySelector('#clipboard');
+ const currentTarget = e.currentTarget;
+ const textarea = document.querySelector('#' + currentTarget.dataset.target);
textarea.focus();
textarea.select();
@@ -325,12 +357,8 @@ class="copy-clipboard"
succeeded = false;
}
if (succeeded) {
- const hint = parentContainer.querySelector('#copied');
- if (!hint) {
- return
- }
- hint.style.display = 'block';
- setTimeout(() => hint.style.display = 'none', 2000);
+ currentTarget.classList.add('copied')
+ setTimeout(() => currentTarget.classList.remove('copied'), 600);
} else {
// fallback: show textarea if browser does not support copying directly
textarea.style.top = 0;
diff --git a/tests/Renderer/HtmlRendererTest.php b/tests/Renderer/HtmlRendererTest.php
index 6e9e7c8..610653c 100644
--- a/tests/Renderer/HtmlRendererTest.php
+++ b/tests/Renderer/HtmlRendererTest.php
@@ -140,6 +140,7 @@ public function testRenderCallStackItemIfFileIsNotExistAndLineMoreZero(): void
'args' => [],
'index' => 1,
'isVendorFile' => false,
+ 'reflectionParameters' => [],
]));
}