Skip to content

PHP SAPI module #107

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

Merged
merged 26 commits into from
Jan 12, 2023
Merged

PHP SAPI module #107

merged 26 commits into from
Jan 12, 2023

Conversation

adamziel
Copy link
Collaborator

@adamziel adamziel commented Jan 11, 2023

What is this PR all about?

Introduces a PHP SAPI module that enables setting $_POST, $_SERVER, php://input and all the other PHP values from JavaScript.

What problem does it solve?

Before this PR, most superglobal values were set by prepending code snippets like $_SERVER['DOCUMENT_ROOT'] = ${JSON.stringify(documentRoot)}; every time some code was evaluated. Unfortunately, that technique couldn't populate everything, e.g. php://input remained empty.

How does it work?

PHP SAPI is used to integrate PHP with webservers and runtimes. A few SAPIs you might be familiar with are php-cgi, php-fpm, and php-cli. A SAPI consumes the request information from the runtime, passes it to PHP, triggers the code execution, and passes the response back to the runtime.

This PR introduces a WASM SAPI that accepts input information from JavaScript, sets up a PHP request, and passes a response back to JS. The most important changes are in the php_wasm.c file. The rest of the PR is adjusting the existing codebase to the new way of working with PHP.

Briefly speaking, the SAPI module exposes a few setters like wasm_set_query_string or wasm_add_SERVER_entry and a wasm_sapi_handle_request() function that triggers the request execution. The output information are written to /tmp/stdout, /tmp/stderr, and /tmp/headers.json by the C module and read by the PHP JavaScript class.

Because the request body and the query string are parsed by the same PHP internal functions as they would on a webserver, array syntax like settings[newsletter]=1 is handled correctly.

One surprising thing is the ability to set arbitrary $_FILES entries with wasm_add_uploaded_file. This is because JavaScript typically has access to any uploaded File objects and it would be wasteful to re-serialize them only so that PHP can parse them all over again. With wasm_add_uploaded_file you can first write the uploaded files to the filesystem and then simply let PHP know about their existence.

Solves #103

@adamziel
Copy link
Collaborator Author

PHP builds regenerated in 3a175a2

bgrgicak added a commit that referenced this pull request May 7, 2025
## Motivation for the change, related issues

Today, running the CLI apps with Bun is fast, but running with Node.js
requires completing the build process which is much slower. I would like
a faster update/run/debug loop when working on `@php-wasm/node`,
especially for the XDebug work.

In addition, if we can run unbuilt code directly in Node, we can debug
that code in VSCode/Cursor and hopefully use the [WebAssembly DWARF
Debugging](https://marketplace.visualstudio.com/items?itemName=ms-vscode.wasm-dwarf-debugging)
VSCode extension to step-debug WebAssembly in the editor.

## Implementation details

This PR adds:
- New "debug" and "dev-node" (watch) nx targets for `@php-wasm/cli` that
run in Node.js. Try `npx nx debug php-wasm-cli -- -i`. It starts quickly
with no build process.
- A "Debug PHP-WASM CLI" launch target for VSCode/Cursor. Just set a
breakpoint and run the target.

I tried running with packages like `ts-node` and `tsx` but did not
manage to get any of them to work. So I tried with Node.js
type-stripping and was able to get it working.

Unfortunately, there are a lot of adjustments needed to run directly in
Node.js this way. Most of the changes were small and unoffensive IMO,
but some, like the need to avoid decorator syntax for now, were more
annoying.

Here's a list of the kinds of changes:
- Add an ESM module loader for Node to resolve our package aliases to
paths within the repo.
- It might be possible for us to use a prebuilt package for this like
[tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths), but I
wasn't able to get this to work properly.
- The custom loader turned out to be fairly direct and a lot less
trouble.
- ~Upgrade TypeScript to get the `--erasableSyntaxOnly` option to
enforce Node.js type-stripping compatibility.~
- NOTE: This update was backed out due to type conflicts between TS 5.8,
`@typings/node`, and our current Vite version. This means we don't have
linting checks to require only "erasable" types (types that can be
stripped by Node.js), but this PR still allows our CLI packages to run
in Node.js without a build step.
- Adjust our TypeScript to avoid language features that break
type-stripping. Docs
[here](https://nodejs.org/docs/latest-v23.x/api/typescript.html#typescript-features).
  - Avoid enum declarations
  - Avoid type namespaces
  - Avoid parameter properties
- Avoid decorator syntax for now.
- Decorators are not supported by Node's type-stripping. Docs
[here](https://nodejs.org/docs/latest-v23.x/api/typescript.html#typescript-features).
- > Since Decorators are currently a [TC39 Stage 3
proposal](https://github.com/tc39/proposal-decorators) and will soon be
supported by the JavaScript engine, they are not transformed and will
result in a parser error. This is a temporary limitation and will be
resolved in the future.
- The workaround is to manually apply decorators. It is awkward, but
thankfully, fs-helpers.ts is the only place I see using @decorator
syntax.
- Make type-only imports explicit. This is enforced by the
[consistent-type-imports](https://typescript-eslint.io/rules/consistent-type-imports/)
rule, and making the changes was automated using that rules fixer. (Just
`nx run-many --all --target=lint --fix`)
- We don't have to enforce this everywhere, but I think selective
enforcement would be more confusing.
- Add some Node.js boilerplate to the `@php-wasm/node` php_x_y.js files:
  - Define our own `__dirname` constant
- Explicitly create a `require()` function since Emscripten uses
require() in the generated code. Using the default, global require()
triggers an error when used within an ES module, but the created
require() appears to work fine and is the recommended fix.
- Other small tweaks like adjusting how we resolve the default php.ini
path for `@php-wasm/cli`.

There are a few things left to do:
- [x] Support importing raw file content in the custom loader. This is
required by Playground CLI (for example,
[here](https://github.com/Automattic/wordpress-playground-private/blob/b0ebbd963aedabf5adf4bd70d2e128e21c4fe682/packages/playground/blueprints/src/lib/steps/define-wp-config-consts.ts#L4)).
- [x] Add nx related targets for Playground CLI
- [x] Prompt for PHP version before launching CLI's in Debug
- [x] Prompt for args before launch CLI's in Debug mode
- [x] Fix remaining test failures

## Testing Instructions (or ideally a Blueprint)

- CI
- Run `npx nx debug php-wasm-cli` and see that it works to run the CLI
(it also enables debugging, but that will be tested with the VSCode
launch config)
- Run `npx nx dev-node php-wasm-cli`, see that it works to run the CLI,
and confirm that it reruns when relevant TS modules are changed.
- Run `npx nx debug playground-cli` and see that it works to run the CLI
(it also enables debugging, but that will be tested with the VSCode
launch config)
- Test the "Debug PHP-WASM CLI" launch config in VSCode or Cursor
  - Set a breakpoint in php-wasm/cli
  - Run the target
  - Confirm you hit the breakpoint
- Test the "Debug Playground CLI" launch config in VSCode or Cursorr
  - Set a breakpoint in php-wasm/cli
  - Run the target
  - Confirm you hit the breakpoint

---------

Co-authored-by: Bero <[email protected]>
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.

1 participant