Skip to content

[ xdebug ] Map paths and skip paths with Xdebug 3.5#3115

Merged
brandonpayton merged 9 commits intotrunkfrom
ignore-files-and-directories-with-xdebug
Feb 28, 2026
Merged

[ xdebug ] Map paths and skip paths with Xdebug 3.5#3115
brandonpayton merged 9 commits intotrunkfrom
ignore-files-and-directories-with-xdebug

Conversation

@mho22
Copy link
Copy Markdown
Collaborator

@mho22 mho22 commented Jan 12, 2026

Motivation for the change, related issues

Related pull request : #2423

This pull request aims to provide a way to ignore paths from step debugging. This feature has been enabled internally in Xdebug 3.5 [ PHP 8.5 ] but for older PHP versions, only IDEs can somehow ignore paths.

This pull request focuses on Xdebug 3.5:

Implementation details

By adding a .xdebug directory with a path.map with the list of path mappings :

/foo/ = /bar/

and a skip.map file with the list of skipped paths :

/php/foo.php = SKIP
/baz/ = SKIP

Which then are determined during runtime loading :

const php = new PHP(await loadNodeRuntime('8.5', { withXdebug: { pathMappings : [ { hostPath : process.cwd(), vfsPath : '/' } ], pathSkippings : [ '/php/bar.php', '/baz' ] }}));

Testing Instructions

Partially in CI

And with this example :

/php/xdebug.php

<?php

echo "In\n";

require_once __DIR__ . "/foo.php";

foo();

echo "Out\n";

/php/foo.php

<?php

require_once __DIR__ . "/../baz/qux.php";

function foo() {
    echo "Foo\n";
	qux();
}

/baz/qux.php

<?php

require_once __DIR__ . "/../php/bar.php";

function qux() {
    echo "Qux\n";
	bar();
}

/php/bar.php

<?php

function bar() {
	echo "Hello Xdebug World!\n";
}

script.js

import { PHP, __private__dont__use } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
import fs from 'fs';


const php = new PHP(await loadNodeRuntime('8.5', { withXdebug: { pathMappings : [ { hostPath : process.cwd(), vfsPath : '/' } ], pathSkippings : [ '/php/bar.php', '/baz' ] }}));

php.mkdir( '/php' );

php.mkdir( '/baz' );

const files = [ '/php/xdebug.php', '/php/foo.php', '/php/bar.php', '/baz/qux.php' ];

files.forEach( file => php.writeFile( file, fs.readFileSync( `${process.cwd()}${file}` ) ) );

const response = await php.runStream( { scriptPath :  `/php/xdebug.php` } );

console.log( await response.stdoutText );

console.log( php.listFiles( '/' );
  1. Add a breakpoint in /php/xdebug.php on line 7
  2. Start the debugger
  3. Run node --no-warnings --loader=./packages/meta/src/node-es-module-loader/loader.mts script.js in the terminal
  4. Step into [ down arrow ]
  5. Witness the magic skip [ It should skip the foo.php file and go to the bar.php one ]
  6. Witness the magic mapping :
[
  'tmp',     'home',
  'dev',     'proc',
  'request', 'internal',
  '.xdebug', 'php',
  'baz'
]

No current working directory exists in the virtual filesystem.

@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented Jan 12, 2026

One issue I am facing while trying PHP 8.5 with Xdebug 3.5 is based on an if statement inside Xdebug's maps.c file on line 164:

/* For CLI scripts, only scan the current directory for map files */
	if (strcmp(script_source, "Standard input code") == 0) {
		size_t length;
		char *current_directory = virtual_getcwd_ex(&length);

		if (scan_directory_exists(current_directory)) {
			scan_directory(current_directory);
		}

		efree(current_directory);

		return;
	}

This is my script :

import { PHP, __private__dont__use } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
import fs from 'fs';
import { FSHelpers } from '@php-wasm/universal';


const php = new PHP(await loadNodeRuntime('8.5', { withXdebug:  true }));

// php.mkdir( '/php/.xdebug' );

// php.writeFile( '/php/.xdebug/paths.map', "/internal/shared/ = SKIP\n./php/bar.php = SKIP\n./baz/ = SKIP\n" );

php.mkdir( '/.xdebug' );

php.writeFile( '/.xdebug/paths.map', "/internal/shared/ = SKIP\n./php/bar.php = SKIP\n./baz/ = SKIP\n" );

FSHelpers.copyRecursive( php[__private__dont__use].FS,  `${process.cwd()}/php`, '/php' );

await php.runStream({ scriptPath: `/php/xdebug.php` });

fs.writeFileSync( 'xdebug.log', php.readFileAsText( '/tmp/xdebug.log' ) );

Even if I use a script path with an existing file path /php/xdebug.php it still considers the php process to be a CLI script. Therefore, it forces me to add a .xdebug directory next to the file I am running. Which is not conceivable in the future. I need to make sure script_source does not return "Standard input code".

@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented Jan 28, 2026

I was wrong earlier and I discovered a possible issue to fix in Xdebug 3.5. The code it crosses is the following :

current_dir = parts->c > 2 ? xdebug_join(slash, parts, 0, parts->c - 2) : NULL;
parent_dir = parts->c > 3 ? xdebug_join(slash, parts, 0, parts->c - 3) : NULL;
grand_dir = parts->c > 4 ? xdebug_join(slash, parts, 0, parts->c - 4) : NULL;

These variables are responsible of finding the .xdebug directory needed for path mappings/skipping.

But it seems to be problematically related to the root directory. Explanation :

If my current debugging directory is /foo/bar/baz/qux/xdebug.php, then :

file_path = "/foo/bar/baz/qux/xdebug.php"

current_dir = "/foo/bar/baz/qux"
parent_dir = "/foo/bar/baz"
grand_dir = "/foo/bar"

Ok. Now :

If my current debugging directory is /foo/bar/xdebug.php, then :

file_path = "/foo/bar/xdebug.php"

current_dir = "/foo/bar"
parent_dir = "/foo"
grand_dir = (NULL)

While grand_dir should probably return "/" instead. This is why I should PR Xdebug with this new simple suggestion :

- current_dir = parts->c > 2 ? xdebug_join(slash, parts, 0, parts->c - 2) : NULL;
+ current_dir = parts->c >= 2 ? xdebug_join(slash, parts, 0, parts->c - 2) : NULL;
- parent_dir = parts->c > 3 ? xdebug_join(slash, parts, 0, parts->c - 3) : NULL;
+ parent_dir = parts->c >= 3 ? xdebug_join(slash, parts, 0, parts->c - 3) : NULL;
- grand_dir = parts->c > 4 ? xdebug_join(slash, parts, 0, parts->c - 4) : NULL;
+ grand_dir = parts->c >= 4 ? xdebug_join(slash, parts, 0, parts->c - 4) : NULL;

Simply replacing > with >=.

Resulting in :

file_path = "/foo/bar/xdebug.php"

current_dir = "/foo/bar"
parent_dir = "/foo"
grand_dir = ""

Which will then produce the right scan in xdebug_logs :

[42] [Path Mapping] INFO: Scanning for map files with pattern '/.xdebug/*.map'
[42] [Path Mapping] INFO: Reading mapping file '/.xdebug/paths.map'
[42] [Path Mapping] INFO: Scanning for map files with pattern '/foo/.xdebug/*.map'
[42] [Path Mapping] DEBUG: No map files found with pattern '/foo/.xdebug/*.map'
[42] [Path Mapping] INFO: Scanning for map files with pattern '/foo/bar/.xdebug/*.map'
[42] [Path Mapping] DEBUG: No map files found with pattern '/foo/bar/.xdebug/*.map'
[42] [Path Mapping] DEBUG: Found 3 path mapping rules

This will help us create a /.xdebug directory at the root of our virtual filesystem since the index.php file is located in /wordpress/index.php

Currently it gives us this :

file_path = "/wordpress/index.php"

current_dir = "/wordpress"
parent_dir = (NULL)
grand_dir = (NULL)
[42] [Path Mapping] INFO: Scanning for map files with pattern '/wordpress/.xdebug/*.map'
[42] [Path Mapping] DEBUG: No map files found with pattern '/wordpress/.xdebug/*.map'
[42] [Path Mapping] DEBUG: Found 0 path mapping rules

While with my suggestion :

file_path = "/wordpress/index.php"

current_dir = "/wordpress"
parent_dir = ""
grand_dir = (NULL)
[42] [Path Mapping] INFO: Scanning for map files with pattern '/.xdebug/*.map'
[42] [Path Mapping] INFO: Reading mapping file '/.xdebug/paths.map'
[42] [Path Mapping] INFO: Scanning for map files with pattern '/wordpress/.xdebug/*.map'
[42] [Path Mapping] DEBUG: No map files found with pattern '/wordpress/.xdebug/*.map'
[42] [Path Mapping] DEBUG: Found 3 path mapping rules

Edit : The Xdebug pull request is ready for review.

@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented Jan 28, 2026

Eureka! I found a way to :

  • map paths from cwd to vfs
  • skip paths from cwd to vfs

in :

  • VSCODE
  • PHPSTORM
  • XDEBUG 3.5

🎉

@adamziel
Copy link
Copy Markdown
Collaborator

Linking to the XDebug PR for posterity xdebug/xdebug#1062

@adamziel
Copy link
Copy Markdown
Collaborator

adamziel commented Jan 28, 2026

@mho22 That PR is great, thank you for contributing upstream! We don't have to wait for it to be merged, released, etc, too – we can just patch our XDebug version.

@mho22 mho22 force-pushed the ignore-files-and-directories-with-xdebug branch from 0a68413 to 2e7fe43 Compare February 2, 2026 15:04
@mho22 mho22 changed the title [ xdebug ] Exclude file and directory paths from IDEs [ xdebug ] Map paths and skip paths with Xdebug 3.5 Feb 2, 2026
@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented Feb 2, 2026

I decided to split this pull request into two separate ones : this one for Xdebug 3.5 path mappings and skippings and the other for IDE path skipping.

@mho22 mho22 force-pushed the ignore-files-and-directories-with-xdebug branch 2 times, most recently from 8a88dc2 to 2c237b8 Compare February 3, 2026 09:36
@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented Feb 3, 2026

@adamziel Using PHP.wasm CLI :

It doesn't need to map paths since it calls useHostFilesystem(php); which means the current working directory maps perfectly the paths from Xdebug. However, about path skipping, I had to list all the virtual filesystem directories we don't want the user to reach, so :

withXdebug: hasXdebugOption ?? addXdebugConfig({
	pathSkippings: [
		'/dev/',
		'/home/',
		'/internal/',
		'/request/',
		'/proc/',
	],
}),

Using Playground CLI, it becomes more technical. We have to check if PHP is 8.5 or higher. If it is higher, then we recreate the tempDirSymlink and set the Xdebug Config the same way as for PHP.wasm CLI but with mounts and cwd. If it is lower than PHP8.5 we need the args.experimentalUnsafeIdeIntegration to be true to create IDE configs.

Am I missing something ?

@mho22 mho22 marked this pull request as ready for review February 3, 2026 12:05
@mho22 mho22 marked this pull request as draft February 3, 2026 12:07
@mho22 mho22 marked this pull request as ready for review February 3, 2026 14:06
@brandonpayton
Copy link
Copy Markdown
Member

This is great! I have an TODO in the multi-worker PR to find a way to avoid debug breaks during boot. We previously worked around it by booting with a dedicated worker, but in that PR, all workers are equal even though we do boot through one of them.

This should at least let us avoid breaking on /internal PHP files. It's likely we'd still break on /wordpress files during boot, but maybe that is OK for now.

I am reviewing this now but wanted to first share this happy sentiment along the way. :)

Copy link
Copy Markdown
Member

@brandonpayton brandonpayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @mho22! This looks good overall, but I left a few requests for changes.

mounts?: Mount[];
/**
* The IDE key to use for the debug configuration. Defaults to 'PLAYGROUNDCLI'.
* The IDE key to use for the debug configuration. Defaults to 'PHPWASMCLI'.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably fine, but I want to note that Defaults to 'PHPWASMCLI' is the kind of comment that can easily become out of sync with reality since this is not where the default is assigned.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PHPWASMCLI default here needs to stay documented because it must match the one used at runtime in with-xdebug.ts on line 76 when writing the xdebug.ini file and the ideKey in xdebug-path-mappings.ts on line 489.

That said, I could extract it into a shared constant to remove the drift risk entirely. Would you prefer that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the value needs to be the same between both, it sounds like it would be good to have a shared constant/declaration that expresses that. But whatever you think best works for me.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I created a DEFAULT_IDE_KEY constant.

@mho22 mho22 force-pushed the ignore-files-and-directories-with-xdebug branch 2 times, most recently from 051b84d to ef502d6 Compare February 17, 2026 15:58
@mho22 mho22 force-pushed the ignore-files-and-directories-with-xdebug branch from ef502d6 to 46b2777 Compare February 24, 2026 10:30
Copy link
Copy Markdown
Member

@brandonpayton brandonpayton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @mho22! I left additional comments, but please feel free to merge this when you think it is ready.

@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented Feb 25, 2026

@brandonpayton I added the requested changes except for the CLI output. I'll create a new pull request for that. Don't hesitate to review the new changes and merge if ready.

@brandonpayton brandonpayton merged commit 349a2ab into trunk Feb 28, 2026
42 checks passed
@brandonpayton brandonpayton deleted the ignore-files-and-directories-with-xdebug branch February 28, 2026 05:27
@brandonpayton
Copy link
Copy Markdown
Member

@brandonpayton I added the requested changes except for the CLI output. I'll create a new pull request for that. Don't hesitate to review the new changes and merge if ready.

Thanks, @mho22!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants