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 @@ -
+
+ + + + +
+
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 @@
-
- - htmlEncode($throwable->getName())?> - — - - - - +
+
+ + htmlEncode($throwable->getName())?> + — + + + + +
+
+
+
+ + getAttribute(ErrorCatcher::REQUEST_ATTRIBUTE_NAME_TIMER)) ?> + + + + +
+
+ + + +
@@ -85,19 +131,9 @@ renderPreviousExceptions($originalException) ?> - - Copied! - - - - - - +
+
@@ -119,19 +155,17 @@ class="copy-clipboard" renderCurl($request)) !== 'curl'): ?>
- - Copied! -

cURL

- - - - - + +
+

cURL

+ + + +
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' => [], ])); }