Skip to content

Emit a dispatch-free runtime.prepend.php from the php-builtin applier#278

Closed
epeicher wants to merge 1 commit into
trunkfrom
php-builtin-prepend-artifact
Closed

Emit a dispatch-free runtime.prepend.php from the php-builtin applier#278
epeicher wants to merge 1 commit into
trunkfrom
php-builtin-prepend-artifact

Conversation

@epeicher

@epeicher epeicher commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What it does

The php-builtin runtime applier now writes a second artifact, runtime.prepend.php, alongside runtime.php. It contains the same environment setup (DB constants, the lazy SQLite $wpdb proxy, the uploads proxy) without the CLI-server routing tail that dispatches WordPress.

runtime.php and start.sh are unchanged, so standalone php -S usage is unaffected.

Rationale

runtime.php glues together two unrelated jobs: (1) environment setup and (2) a request router that requires WordPress. Both are correct when reprint owns the server (start.shphp -S … runtime.php).

But WordPress Studio's native-PHP pull-reprint runs its own php -S … router.php workers and injects reprint's environment via -d auto_prepend_file=runtime.php. auto_prepend_file is a per-request bootstrap slot, not a dispatch slot — so the routing tail dispatches WordPress during the prepend phase, the SQLite $wpdb shim no longer governs the request that actually serves the page, WordPress falls back to the real MySQL wpdb, and every pulled site dies at startup with "Error establishing a database connection" (the worker exits 1).

Studio currently works around this by string-stripping the router tail off runtime.php downstream. This change supplies the clean artifact instead. It mirrors the existing nginx-fpm applier, which already ships a routing-free runtime.php consumed via auto_prepend_file.

Implementation

  • PhpBuiltinApplier::apply() computes the routing-free base once, then writes runtime.php = base + routing tail (unchanged) and runtime.prepend.php = base only.
  • Fixes the generate_cli_server_routing() docblock, which falsely claimed the routing tail was guarded by a php_sapi_name check. It runs unconditionally — and php_sapi_name() is cli-server in both modes, so it could not distinguish them anyway.
  • README runtime-output table lists the new file.

New test PhpBuiltinPrependArtifactTest covers:

  • both files are written; runtime.php keeps the routing tail; runtime.prepend.php omits it but is the exact base prefix of runtime.php
  • both artifacts are php -l-clean
  • behavioral: running php -d auto_prepend_file=runtime.prepend.php <probe> resolves $GLOBALS['wpdb'] to the SQLite loader with no dispatch and no DB error — exactly Studio's injection path

Testing instructions

TBD

The php-builtin applier wrote a single runtime.php that concatenates the
routing-free base layers (constants, SQLite $wpdb shim, uploads proxy) with
a CLI-server routing tail that dispatches WordPress (require index.php). That
file is correct as a standalone `php -S` router, but when a host owns its own
router and injects runtime.php via auto_prepend_file, the tail dispatches
WordPress during the prepend phase, bypassing the SQLite shim and falling back
to MySQL — "Error establishing a database connection".

Emit runtime.prepend.php alongside runtime.php: the same base layers with no
routing tail, purpose-built for hosts that own request dispatch (e.g. Studio's
native php -S router). runtime.php and start.sh are unchanged, so standalone
usage is unaffected. This mirrors the existing nginx-fpm applier, which already
consumes the base-only output via auto_prepend_file.

Also fix the generate_cli_server_routing() docblock, which falsely claimed the
routing tail was guarded by a php_sapi_name check — it runs unconditionally,
and php_sapi_name() is 'cli-server' in both modes so it could not distinguish
them anyway.
@github-actions

Copy link
Copy Markdown
Contributor

Pull pipeline performance — large-directory

Site: large-directory · 2,000+ plus targeted file-transfer scenarios files · 10,000 posts · 25,000 postmeta · PHP 8.5.7

Stage PR trunk Δ Status Details
playground-sqlite-db-pull 9.36 s 9.81 s ⚪ -451 ms (-4.6%) condition=db-pull in PHP.wasm
runtime=php.wasm 8.3
wp_mysql_parser=enabled
mode=lexer
native_lexer=verified
native_token_stream=WP_MySQL_Native_Token_Stream
native_token_count=18
native_parser=selected
trunk: condition=db-pull in PHP.wasm
runtime=php.wasm 8.3
wp_mysql_parser=enabled
mode=lexer
native_lexer=verified
native_token_stream=WP_MySQL_Native_Token_Stream
native_token_count=18
native_parser=selected
playground-sqlite-db-apply 3.56 s 3.64 s ⚪ -88 ms (-2.4%) condition=db-apply to SQLite in PHP.wasm
runtime=php.wasm 8.3
wp_mysql_parser=enabled
mode=parser
native_lexer=verified
native_token_stream=WP_MySQL_Native_Token_Stream
native_token_count=18
native_parser=verified
native_ast=WP_MySQL_Native_Parser_Node
sqlite_driver_parser=verified
trunk: condition=db-apply to SQLite in PHP.wasm
runtime=php.wasm 8.3
wp_mysql_parser=enabled
mode=parser
native_lexer=verified
native_token_stream=WP_MySQL_Native_Token_Stream
native_token_count=18
native_parser=verified
native_ast=WP_MySQL_Native_Parser_Node
sqlite_driver_parser=verified
Total 12.92 s 13.46 s ⚪ -540 ms (-4.0%)

Numbers carry runner noise; treat single-run deltas as directional, not authoritative.

📈 Trunk performance history — commit-by-commit timeline.

@epeicher

Copy link
Copy Markdown
Collaborator Author

Closing this PR as not required, the changes are already in Reprint, the solution was just to use nginx-fpm applier. This was done as part of this change.

The reason this was not identified before:

I had the answer in my hands and walked past it. My analysis explicitly called out the nginx-fpm applier as "the precedent" and "exactly the contract Studio needs" — but I used it only as a justification to add a new artifact to php-builtin, instead of taking the one extra step: just ask reprint for the target that already produces it.

Three things led me there:

  • I anchored on the runtime target as fixed. --runtime=php-builtin was already in the code and the name looked like the obvious match for "PHP built-in server," so I framed the whole problem as "fix php-builtin's output" (downstream strip, or upstream prepend artifact) and never questioned whether it was the right target to request in the first place. The lever I overlooked is that Studio chooses which --runtime to ask for.
  • I let the label deter the reuse. "nginx-fpm" sounds wrong for a php -S server, so I treated it as a model to copy rather than a target to reuse — even though functionally the contract is identical and the name is incidental.
  • I optimized for the "clean seam" over the smallest change. I was reasoning about where responsibility should live (reprint = glue, Studio = routing) and got attracted to adding a "proper" artifact/target in reprint. The simpler question to ask was "which existing target already emits what we need?", and that led to a one-line fix.

That's a real miss, not a toolchain issue: the cheaper hypothesis ("change one argument") was reachable from my own findings, and I should have surfaced it as option 0 before recommending a reprint change.

@epeicher epeicher closed this Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant