-
-
Notifications
You must be signed in to change notification settings - Fork 17
Render exception class PHPDoc description in HTML debug output #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
8918890
23ae325
86d6226
70bd017
4a102aa
2aa9e7e
1e978c3
82446ae
9e3d0d8
aa1cb98
7bd6931
71bfa0c
4a941b7
c4c6883
91cccef
5822909
0ece04b
cf005fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| use Yiisoft\ErrorHandler\ThrowableRendererInterface; | ||
| use Yiisoft\FriendlyException\FriendlyExceptionInterface; | ||
| use Yiisoft\Http\Header; | ||
| use ReflectionClass; | ||
| use ReflectionException; | ||
| use ReflectionFunction; | ||
| use ReflectionMethod; | ||
|
|
@@ -45,15 +46,22 @@ | |
| use function ob_implicit_flush; | ||
| use function ob_start; | ||
| use function realpath; | ||
| use function preg_match; | ||
| use function preg_replace; | ||
| use function preg_replace_callback; | ||
| use function preg_split; | ||
| use function str_replace; | ||
| use function str_starts_with; | ||
| use function stripos; | ||
| use function strlen; | ||
| use function count; | ||
| use function function_exists; | ||
| use function trim; | ||
|
|
||
| use const DIRECTORY_SEPARATOR; | ||
| use const ENT_QUOTES; | ||
| use const EXTR_OVERWRITE; | ||
| use const PREG_SPLIT_DELIM_CAPTURE; | ||
|
|
||
| /** | ||
| * Formats throwable into HTML string. | ||
|
|
@@ -204,10 +212,16 @@ public function render(Throwable $t, ?ServerRequestInterface $request = null): E | |
|
|
||
| public function renderVerbose(Throwable $t, ?ServerRequestInterface $request = null): ErrorData | ||
| { | ||
| $displayThrowable = $t instanceof CompositeException ? $t->getFirstException() : $t; | ||
| $exceptionDescription = $displayThrowable instanceof FriendlyExceptionInterface | ||
| ? null | ||
| : $this->getThrowableDescription($displayThrowable); | ||
|
|
||
| return new ErrorData( | ||
| $this->renderTemplate($this->verboseTemplate, [ | ||
| 'request' => $request, | ||
| 'throwable' => $t, | ||
| 'exceptionDescription' => $exceptionDescription, | ||
| ]), | ||
| [Header::CONTENT_TYPE => self::CONTENT_TYPE], | ||
| ); | ||
|
|
@@ -541,6 +555,108 @@ public function removeAnonymous(string $value): string | |
| return $anonymousPosition !== false ? substr($value, 0, $anonymousPosition) : $value; | ||
| } | ||
|
|
||
| /** | ||
| * Extracts a user-facing description from throwable class PHPDoc. | ||
| * | ||
| * Takes only descriptive text before block tags and normalizes inline | ||
| * {@see ...}/{@link ...} annotations into markdown-friendly form. | ||
|
samdark marked this conversation as resolved.
Outdated
|
||
| */ | ||
| private function getThrowableDescription(Throwable $throwable): ?string | ||
| { | ||
| $docComment = (new ReflectionClass($throwable))->getDocComment(); | ||
| if ($docComment === false) { | ||
| return null; | ||
| } | ||
|
|
||
| $descriptionLines = []; | ||
| foreach (preg_split('/\R/', $docComment) ?: [] as $line) { | ||
| $line = trim($line); | ||
| $line = preg_replace('/^\/\*\*?/', '', $line) ?? $line; | ||
| $line = preg_replace('/\*\/$/', '', $line) ?? $line; | ||
| $line = preg_replace('/^\*/', '', $line) ?? $line; | ||
| $line = trim($line); | ||
|
|
||
| if ($line !== '' && str_starts_with($line, '@')) { | ||
| break; | ||
| } | ||
|
|
||
| $descriptionLines[] = $line; | ||
| } | ||
|
|
||
| $description = trim(implode("\n", $descriptionLines)); | ||
| if ($description === '') { | ||
| return null; | ||
| } | ||
|
|
||
| $description = preg_replace_callback( | ||
| '/\{@(see|link)\s+([^\s}]+)(?:\s+([^}]+))?}/i', | ||
| static function (array $matches): string { | ||
| $target = $matches[2]; | ||
| $label = trim($matches[3] ?? ''); | ||
|
|
||
| if (preg_match('/^https?:\/\//i', $target) === 1) { | ||
| $text = $label !== '' ? $label : $target; | ||
| return '[' . $text . '](' . $target . ')'; | ||
| } | ||
|
|
||
| if ($label !== '') { | ||
| return $label . ' (`' . $target . '`)'; | ||
| } | ||
|
|
||
| return '`' . $target . '`'; | ||
| }, | ||
| $description, | ||
| ) ?? $description; | ||
|
|
||
| $paragraphs = preg_split('/\R\s*\R/', $description) ?: []; | ||
| $result = []; | ||
|
|
||
| foreach ($paragraphs as $paragraph) { | ||
| $paragraph = trim($paragraph); | ||
| if ($paragraph === '') { | ||
| continue; | ||
| } | ||
|
|
||
| $parts = preg_split( | ||
| '/(\[[^]]+]\([^)]+\)|`[^`]+`)/', | ||
|
vjik marked this conversation as resolved.
Outdated
|
||
| $paragraph, | ||
| -1, | ||
| PREG_SPLIT_DELIM_CAPTURE, | ||
| ) ?: []; | ||
|
dbuhonov marked this conversation as resolved.
Outdated
|
||
|
|
||
| $html = ''; | ||
| foreach ($parts as $part) { | ||
| if ($part === '') { | ||
| continue; | ||
| } | ||
|
|
||
| if (preg_match('/^\[([^]]+)]\(([^)]+)\)$/', $part, $matches) === 1) { | ||
| if (preg_match('/^https?:\/\//i', $matches[2]) === 1) { | ||
| $html .= '<a href="' . $this->htmlEncode($matches[2]) . '">' | ||
| . $this->htmlEncode($matches[1]) | ||
| . '</a>'; | ||
| } else { | ||
| $html .= $this->htmlEncode($matches[1]) | ||
| . ' (<code>' . $this->htmlEncode($matches[2]) . '</code>)'; | ||
| } | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| if (preg_match('/^`([^`]+)`$/', $part, $matches) === 1) { | ||
| $html .= '<code>' . $this->htmlEncode($matches[1]) . '</code>'; | ||
| continue; | ||
| } | ||
|
|
||
| $html .= $this->htmlEncode($part); | ||
| } | ||
|
|
||
| $result[] = '<p>' . $html . '</p>'; | ||
| } | ||
|
Comment on lines
+634
to
+669
|
||
|
|
||
| return $result === [] ? null : implode("\n", $result); | ||
| } | ||
|
samdark marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Renders a template. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\ErrorHandler\Tests\Support; | ||
|
|
||
| use RuntimeException; | ||
|
|
||
| /** | ||
| * Test summary with {@see RuntimeException}. | ||
| * | ||
| * More details with {@link https://www.yiiframework.com Yii Framework}. | ||
| * | ||
| * @link https://example.com Ignored tag. | ||
| */ | ||
| final class TestDocBlockException extends RuntimeException {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\ErrorHandler\Tests\Support; | ||
|
|
||
| use RuntimeException; | ||
|
|
||
| /** | ||
| * | ||
| * @link https://example.com Ignored tag. | ||
| */ | ||
| final class TestEmptyDescriptionDocBlockException extends RuntimeException {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\ErrorHandler\Tests\Support; | ||
|
|
||
| use RuntimeException; | ||
|
|
||
| final class TestExceptionWithoutDocBlock extends RuntimeException {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\ErrorHandler\Tests\Support; | ||
|
|
||
| use RuntimeException; | ||
|
|
||
| /** | ||
| * Description with raw `inline-code` and {@see RuntimeException}. | ||
| */ | ||
| final class TestInlineCodeDocBlockException extends RuntimeException {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\ErrorHandler\Tests\Support; | ||
|
|
||
| use RuntimeException; | ||
|
|
||
| /** | ||
| * Unsafe HTML <img src="x" onerror="alert(1)"> should not survive. | ||
| * | ||
| * {@link javascript:alert(1) Click me} and {@link https://www.yiiframework.com Safe link}. | ||
| */ | ||
| final class TestUnsafeDocBlockException extends RuntimeException {} |
Uh oh!
There was an error while loading. Please reload this page.