close
Skip to content

[CLI] php command to run PHP scripts#2641

Merged
adamziel merged 8 commits intotrunkfrom
draft-php-command
Mar 12, 2026
Merged

[CLI] php command to run PHP scripts#2641
adamziel merged 8 commits intotrunkfrom
draft-php-command

Conversation

@adamziel
Copy link
Collaborator

@adamziel adamziel commented Sep 17, 2025

Motivation for the change, related issues

Adds a new CLI command called php that can run PHP scripts using the PHP CLI SAPI.

Try it locally as follows:

mkdir tmp
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
cp wp-cli.phar tmp
node --no-warnings=ExperimentalWarning  --experimental-strip-types --experimental-transform-types --import ./packages/meta/src/node-es-module-loader/register.mts ./packages/playground/cli/src/cli.ts --php=8.0 --mount-before-install=./tmp:/wordpress --skip-wordpress-setup -- php /wordpress/wp-cli.phar user list

You should see:

Starting a PHP server...
Setting up WordPress latest
Resolved WordPress release URL: https://downloads.w.org/release/wordpress-6.8.2.zip
Fetching SQLite integration plugin...
Booting WordPress...
Booted!
Running the Blueprint...
Running the Blueprint – 100%
Finished running the blueprint
+----+------------+--------------+---------------------+---------------------+---------------+
| ID | user_login | display_name | user_email          | user_registered     | roles         |
+----+------------+--------------+---------------------+---------------------+---------------+
| 1  | admin      | admin        | admin@localhost.com | 2025-07-15 23:08:50 | administrator |
+----+------------+--------------+---------------------+---------------------+---------------+

The part after the double dash is the php command to run, in this case it's php /wordpress/wp-cli.phar user list

Discussion

Developer experience

The command accepts all the same parameters as the server command. This means two things:

  • It is a powerful low-level tool that can reproduce the same PHP runtime as the server command.
  • It is very annoying to use.

Let's discuss how can we simplify the experience without adding implicit magic.

Paths

All the paths are VFS paths, see how I'm running php /wordpress/wp-cli.phar and not php ./wp-cli.phar. I'd love to be able to run php ./wp-cli.phar --import-wxr=./wxr.xml and such – is there any sensible way of making it happen?

Using only host paths isn't an option. The complete site directory structure only exists in VFS. I can't think of a portable way of re-creating /wordpress on host. Even with native mounts in place, there may be 15 different mounts in VFS /wordpress that are not there on host and that we cannot easily overlay on host.

Mixing host paths and WordPress paths feels super risky. Imagine we magically had a way to resolve both /wordpress/wp-cli.phar and /Users/adam/www/wp/wp-cli.phar. Are we better off in any way now? What's cwd and how do we calculate the path when wp-cli command refers to ../imports/wxr.xml? What if both VFS and host have different files under that path? How would rename etc. work?

That leaves us with only one option – using only VFS paths like we would do in Docker. This complicates things for the package consumer – we need to train them to mount their PHP scripts and source data like they'd do with Docker.

Can anyone see any alternatives?

Auto-download wp-cli.phar

We could ship two commands: php that lets you run any command, and wp that downloads and runs wp-cli.phar specifically. That way we'd save folks from typing that same one mount every single time.

cc @JanJakes @zaerl @brandonpayton @Jameswlepage @akirk

@adamziel adamziel requested a review from a team September 17, 2025 15:46
@brandonpayton
Copy link
Member

@adamziel I think it would be great to get this in, and I've started reviewing this with Claude. Tomorrow may be too busy, but I wanted to let you know that I'm looking at this.

@JanJakes
Copy link
Member

@adamziel This is so cool! There was a typo in the testing command (-- php=8.0 instead of --php=8.0); I fixed it.

Fiddling around, it got me thinking... What would it take to support the PHP interactive shell? I mean like:

$ php -a
Interactive shell

php > echo 'test';
test

Although I'm not sure if that's usable for WP CLI at all.

@adamziel
Copy link
Collaborator Author

adamziel commented Jan 19, 2026

@JanJakes thank you for fixing the typo!

Supporting interactive shell would take either:

  1. Create readline/libedit bindings for Node.js. I've tried that in the past and it seems like we'd need a native C module for that. It sounds scary, but fs-ext-prebuilt shows it's not that difficult.
  2. Build our own PHP shell, perhaps more like ipython with multiline entry and syntax coloring.
  3. Reuse psysh or another existing PHP shell. We may or may not still need readline bindings.

Number 3 would likely give us the most benefit for the effort, but if it still requires those bindings, then 2 may give us more benefits with less work.

Rebases PR #2641 onto current trunk. Introduces a new CLI command
'php' that runs PHP scripts using the PHP CLI SAPI via a new
PlaygroundCliWorker base class.

Changes:
- Add playground-cli-worker.ts with PlaygroundCliWorker base class
  containing runCLIScript() and dispose() methods
- Both V1 and V2 worker classes now extend PlaygroundCliWorker
- Add 'php' command to yargs CLI configuration
- Add nargs:1 to mount options to support '--' separator for php args
Tests verify that the php command:
- Runs inline PHP scripts and captures stdout output
- Exits with the correct non-zero exit code on PHP errors
- Captures stderr output from error_log()
@brandonpayton
Copy link
Member

@adamziel I had Claude take a look at this to resolve conflicts, refine, and add tests. I haven't reviewed the changes yet but will go ahead and push them. If they get in the way or are otherwise troublesome, we can revert them.

@adamziel
Copy link
Collaborator Author

adamziel commented Mar 6, 2026

@copilot lint

Copy link
Contributor

Copilot AI commented Mar 6, 2026

@adamziel I've opened a new pull request, #3349, to work on those changes. Once the pull request is ready, I'll request review from you.

The `process.exit` parameter accepts `string | number | null | undefined`,
not just `number | undefined`. Widen the mock parameter type to fix TS2345.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 6, 2026 11:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new php subcommand to the Playground CLI to execute PHP scripts via the PHP CLI SAPI, reusing the existing runtime/mount/server setup options.

Changes:

  • Adds php command wiring in argument parsing and command dispatch.
  • Introduces a shared PlaygroundCliWorker base class with runCLIScript() and shared disposal behavior.
  • Adds Vitest coverage for running PHP, exit codes, and stderr capture.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/playground/cli/src/run-cli.ts Registers php command and dispatches to worker to run a PHP CLI invocation
packages/playground/cli/src/playground-cli-worker.ts Adds a shared worker base with runCLIScript() piping stdout/stderr and a comlink-callable dispose()
packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts Refactors v1 worker to extend shared PlaygroundCliWorker
packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts Refactors v2 worker to extend shared PlaygroundCliWorker
packages/playground/cli/tests/run-cli.spec.ts Adds tests for the new php command behavior

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1501 to +1505
// Wait until the next tick before exiting to
// ensure the output is flushed.
await new Promise((resolve) => setTimeout(resolve, 0));
await disposeCLI();
process.exit(exitCode);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This exit/cleanup sequence can drop output: runCLIScript() currently starts piping stdout/stderr but (in the new worker implementation) does not await those pipes finishing. That means disposeCLI() can terminate workers while output is still in-flight, and the setTimeout(0) tick is not a reliable flush. Make runCLIScript() only resolve after stdout/stderr piping completes (or otherwise signal completion), and then you can remove the timing-based flush workaround.

Suggested change
// Wait until the next tick before exiting to
// ensure the output is flushed.
await new Promise((resolve) => setTimeout(resolve, 0));
await disposeCLI();
process.exit(exitCode);
await disposeCLI();
process.exitCode = exitCode;
return;

Copilot uses AI. Check for mistakes.
claude and others added 5 commits March 6, 2026 12:48
- In `runCLIScript`, await both `pipeTo()` promises alongside `exitCode`
  via `Promise.all` so the method only resolves after stdout/stderr are
  fully drained.
- Remove the `setTimeout(0)` flush workaround in the `php` command
  handler — streams are now properly drained by `runCLIScript`. Keep
  `process.exit()` as a hard cut-off to prevent Node from hanging on
  open handles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the PlaygroundCliWorker intermediary class and restore the
original type union. The runCLIScript method is replaced with inline
stream handling that properly awaits pipeTo() promises via Promise.all,
eliminating the setTimeout workaround for output flushing.
@adamziel adamziel merged commit 7626936 into trunk Mar 12, 2026
43 checks passed
@adamziel adamziel deleted the draft-php-command branch March 12, 2026 15:15
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.

6 participants