vincecore/ide-link-bundle
PHP package that adds IDE deep links to error pages, logs, and stack traces, so you can jump from the browser or terminal straight to the exact file and line in PhpStorm, VS Code, or another editor. Configurable URL schemes and simple setup make debugging faster.
What it actually solves. When your app runs in a VM/Vagrant/Docker container, stack traces contain container paths (/var/www/app/src/Foo.php). Your IDE is on the host (/Users/you/code/app/src/Foo.php). The exception page's "open in editor" links point at files that don't exist on the host, so clicking a frame does nothing. vincecore/ide-link-bundle is a small Symfony bundle that rewrites those paths before the error page renders them.
Reality check before you install anything. This is a Symfony bundle from 2016 with 2 stars and no dependents (score 0.01). It targets Symfony 2/3-era APIs (getRequest(), YAML service definitions, non-autowired services). It will not composer require cleanly into a modern Symfony app without a fork, and it is not a Laravel package at all — no service provider, no auto-discovery, no config/ file. Treat it as a reference implementation of a 30-line idea, not a dependency.
Where to look first (in the repo, in this order):
README.md — the config keys for the two path prefixes (container → host).DependencyInjection/*Extension.php — the real config schema; READMEs from this era are usually stale.Resources/config/services.yml — tells you whether it hooks kernel.exception, a Twig extension, or the exception controller.EventListener/ or Twig/ — the actual rewrite logic. This is the only file you actually care about.First use case (Laravel). Skip the package; port the idea. You want: in local only, take every frame in the exception payload and str_replace(containerPrefix, hostPrefix, $frame['file']).
// config/ide-link.php
return [
'enabled' => env('APP_DEBUG') && env('IDE_LINK_HOST_PREFIX'),
'map' => [
'/var/www/app' => env('IDE_LINK_HOST_PREFIX'), // /Users/you/code/app
],
];
Then map frames inside a decorating exception handler:
class IdeLinkHandler extends Handler
{
public function __construct(private Handler $inner) {}
protected function convertExceptionToArray(Throwable $e): array
{
$data = parent::convertExceptionToArray($e);
$data['trace'] = collect($data['trace'])->map(function ($frame) {
if (!empty($frame['file'])) {
$frame['file'] = app(PathMapper::class)->map($frame['file']);
}
return $frame;
})->all();
return $data;
}
}
Bind PathMapper as a singleton, keep it a pure string -> string function, and you have effectively reimplemented the bundle in one afternoon with no abandoned-dependency risk.
Prefix-mapping table, longest-match-wins. The core operation is a replacement of path prefixes, not substrings. Build the map as an array, sort keys by length descending, walk it, return on first match. Naive str_replace across the whole HTML is what the 2016-era code likely did and it's the wrong shape — it will happily corrupt a filename that happens to contain your container prefix.
final class PathMapper
{
/** @param array<string,string> $map containerPrefix => hostPrefix */
public function __construct(private array $map, private bool $enabled) {}
public function map(string $path): string
{
if (!$this->enabled) return $path;
$keys = array_keys($this->map);
usort($keys, fn ($a, $b) => strlen($b) <=> strlen($a));
foreach ($keys as $from) {
if (str_starts_with($path, rtrim($from, '/'))) {
return rtrim($this->map[$from], '/') . substr($path, strlen(rtrim($from, '/')));
}
}
return $path;
}
}
Symfony equivalent (if you're actually in Symfony). The bundle almost certainly registers a kernel.exception listener and mutates the FlattenException traces. The key is listener priority: it must run before whatever renders the trace (priority > 0), otherwise the exception page has already been built from the original frames. The same longest-prefix logic applies; in Symfony the config is typically a two-entry hash of /var/www → /Users/you, plus a debug-only guard.
Multiple environments, one map. Developers flip between Docker, Vagrant, and plain php artisan serve on the host. Drive the map from env (IDE_LINK_HOST_PREFIX, IDE_LINK_CONTAINER_PREFIX) rather than hardcoding it in a config file — the host prefix is per-developer, the container prefix is per-project. Commit the container prefix; let each dev set the host one in .env.
IDE scheme is a separate concern. Rewriting the path only makes the link correct; it doesn't make it open. phpstorm://open?file=...&line=... and vscode://file/... have different escaping rules, and on macOS/Linux you often need a small ~/bin handler that converts the URL back into a CLI call. Keep the scheme handling in your layout/JS, out of the path mapper.
Integration point in Laravel. Ignition (Spatie) replaced Whoops and has its own frame rendering — if you're on a recent Laravel, hook Spatie\Ignition\Ignition::make()->... or register a SolutionProvider, or override Handler::render/convertExceptionToArray as above. Confirm which renderer is active before writing the mapper; mapping the wrong payload is a silent no-op.
symfony/symfony constraints from 2016. Fork and port, or copy the logic. Do not add it to composer.json and hope.realpath(). PHP reports the realpath of an included file, so if your project is symlinked inside the container, your configured prefix won't match the file's actual reported path. Check the actual getFile() values before writing the map — don't guess.file:///C:/... triples-of-slashes. The 2016 code predates most people caring about this; test it.APP_DEBUG / kernel.debug and return the input unchanged otherwise.file value from the raw exception before mapping, then the mapped value. Nine times out of ten the configured prefix is off by a trailing slash or the code is running from a different docroot than you assumed (/app vs /var/www/html).kernel.exception at a priority higher than the exception renderer, and verify by temporarily logging inside the listener.PathMapperInterface) so teams can swap in a container-aware resolver or a filesystem-abstraction lookup later. That's the one design improvement worth making over the original./var/www/html → project root — a five-line config, not a Symfony bundle.How can I help you explore Laravel packages today?