Skip to content

Customizable FileLinkFormatter #60338

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

Open
meiyasan opened this issue May 3, 2025 · 2 comments
Open

Customizable FileLinkFormatter #60338

meiyasan opened this issue May 3, 2025 · 2 comments

Comments

@meiyasan
Copy link

meiyasan commented May 3, 2025

Hello,

While integrating Symfony’s FileLinkFormatter to display relative paths for Dockerized environments, I encountered limitations that make the class hard to adapt for such advanced setups.

The class is marked as final, and most of its configuration (like $baseDir, $urlFormat, and $requestStack) is declared as private directly in the constructor. As a result:
• It’s difficult to extend or decorate the class behavior cleanly, and the class cannot be reused.
• Custom relative/host path mappings require rewriting the class instead of reusing its logic.

Would you consider:
• Making the class more extensible (e.g. via an interface or service decoration hook)?
• Promoting key properties to protected or configurable through setters?
• Or, exposing relative path rewriting directly as a feature?

These changes would make Symfony debugging much easier in virtualized setups and would avoid the need for brittle custom implementations.

Thanks for your advising on that or any help to bypass this issue !

@GromNaN
Copy link
Member

GromNaN commented May 3, 2025

Some IDE support file mapping directly. In PhpStorm, you have to set Deployment Mapping

Making the class more extensible (e.g. via an interface or service decoration hook)?

In VarDumper and CliDumber, the FileLinkFormatter instance is not injected. So you would have to introduce some global setting to configure it if you want to change the class.

Or, exposing relative path rewriting directly as a feature?

Adding this feature would be the more beneficial: this is a common need and it may be added without much complexity.

Maybe using some syntax like this:

SYMFONY_IDE=vscode+file_mapping[/path/in/container:/local/path]

On an other note: when I was faced with this problem, I cheated by mounting the files in Docker on the same path as locally. As a result, the file paths coincide between the local filesystem and Docker.
Of course it's not THE best solution, but it works.

@meiyasan
Copy link
Author

meiyasan commented May 4, 2025

Thanks @GromNaN that's a great comment, very helpful. here it goes for me, I defined a target path in my .env, then I modified the framework.ide variable introducing %b and the syntax pattern[from,to]:

# .env
APP_PATH=/path/to/my/target/directory
framework:
    ide: "vscode://file/%%f[%%b:%env(APP_PATH)%]:%%l"

At this time I still decorate the class but I believe it can almost be copied/pasted. I am not sure how to proceed with a PR, and what is the repository to be modified, but let me know if you would like me to make it, if this code is found to be useful.

<?php

namespace ...;

use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter as SymfonyFileLinkFormatter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class FileLinkFormatter extends SymfonyFileLinkFormatter
{
    protected array|false $fileLinkFormat;

    /**
     * @param string|\Closure $urlFormat The URL format, or a closure that returns it on-demand
     */
    public function __construct(
        string|array|null $fileLinkFormat = null,
        protected ?RequestStack $requestStack = null,
        protected ?string $baseDir = null,
        protected string|\Closure|null $urlFormat = null,
    ) {
        $fileLinkFormat ??= $_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? '';
        if (!\is_array($f = $fileLinkFormat)) {
            $f = (ErrorRendererInterface::IDE_LINK_FORMATS[$f] ?? $f) ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l';
            $i = strpos($f, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f);
            $fileLinkFormat = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, \PREG_SPLIT_DELIM_CAPTURE);
        }

        $this->fileLinkFormat = $fileLinkFormat;
        $this->baseDir = $baseDir;
    }

    public function format(string $file, int $line): string|false
    {
        if ($fmt = $this->getFileLinkFormat()) {
            for ($i = 1; isset($fmt[$i]); ++$i) {
                if (str_starts_with($file, $k = $fmt[$i++])) {
                    $file = substr_replace($file, $fmt[$i], 0, \strlen($k));
                    break;
                }
            }

            $dict = ['%f' => $file, '%l' => $line, '%b' => $this->baseDir];
            $fmt0 = preg_replace_callback(
                '/([^\/]+)\[([^\,\:\;]+)[\,\:\;]([^\,\:\;]+)\]/',
                function ($matches) use (&$dict, $file) {
                    $from = strtr($matches[2], $dict);
                    $to = strtr($matches[3], $dict);
                    return str_replace($from, $to, strtr($matches[1], $dict));
                },
                $fmt[0]
            );

            return strtr($fmt0, $dict);
        }

        return false;
    }

    /**
     * @internal
     */
    public function __sleep(): array
    {
        $this->fileLinkFormat = $this->getFileLinkFormat();

        return ['fileLinkFormat', 'baseDir'];
    }

    /**
     * @internal
     */
    public static function generateUrlFormat(UrlGeneratorInterface $router, string $routeName, string $queryString): ?string
    {
        try {
            return $router->generate($routeName).$queryString;
        } catch (\Throwable) {
            return null;
        }
    }

    protected function getFileLinkFormat(): array|false
    {
        if ($this->fileLinkFormat) {
            return $this->fileLinkFormat;
        }

        if ($this->requestStack && $this->baseDir && $this->urlFormat) {
            $request = $this->requestStack->getMainRequest();

            if ($request instanceof Request && (!$this->urlFormat instanceof \Closure || $this->urlFormat = ($this->urlFormat)())) {
                return [ 
                    $request->getSchemeAndHttpHost().$this->urlFormat,
                    $this->baseDir.\DIRECTORY_SEPARATOR, '',
                ];
            }
        }

        return false;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants