Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8918890
Render exception class PHPDoc description in HTML debug output
dbuhonov Mar 6, 2026
23ae325
Escape unsafe links in exception class PHPDoc descriptions rendered i…
dbuhonov Mar 10, 2026
86d6226
Apply PHP CS Fixer and Rector changes (CI)
dbuhonov Mar 10, 2026
70bd017
Add tests for rendering exception PHPDoc descriptions and unsafe cont…
dbuhonov Mar 10, 2026
4a102aa
Merge remote-tracking branch 'fork/feature/104-render-exception-descr…
dbuhonov Mar 10, 2026
2aa9e7e
Update throwable description handling to produce HTML fragments for e…
dbuhonov Mar 11, 2026
1e978c3
Add tests for escaping unsafe content in exception PHPDoc description…
dbuhonov Mar 11, 2026
82446ae
Improve verbose error rendering by adding solution and original excep…
dbuhonov Mar 12, 2026
9e3d0d8
Keep original throwable for templates, prepare separate displayThrowa…
dbuhonov Mar 12, 2026
aa1cb98
Apply PHP CS Fixer and Rector changes (CI)
dbuhonov Mar 12, 2026
7bd6931
Apply PHP CS Fixer and Rector changes (CI)
samdark Mar 12, 2026
71bfa0c
Potential fix for pull request finding
dbuhonov Mar 19, 2026
4a941b7
Normalize throwable description handling to ensure safe markdown-to-H…
dbuhonov Mar 19, 2026
c4c6883
Handle safe markdown links with query parameters in throwable descrip…
dbuhonov Mar 19, 2026
91cccef
Add test for handling parentheses in markdown links within throwable …
dbuhonov Mar 19, 2026
5822909
Add test for handling parentheses in markdown links in exception PHPD…
dbuhonov Mar 19, 2026
0ece04b
Add test for handling parentheses in markdown links in exception PHPD…
dbuhonov Mar 20, 2026
cf005fd
Render exception class PHPDoc description with safe markdown links in…
dbuhonov Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
use Yiisoft\Http\Header;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
Expand Down Expand Up @@ -45,11 +46,17 @@
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;
Expand Down Expand Up @@ -204,10 +211,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;
Comment thread
vjik marked this conversation as resolved.
Outdated
$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],
);
Expand Down Expand Up @@ -541,6 +554,60 @@ 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.
Comment thread
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;
}

return 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;
}
Comment thread
samdark marked this conversation as resolved.

/**
* Renders a template.
*
Expand Down
5 changes: 5 additions & 0 deletions templates/development.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/**
* @var ServerRequestInterface|null $request
* @var Throwable $throwable
* @var string|null $exceptionDescription
*/

$theme = $_COOKIE['yii-exception-theme'] ?? '';
Expand Down Expand Up @@ -93,6 +94,10 @@
<?= nl2br($this->htmlEncode($exceptionMessage)) ?>
</div>

<?php if ($exceptionDescription !== null): ?>
<div class="exception-description solution"><?= $this->parseMarkdown($exceptionDescription) ?></div>
Comment thread
samdark marked this conversation as resolved.
Outdated
<?php endif ?>

<?php if ($solution !== null): ?>
<div class="solution"><?= $this->parseMarkdown($solution) ?></div>
<?php endif ?>
Expand Down
41 changes: 41 additions & 0 deletions tests/Renderer/HtmlRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
use RuntimeException;
use Yiisoft\ErrorHandler\Exception\ErrorException;
use Yiisoft\ErrorHandler\Renderer\HtmlRenderer;
use Yiisoft\ErrorHandler\Tests\Support\TestDocBlockException;
use Yiisoft\ErrorHandler\Tests\Support\TestExceptionWithoutDocBlock;
use Yiisoft\ErrorHandler\Tests\Support\TestFriendlyException;
use Yiisoft\ErrorHandler\Tests\Support\TestHelper;

use function dirname;
Expand Down Expand Up @@ -66,6 +69,44 @@ public function testVerboseOutput(): void
$this->assertStringContainsString($exceptionMessage, (string) $errorData);
}

public function testVerboseOutputRendersThrowableDescriptionFromDocComment(): void
{
$renderer = new HtmlRenderer();
$exception = new TestDocBlockException('exception-test-message');

$errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock());
$result = (string) $errorData;

$this->assertStringContainsString('<div class="exception-description solution">', $result);
$this->assertStringContainsString('Test summary with <code>RuntimeException</code>.', $result);
$this->assertStringContainsString(
'<a href="https://www.yiiframework.com">Yii Framework</a>',
$result,
);
}

public function testVerboseOutputDoesNotRenderThrowableDescriptionWhenNoDocComment(): void
{
$renderer = new HtmlRenderer();
$exception = new TestExceptionWithoutDocBlock('exception-test-message');

$errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock());

$this->assertStringNotContainsString('<div class="exception-description solution">', (string) $errorData);
}

public function testVerboseOutputKeepsFriendlyExceptionBehaviorWithoutDescriptionDuplication(): void
{
$renderer = new HtmlRenderer();
$exception = new TestFriendlyException('exception-test-message');

$errorData = $renderer->renderVerbose($exception, $this->createServerRequestMock());
$result = (string) $errorData;

$this->assertStringContainsString('<div class="solution">', $result);
$this->assertStringNotContainsString('<div class="exception-description solution">', $result);
}

public function testNonVerboseOutputWithCustomTemplate(): void
{
$templateFileContents = '<html><?php echo $throwable->getMessage();?></html>';
Expand Down
16 changes: 16 additions & 0 deletions tests/Support/TestDocBlockException.php
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 {}
9 changes: 9 additions & 0 deletions tests/Support/TestExceptionWithoutDocBlock.php
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 {}
Loading