Skip to content

[XDebug] Set PHP Root in @php-wasm/cli #2421

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

Draft
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion packages/php-wasm/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime, useHostFilesystem } from '@php-wasm/node';
import { startBridge } from '@php-wasm/xdebug-bridge';
import path from 'path';
import { cwd } from 'process';

let args = process.argv.slice(2);
if (!args.length) {
Expand Down Expand Up @@ -106,7 +107,9 @@ ${process.argv[0]} ${process.execArgv.join(' ')} ${process.argv[1]}
useHostFilesystem(php);

if (hasDevtoolsOption && hasXdebugOption) {
const bridge = await startBridge({});
const bridge = await startBridge({
phpRoot: cwd(),
});

bridge.start();
}
Expand Down
8 changes: 8 additions & 0 deletions packages/php-wasm/compile/php-wasm-sapi-override/config.m4
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
dnl config.m4 for extension wasm_sapi_override

PHP_ARG_ENABLE(wasm_sapi_override, whether to enable wasm_sapi_override support,
[ --enable-wasm_sapi_override Enable wasm_sapi_override support])

if test "$PHP_wasm_sapi_override" != "no"; then
PHP_NEW_EXTENSION(wasm_sapi_override, wasm_sapi_override.c, $ext_shared)
fi
5 changes: 5 additions & 0 deletions packages/php-wasm/compile/php-wasm-sapi-override/config.w32
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ARG_ENABLE("wasm_sapi_override", "Enable wasm_sapi_override support", "no");

if (PHP_wasm_sapi_override == "yes") {
EXTENSION("wasm_sapi_override", "wasm_sapi_override.c");
}
135 changes: 135 additions & 0 deletions packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Allows changing the SAPI name at runtime.
*
* Affects:
* - php_sapi_name()
* - PHP_SAPI constant
*
* Usage:
* ```php
* set_sapi_name('wasm');
* ```
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "SAPI.h"
#include "zend_constants.h"
#include "ext/standard/info.h"
#include "wasm_sapi_override.h"

static char *set_sapi_name_original_name = NULL;
static char *set_sapi_name_prev_allocated = NULL;

/* {{{ proto bool set_sapi_name(string $new_name) */
PHP_FUNCTION(set_sapi_name)
{
char *new_name;
size_t new_len;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(new_name, new_len)
ZEND_PARSE_PARAMETERS_END();

/* --- overwrite sapi_module.name ------------------------------------- */
if (!set_sapi_name_original_name) {
set_sapi_name_original_name = sapi_module.name; /* remember for restore */
}

char *buf = pemalloc(new_len + 1, 1); /* persistent */
memcpy(buf, new_name, new_len);
buf[new_len] = '\0';

if (set_sapi_name_prev_allocated) {
pefree(set_sapi_name_prev_allocated, 1);
}
sapi_module.name = buf;
set_sapi_name_prev_allocated = buf;

/* --- update PHP_SAPI constant --------------------------------------- */
zend_string *const_name = zend_string_init("PHP_SAPI", sizeof("PHP_SAPI") - 1, 0);
zend_constant *c = zend_hash_find_ptr(EG(zend_constants), const_name);

zend_string *new_zstr = zend_string_init(new_name, new_len, 1); /* persistent */

if (c) {
if (Z_TYPE(c->value) == IS_STRING) {
zend_string *old = Z_STR(c->value);
/* Only release if it's not interned (interned strings live for entire process) */
if (!ZSTR_IS_INTERNED(old)) {
zend_string_release_ex(old, 1);
}
}
ZVAL_STR(&c->value, new_zstr);
} else {
zval zv;
ZVAL_STR(&zv, new_zstr);
zend_register_constant(&(zend_constant){
.name = const_name,
.value = zv,
});
}
zend_string_release_ex(const_name, 0);

RETURN_TRUE;
}
/* }}} */

/* arginfo */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_sapi_name, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, new_name, IS_STRING, 0)
ZEND_END_ARG_INFO()

/* function list */
static const zend_function_entry wasm_sapi_override_functions[] = {
PHP_FE(set_sapi_name, arginfo_set_sapi_name)
PHP_FE_END
};

/* MINIT / MSHUTDOWN */
static PHP_MINIT_FUNCTION(wasm_sapi_override) { return SUCCESS; }

static PHP_MSHUTDOWN_FUNCTION(wasm_sapi_override)
{
if (set_sapi_name_prev_allocated) {
pefree(set_sapi_name_prev_allocated, 1);
set_sapi_name_prev_allocated = NULL;
}
if (set_sapi_name_original_name) {
sapi_module.name = set_sapi_name_original_name;
}
return SUCCESS;
}

/* info */
static PHP_MINFO_FUNCTION(wasm_sapi_override)
{
php_info_print_table_start();
php_info_print_table_row(2, "set_sapi_name support", "enabled");
php_info_print_table_row(2, "version", PHP_WASM_SAPI_OVERRIDE_MODULE_VERSION);
php_info_print_table_row(2, "current SAPI", sapi_module.name);
php_info_print_table_end();
}

/* module entry */
zend_module_entry wasm_sapi_override_module_entry = {
STANDARD_MODULE_HEADER,
"wasm_sapi_override", /* Extension name */
wasm_sapi_override_functions, /* Function entries */
PHP_MINIT(wasm_sapi_override), /* PHP_MINIT - Module initialization */
PHP_MSHUTDOWN(wasm_sapi_override), /* PHP_MSHUTDOWN - Module shutdown */
NULL, /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(wasm_sapi_override), /* PHP_MINFO - Module info */
PHP_WASM_SAPI_OVERRIDE_MODULE_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_WASM_SAPI_OVERRIDE
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(wasm_sapi_override)
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* wasm_memory_storage extension for PHP */

#ifndef PHP_WASM_SAPI_OVERRIDE_H
# define PHP_WASM_SAPI_OVERRIDE_H

extern zend_module_entry wasm_sapi_override_module_entry;
# define phpext_wasm_sapi_override_ptr &wasm_sapi_override_module_entry

# define PHP_WASM_SAPI_OVERRIDE_MODULE_VERSION "0.0.1"

#endif /* PHP_WASM_SAPI_OVERRIDE_H */
4 changes: 4 additions & 0 deletions packages/php-wasm/compile/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ RUN git clone https://github.com/php/php-src.git php-src \
# Work around memory leak due to PHP using Emscripten's incomplete mmap/munmap support
COPY ./php-wasm-memory-storage /root/php-src/ext/wasm_memory_storage

# Allow changing the SAPI name at runtime
COPY ./php-wasm-sapi-override /root/php-src/ext/wasm_sapi_override

# Polyfill for dns functions
COPY ./php-wasm-dns-polyfill/ /root/php-src/ext/dns_polyfill

Expand Down Expand Up @@ -412,6 +415,7 @@ CURL_LIBS="-I/root/lib/lib -L/root/lib/lib" \
--enable-ctype \
--enable-tokenizer \
--enable-wasm_memory_storage \
--enable-wasm_sapi_override \
--enable-dns_polyfill \
--enable-post_message_to_js \
$(cat /root/.php-configure-flags)
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/supported-php-versions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* @property {string} lastRelease
*/

export const lastRefreshed = '2025-07-22T07:55:56.719Z';
export const lastRefreshed = '2025-07-24T23:47:22.787Z';

/**
* @type {PhpVersion[]}
Expand Down
36 changes: 14 additions & 22 deletions packages/php-wasm/universal/src/lib/php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ type MountObject = {
*/
export class PHP implements Disposable {
protected [__private__dont__use]: any;
#sapiName?: string;
#webSapiInitialized = false;
#wasmErrorsTarget: UnhandledRejectionsTarget | null = null;
#eventListeners: Map<string, Set<PHPEventListener>> = new Map();
Expand All @@ -77,6 +76,7 @@ export class PHP implements Disposable {
requestHandler?: PHPRequestHandler;
private cliCalled = false;
private runStreamCalled = false;
private sapiName = 'playground';

/**
* An exclusive lock that prevent multiple requests from running at
Expand Down Expand Up @@ -338,19 +338,8 @@ export class PHP implements Disposable {

/** @inheritDoc */
async setSapiName(newName: string) {
const result = this[__private__dont__use].ccall(
'wasm_set_sapi_name',
NUMBER,
[STRING],
[newName]
);
if (result !== 0) {
throw new Error(
'Could not set SAPI name. This can only be done before the PHP WASM module is initialized.' +
'Did you already dispatch any requests?'
);
}
this.#sapiName = newName;
this.sapiName = newName;
this.defineConstant('PLAYGROUND_SAPI_NAME', newName);
}

/**
Expand Down Expand Up @@ -591,6 +580,7 @@ export class PHP implements Disposable {
async () => {
if (!this.#webSapiInitialized) {
await this.#initWebRuntime();
this.setSapiName(this.sapiName);
this.#webSapiInitialized = true;
}
if (
Expand Down Expand Up @@ -1252,10 +1242,6 @@ export class PHP implements Disposable {
// Initialize the new runtime
this.initializeRuntime(runtime);

if (this.#sapiName) {
this.setSapiName(this.#sapiName);
}

// Copy the old /internal directory to the new filesystem
copyFS(oldFS, this[__private__dont__use].FS, '/internal');

Expand Down Expand Up @@ -1346,14 +1332,20 @@ export class PHP implements Disposable {
);
}

const sapiNameBefore = this.sapiName;
this.sapiName = 'cli';
return await this.#executeWithErrorHandling(() => {
return this[__private__dont__use].ccall('run_cli', null, [], [], {
async: true,
});
}).then((response) => {
response.exitCode.finally(release);
return response;
});
})
.then((response) => {
response.exitCode.finally(release);
return response;
})
.finally(() => {
this.setSapiName(sapiNameBefore);
});
}

setSkipShebang(shouldSkip: boolean) {
Expand Down
Binary file modified packages/php-wasm/web/public/php/jspi/8_0_30/php_8_0.wasm
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/php/jspi/php_8_0.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 9 additions & 13 deletions packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,17 @@ export async function startBridge(config: StartBridgeConfig) {
}

const getPHPFile = config.phpInstance
? (path: string) =>
new Promise<string>(() =>
config.phpInstance!.readFileAsText(path)
)
? (path: string) => config.phpInstance!.readFileAsText(path)
: config.getPHPFile
? config.getPHPFile
: (path: string) =>
new Promise<string>(() => {
// Default implementation: read from filesystem
// Convert file:/// URLs to local paths
const localPath = path.startsWith('file://')
? path.replace('file://', '')
: path;
return readFileSync(localPath, 'utf-8');
});
: (path: string) => {
// Default implementation: read from filesystem
// Convert file:/// URLs to local paths
const localPath = path.startsWith('file://')
? path.replace('file://', '')
: path;
return readFileSync(localPath, 'utf-8');
};

const phpFiles = getPhpFiles(phpRoot);
return new XdebugCDPBridge(dbgpSession, cdpServer, {
Expand Down
Loading
Loading