Skip to content

Commit 06bf83f

Browse files
nlemoinenicolas-grekas
authored andcommitted
[ErrorHandler] Improve fileLinkFormat handling
- Avoid repeating file link format guessing (logic is already in FileLinkFormatter class) - Always set a fileLinkFormat to a FileLinkFormatter object to handle path mappings properly
1 parent 0ee2cb7 commit 06bf83f

File tree

4 files changed

+285
-19
lines changed

4 files changed

+285
-19
lines changed

ErrorRenderer/FileLinkFormatter.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ErrorHandler\ErrorRenderer;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\RequestStack;
16+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
17+
18+
/**
19+
* Formats debug file links.
20+
*
21+
* @author Jérémy Romey <[email protected]>
22+
*
23+
* @final
24+
*/
25+
class FileLinkFormatter
26+
{
27+
private array|false $fileLinkFormat;
28+
private ?RequestStack $requestStack = null;
29+
private ?string $baseDir = null;
30+
private \Closure|string|null $urlFormat;
31+
32+
/**
33+
* @param string|\Closure $urlFormat the URL format, or a closure that returns it on-demand
34+
*/
35+
public function __construct(string|array $fileLinkFormat = null, RequestStack $requestStack = null, string $baseDir = null, string|\Closure $urlFormat = null)
36+
{
37+
$fileLinkFormat ??= $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? '';
38+
39+
if (!\is_array($f = $fileLinkFormat)) {
40+
$f = (ErrorRendererInterface::IDE_LINK_FORMATS[$f] ?? $f) ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l';
41+
$i = strpos($f, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f);
42+
$fileLinkFormat = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, \PREG_SPLIT_DELIM_CAPTURE);
43+
}
44+
45+
$this->fileLinkFormat = $fileLinkFormat;
46+
$this->requestStack = $requestStack;
47+
$this->baseDir = $baseDir;
48+
$this->urlFormat = $urlFormat;
49+
}
50+
51+
/**
52+
* @return string|false
53+
*/
54+
public function format(string $file, int $line): string|bool
55+
{
56+
if ($fmt = $this->getFileLinkFormat()) {
57+
for ($i = 1; isset($fmt[$i]); ++$i) {
58+
if (str_starts_with($file, $k = $fmt[$i++])) {
59+
$file = substr_replace($file, $fmt[$i], 0, \strlen($k));
60+
break;
61+
}
62+
}
63+
64+
return strtr($fmt[0], ['%f' => $file, '%l' => $line]);
65+
}
66+
67+
return false;
68+
}
69+
70+
/**
71+
* @internal
72+
*/
73+
public function __sleep(): array
74+
{
75+
$this->fileLinkFormat = $this->getFileLinkFormat();
76+
77+
return ['fileLinkFormat'];
78+
}
79+
80+
/**
81+
* @internal
82+
*/
83+
public static function generateUrlFormat(UrlGeneratorInterface $router, string $routeName, string $queryString): ?string
84+
{
85+
try {
86+
return $router->generate($routeName).$queryString;
87+
} catch (\Throwable) {
88+
return null;
89+
}
90+
}
91+
92+
private function getFileLinkFormat(): array|false
93+
{
94+
if ($this->fileLinkFormat) {
95+
return $this->fileLinkFormat;
96+
}
97+
98+
if ($this->requestStack && $this->baseDir && $this->urlFormat) {
99+
$request = $this->requestStack->getMainRequest();
100+
101+
if ($request instanceof Request && (!$this->urlFormat instanceof \Closure || $this->urlFormat = ($this->urlFormat)())) {
102+
return [
103+
$request->getSchemeAndHttpHost().$this->urlFormat,
104+
$this->baseDir.\DIRECTORY_SEPARATOR, '',
105+
];
106+
}
107+
}
108+
109+
return false;
110+
}
111+
}
112+
113+
if (!class_exists(\Symfony\Component\HttpKernel\Debug\FileLinkFormatter::class, false)) {
114+
class_alias(FileLinkFormatter::class, \Symfony\Component\HttpKernel\Debug\FileLinkFormatter::class);
115+
}

ErrorRenderer/HtmlErrorRenderer.php

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Symfony\Component\ErrorHandler\Exception\FlattenException;
1616
use Symfony\Component\HttpFoundation\RequestStack;
1717
use Symfony\Component\HttpFoundation\Response;
18-
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
1918
use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator;
2019
use Symfony\Component\VarDumper\Cloner\Data;
2120
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
@@ -37,7 +36,7 @@ class HtmlErrorRenderer implements ErrorRendererInterface
3736

3837
private bool|\Closure $debug;
3938
private string $charset;
40-
private string|array|FileLinkFormatter|false $fileLinkFormat;
39+
private FileLinkFormatter $fileLinkFormat;
4140
private ?string $projectDir;
4241
private string|\Closure $outputBuffer;
4342
private ?LoggerInterface $logger;
@@ -52,10 +51,7 @@ public function __construct(bool|callable $debug = false, string $charset = null
5251
{
5352
$this->debug = \is_bool($debug) ? $debug : $debug(...);
5453
$this->charset = $charset ?: (\ini_get('default_charset') ?: 'UTF-8');
55-
$fileLinkFormat ??= $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? null;
56-
$this->fileLinkFormat = \is_string($fileLinkFormat)
57-
? (ErrorRendererInterface::IDE_LINK_FORMATS[$fileLinkFormat] ?? $fileLinkFormat ?: false)
58-
: ($fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: false);
54+
$this->fileLinkFormat = $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : new FileLinkFormatter($fileLinkFormat);
5955
$this->projectDir = $projectDir;
6056
$this->outputBuffer = \is_string($outputBuffer) ? $outputBuffer : $outputBuffer(...);
6157
$this->logger = $logger;
@@ -210,15 +206,6 @@ private function getFileRelative(string $file): ?string
210206
return null;
211207
}
212208

213-
private function getFileLink(string $file, int $line): string|false
214-
{
215-
if ($fmt = $this->fileLinkFormat) {
216-
return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line);
217-
}
218-
219-
return false;
220-
}
221-
222209
/**
223210
* Formats a file path.
224211
*
@@ -242,11 +229,9 @@ private function formatFile(string $file, int $line, string $text = null): strin
242229
$text .= ' at line '.$line;
243230
}
244231

245-
if (false !== $link = $this->getFileLink($file, $line)) {
246-
return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', $this->escape($link), $text);
247-
}
232+
$link = $this->fileLinkFormat->format($file, $line);
248233

249-
return $text;
234+
return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', $this->escape($link), $text);
250235
}
251236

252237
/**
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\RequestStack;
18+
19+
class FileLinkFormatterTest extends TestCase
20+
{
21+
public function testWhenNoFileLinkFormatAndNoRequest()
22+
{
23+
$sut = new FileLinkFormatter([]);
24+
25+
$this->assertFalse($sut->format('/kernel/root/src/my/very/best/file.php', 3));
26+
}
27+
28+
public function testAfterUnserialize()
29+
{
30+
$ide = $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? null;
31+
$_ENV['SYMFONY_IDE'] = $_SERVER['SYMFONY_IDE'] = null;
32+
$sut = unserialize(serialize(new FileLinkFormatter()));
33+
34+
$this->assertSame('file:///kernel/root/src/my/very/best/file.php#L3', $sut->format('/kernel/root/src/my/very/best/file.php', 3));
35+
36+
if (null === $ide) {
37+
unset($_ENV['SYMFONY_IDE'], $_SERVER['SYMFONY_IDE']);
38+
} else {
39+
$_ENV['SYMFONY_IDE'] = $_SERVER['SYMFONY_IDE'] = $ide;
40+
}
41+
}
42+
43+
public function testWhenFileLinkFormatAndNoRequest()
44+
{
45+
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';
46+
47+
$sut = new FileLinkFormatter('debug://open?url=file://%f&line=%l', new RequestStack());
48+
49+
$this->assertSame("debug://open?url=file://$file&line=3", $sut->format($file, 3));
50+
}
51+
52+
public function testWhenNoFileLinkFormatAndRequest()
53+
{
54+
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';
55+
$requestStack = new RequestStack();
56+
$request = new Request();
57+
$requestStack->push($request);
58+
59+
$request->server->set('SERVER_NAME', 'www.example.org');
60+
$request->server->set('SERVER_PORT', 80);
61+
$request->server->set('SCRIPT_NAME', '/index.php');
62+
$request->server->set('SCRIPT_FILENAME', '/public/index.php');
63+
$request->server->set('REQUEST_URI', '/index.php/example');
64+
65+
$sut = new FileLinkFormatter([], $requestStack, __DIR__, '/_profiler/open?file=%f&line=%l#line%l');
66+
67+
$this->assertSame('http://www.example.org/_profiler/open?file=file.php&line=3#line3', $sut->format($file, 3));
68+
}
69+
70+
public function testIdeFileLinkFormat()
71+
{
72+
$file = __DIR__.\DIRECTORY_SEPARATOR.'file.php';
73+
74+
$sut = new FileLinkFormatter('atom');
75+
76+
$this->assertSame("atom://core/open/file?filename=$file&line=3", $sut->format($file, 3));
77+
}
78+
79+
public function testSerialize()
80+
{
81+
$this->assertInstanceOf(FileLinkFormatter::class, unserialize(serialize(new FileLinkFormatter())));
82+
}
83+
84+
/**
85+
* @dataProvider providePathMappings
86+
*/
87+
public function testIdeFileLinkFormatWithPathMappingParameters($mappings)
88+
{
89+
$params = array_reduce($mappings, function ($c, $m) {
90+
return "$c&".implode('>', $m);
91+
}, '');
92+
$sut = new FileLinkFormatter("vscode://file/%f:%l$params");
93+
foreach ($mappings as $mapping) {
94+
$fileGuest = $mapping['guest'].'file.php';
95+
$fileHost = $mapping['host'].'file.php';
96+
$this->assertSame("vscode://file/$fileHost:3", $sut->format($fileGuest, 3));
97+
}
98+
}
99+
100+
public static function providePathMappings()
101+
{
102+
yield 'single path mapping' => [
103+
[
104+
[
105+
'guest' => '/var/www/app/',
106+
'host' => '/user/name/project/',
107+
],
108+
],
109+
];
110+
yield 'multiple path mapping' => [
111+
[
112+
[
113+
'guest' => '/var/www/app/',
114+
'host' => '/user/name/project/',
115+
],
116+
[
117+
'guest' => '/var/www/app2/',
118+
'host' => '/user/name/project2/',
119+
],
120+
],
121+
];
122+
}
123+
}

Tests/ErrorRenderer/HtmlErrorRendererTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,47 @@ public static function getRenderData(): iterable
5454
$expectedNonDebug,
5555
];
5656
}
57+
58+
/**
59+
* @dataProvider provideFileLinkFormats
60+
*/
61+
public function testFileLinkFormat(\ErrorException $exception, string $fileLinkFormat, bool $withSymfonyIde, string $expected)
62+
{
63+
if ($withSymfonyIde) {
64+
$_ENV['SYMFONY_IDE'] = $fileLinkFormat;
65+
}
66+
$errorRenderer = new HtmlErrorRenderer(true, null, $withSymfonyIde ? null : $fileLinkFormat);
67+
68+
$this->assertStringContainsString($expected, $errorRenderer->render($exception)->getAsString());
69+
}
70+
71+
public static function provideFileLinkFormats(): iterable
72+
{
73+
$exception = new \ErrorException('Notice', 0, \E_USER_NOTICE);
74+
75+
yield 'file link format set as known IDE with SYMFONY_IDE' => [
76+
$exception,
77+
'vscode',
78+
true,
79+
'href="vscode://file/'.__DIR__,
80+
];
81+
yield 'file link format set as a raw format with SYMFONY_IDE' => [
82+
$exception,
83+
'phpstorm://open?file=%f&line=%l',
84+
true,
85+
'href="phpstorm://open?file='.__DIR__,
86+
];
87+
yield 'file link format set as known IDE without SYMFONY_IDE' => [
88+
$exception,
89+
'vscode',
90+
false,
91+
'href="vscode://file/'.__DIR__,
92+
];
93+
yield 'file link format set as a raw format without SYMFONY_IDE' => [
94+
$exception,
95+
'phpstorm://open?file=%f&line=%l',
96+
false,
97+
'href="phpstorm://open?file='.__DIR__,
98+
];
99+
}
57100
}

0 commit comments

Comments
 (0)