Skip to content

Env: Add an experimental PHP runtime - #85512

Open
ellatrix wants to merge 3 commits into
trunkfrom
add/wp-env-php-runtime
Open

ellatrix wants to merge 3 commits into
trunkfrom
add/wp-env-php-runtime

Conversation

@ellatrix

@ellatrix ellatrix commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

What?

Follow up to #74609.

Adds an experimental php runtime to wp-env: wp-env start --runtime=php serves WordPress with the PHP installed on the computer, using PHP's built-in web server and a SQLite database. No Docker.

Why?

Docker is slow to start and heavy to run, especially with one site per worktree. The Playground runtime avoids Docker, but runs PHP as WebAssembly, which is noticeably slower: in a local comparison, the paragraph and writing flow e2e specs took 57 seconds on PHP's built-in server and 2.1 minutes on Playground. Most WordPress developers already have PHP installed.

How?

A third runtime next to docker and playground, in packages/env/lib/runtime/php/:

  • start downloads the configured sources with the existing download code, plus the SQLite Database Integration plugin. The first time, it copies WordPress core into php/ in the work directory (copy-on-write where the file system supports it), links the plugins, themes and mappings into it, and installs WordPress. Later starts only rewrite wp-config.php, so a new WP_ENV_PORT takes effect, and start the server. --update downloads the sources again and refreshes the core files, keeping the database.
  • A mapping inside another mapping, at any depth, is linked inside the site folder, never inside the folder the outer mapping points to.
  • Start waits for PHP's own "Development Server started" line, and fails with PHP's log when the port is busy.
  • The database lives in php-database/, outside the folder the server serves. A copy is kept after installation, and reset restores it.
  • The server runs with 8 workers and the same upload limits as the Docker runtime.
  • logs shows the PHP server's log, and follows it with --watch. run is not supported, like Playground. Multisite fails with a clear error. Xdebug, SPX, phpMyAdmin and phpVersion are not supported yet and give a warning.

Testing Instructions

  1. Make sure php with pdo_sqlite is on your PATH.
  2. Run npm run wp-env-test -- start --runtime=php. It should start in a few seconds.
  3. Run WP_BASE_URL=http://localhost:8889 npm run test:e2e -- --project=chromium test/e2e/specs/editor/blocks/paragraph.spec.js test/e2e/specs/editor/various/writing-flow.spec.js test/e2e/specs/editor/blocks/image.spec.js. Locally: 82 passed, 1 skipped.
  4. Run npm run wp-env-test -- reset, stop, then WP_ENV_PORT=8890 npm run wp-env-test -- start --runtime=php and check the site at the new port.

Use of AI Tools

Written with Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

Serves WordPress with the installed PHP's built-in web server and a SQLite
database, without Docker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the [Tool] Env /packages/env label Oct 25, 2026
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

Preview in WordPress Playground

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ellatrix <ellatrix@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

📦 Bundle size

Size Change: 0 B

Total Size: 9.06 MB

2a03bf9 Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric a7ce769 trunk % Change
timeToFirstByte 31.4 ms +13.69% -3.34% 32 ms +7.34% -2.34% -1.88%
largestContentfulPaint 58 ms +3.45% -10.34% 56 ms +7.14% -3.57% 3.57%
lcpMinusTtfb 22.3 ms +25.34% -2.69% 24 ms +8.33% -7.71% -7.08%
wpBeforeTemplate 16.85 ms +21.54% -1.78% 17.27 ms +0.81% -2.26% -2.43%
wpTemplate 11.47 ms +9.76% -2.27% 11.97 ms +4.43% -3.17% -4.18%
wpTotal 28.59 ms +14.45% -2.94% 29.27 ms +7.86% -2.73% -2.32%
wpMemoryUsage 7.68 MB +0% -0% 7.65 MB +0% -0% 0.43%
wpDbQueries 16 +0% -0% 16 +0% -0% 0%

front-end-classic-theme

Metric a7ce769 trunk % Change
timeToFirstByte 24.75 ms +7.88% -1.41% 27 ms +7.04% -1.3% -8.33%
largestContentfulPaint 56 ms +7.14% -0% 64 ms +3.13% -6.25% -12.5%
lcpMinusTtfb 31.7 ms +10.41% -1.42% 35.95 ms +3.2% -5.98% -11.82%
wpBeforeTemplate 15.41 ms +3.37% -0.65% 15.29 ms +2.68% -1.24% 0.78%
wpTemplate 7.03 ms +4.69% -2.84% 9.42 ms +9.24% -2.34% -25.37%
wpTotal 22.4 ms +9.46% -1.12% 24.6 ms +7.11% -0.57% -8.94%
wpMemoryUsage 6.26 MB +0% -0% 6.34 MB +0% -0% -1.22%
wpDbQueries 9 +0% -0% 13 +0% -0% -30.77%

media-processing

Metric a7ce769 trunk % Change
mediaProcessingJpeg 215.48 ms +0.8% -1.86% 216.36 ms +0.21% -1.69% -0.41%
mediaProcessingAvif 3488.31 ms +0.38% -0.62% 3472.64 ms +0.21% -0.86% 0.45%
mediaProcessingJpegToAvif 2432.81 ms +0.21% -0.18% 2419.03 ms +0.96% -0.1% 0.57%

media-upload

Metric a7ce769 trunk % Change
jpegUploadProcessing 892.15 ms +55.77% -1.37% 886.32 ms +1.29% -0.6% 0.66%
pngUploadProcessing 152.12 ms +3.88% -4.75% 151.37 ms +3.94% -4.67% 0.5%
largeJpegUploadProcessing 879.81 ms +0.67% -1.07% 892.36 ms +0.14% -1.28% -1.41%
multipleImageUploadProcessing 988.68 ms +12.31% -0.56% 1390.54 ms +2.43% -28.56% -28.9%

post-editor

Metric a7ce769 trunk % Change
serverResponse 518.22 ms +2.85% -5.7% 522.69 ms +4.62% -8.57% -0.86%
firstPaint 337.38 ms +39.76% -13.04% 370.66 ms +39.48% -38.66% -8.98%
domContentLoaded 1204.17 ms +3.68% -4.26% 1201.77 ms +1.77% -1.83% 0.2%
loaded 1205.78 ms +3.65% -4.27% 1203.03 ms +1.76% -1.85% 0.23%
firstContentfulPaint 508.81 ms +4.75% -7.92% 517.19 ms +1.19% -3.93% -1.62%
firstBlock 3419.53 ms +4.07% -1.57% 3440.45 ms +2.59% -2.49% -0.61%
type 16.98 ms +3.95% -3.18% 18.41 ms +1.63% -6.63% -7.77%
typeWithoutInspector 17.72 ms +4.97% -7.45% 17.57 ms +3.81% -1.88% 0.85%
typeWithTopToolbar 22.55 ms +4.39% -4.7% 23.54 ms +3.36% -1.32% -4.21%
typeContainer 7.91 ms +10.62% -7.33% 8.31 ms +5.54% -13.6% -4.81%
focus 122.85 ms +11.27% -2.47% 114.88 ms +7.23% -2.72% 6.94%
firstFocus 224.08 ms +0% -0% 221.03 ms +0% -0% 1.38%
selectAll 543.3 ms +1.58% -2.13% 551.1 ms +1.59% -2.92% -1.42%
listViewOpen 77.87 ms +7.8% -9.43% 78.45 ms +5.06% -3.76% -0.74%
inserterOpen 42.21 ms +9.52% -11.44% 38.99 ms +6.51% -3.05% 8.26%
inserterHover 11.23 ms +10.6% -6.14% 11.36 ms +8.27% -11.36% -1.14%
inserterSearch 8.42 ms +6.18% -7.72% 8 ms +9.38% -6.25% 5.25%
loadPatterns 629.3 ms +4.4% -2.66% 625.37 ms +4.36% -6.21% 0.63%
wpTotal 508.35 ms +3.04% -5.68% 512.26 ms +4.86% -8.58% -0.76%
wpMemoryUsage 14.71 MB +0% -0% 14.68 MB +0% -0% 0.24%
wpDbQueries 54.5 +0.92% -0.92% 55 +0% -1.82% -0.91%

site-editor

Metric a7ce769 trunk % Change
serverResponse 478.94 ms +5.07% -4.13% 483.17 ms +6.34% -1.3% -0.88%
firstPaint 294.07 ms +15.01% -11.3% 286.89 ms +12.35% -9.9% 2.5%
domContentLoaded 1277.52 ms +0.71% -0.98% 1256.48 ms +1.88% -0.37% 1.67%
loaded 1278.7 ms +0.72% -0.98% 1257.66 ms +1.89% -0.37% 1.67%
firstContentfulPaint 515.2 ms +1.9% -1.12% 504.07 ms +3.65% -3.45% 2.21%
firstBlock 4554.51 ms +0.71% -0.32% 4535.1 ms +1.31% -0.99% 0.43%
type 18.06 ms +5.59% -2.6% 17.86 ms +1.68% -5.94% 1.12%
navigate 114.36 ms +6.22% -4.44% 115.06 ms +11.06% -4.47% -0.61%
loadPatterns 1254.58 ms +14.88% -4.95% 1213.97 ms +6.15% -6.32% 3.35%
loadPages 975.7 ms +2.86% -4.23% 942.92 ms +5.62% -1.35% 3.48%
wpTotal 469.49 ms +5.27% -4.1% 473.88 ms +6.56% -1.37% -0.93%
wpMemoryUsage 12.73 MB +0% -0% 12.70 MB +0% -0% 0.24%
wpDbQueries 44 +2.27% -0% 44 +2.27% -0% 0%

2a03bf9 Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

Custom Completer: should allow newlines after multiple option completions in /test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 1
+ Received  + 1

@@ -1,7 +1,7 @@
  <!-- wp:paragraph -->
- <p>🫐 test</p>
+ <p>🍎 test</p>
  <!-- /wp:paragraph -->

  <!-- wp:paragraph -->
  <p>🫐 test</p>
  <!-- /wp:paragraph -->

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js:466:6
can apply custom gradient background color in /test/e2e/specs/editor/blocks/buttons.spec.js, passed after 1 failed attempt.
Error: expect(received).toBe(expected) // Object.is equality

- Expected  - 1
+ Received  + 1

  <!-- wp:buttons -->
- <div class="wp-block-buttons"><!-- wp:button {"style":{"background":{"gradient":"linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,255,0) 100%)"}}} -->
+ <div class="wp-block-buttons"><!-- wp:button {"style":{"background":{"gradient":"linear-gradient(135deg,rgb(6,147,227) 0%,rgb(0,255,0) 100%)"}}} -->
  <div class="wp-block-button"><a class="wp-block-button__link wp-element-button">Content</a></div>
  <!-- /wp:button --></div>
  <!-- /wp:buttons -->
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/buttons.spec.js:411:21
inserts a block in proper place after having clicked `Browse All` from inline inserter in /test/e2e/specs/editor/various/inserting-blocks.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for getByRole('button', { name: 'Add block' })
    - locator resolved to <button type="button" aria-pressed="false" aria-haspopup="true" aria-expanded="false" aria-label="Add block" class="components-button block-editor-inserter__toggle is-next-40px-default-size has-icon">…</button>
  - attempting click action
    - waiting for element to be visible, enabled and stable
    - element is visible, enabled and stable
    - scrolling into view if needed
  - element was detached from the DOM, retrying

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/inserting-blocks.spec.js:557:5
moves selected text into another paragraph in /test/e2e/specs/editor/various/draggable-blocks.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for locator('[name="editor-canvas"]').contentFrame().getByRole('document', { name: 'Block: Paragraph' }).first()

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/draggable-blocks.spec.js:1156:29
should change the indented list type in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for locator('role=toolbar[name="Block tools"i]').locator('role=button[name="Indent"]')
    - locator resolved to <button id="_r_1b_" type="button" tabindex="-1" aria-label="Indent" aria-disabled="true" data-toolbar-item="true" data-truly-disabled="false" aria-describedby="components-button__description-19" class="components-button components-toolbar-button is-compact has-icon">…</button>
  - attempting click action
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
    - waiting 20ms
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
      - waiting 100ms
    18 × waiting for element to be visible, enabled and stable
       - element is not enabled
     - retrying click action
       - waiting 500ms

    at Editor.clickBlockToolbarButton (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/editor/click-block-toolbar-button.ts:17:15)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:727:3
should merge a following sibling list into the outermost list with Delete from a nested item (#77245) in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

@@ -8,11 +8,11 @@
          "innerBlocks": Array [
            Object {
              "innerBlocks": Array [
                Object {
                  "attributes": Object {
-                   "content": "b‸",
+                   "content": "b",
                  },
                  "name": "core/list-item",
                },
              ],
              "name": "core/list",

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:2854:41
should outdent with children in /test/e2e/specs/editor/blocks/list.spec.js, passed after 1 failed attempt.
TimeoutError: locator.click: Timeout 10000ms exceeded.
Call log:
  - waiting for locator('role=toolbar[name="Block tools"i]').locator('role=button[name="Indent"]')
    - locator resolved to <button id="_r_1l_" type="button" tabindex="-1" aria-label="Indent" aria-disabled="true" data-toolbar-item="true" data-truly-disabled="false" aria-describedby="components-button__description-26" class="components-button components-toolbar-button is-compact has-icon">…</button>
  - attempting click action
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
    - waiting 20ms
    2 × waiting for element to be visible, enabled and stable
      - element is not enabled
    - retrying click action
      - waiting 100ms
    18 × waiting for element to be visible, enabled and stable
       - element is not enabled
     - retrying click action
       - waiting 500ms

    at Editor.clickBlockToolbarButton (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/editor/click-block-toolbar-button.ts:17:15)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/blocks/list.spec.js:1055:3
should preserve tab order while changing active tab in /test/e2e/specs/editor/various/sidebar.spec.js, passed after 1 failed attempt.
Error: expect(locator).toBeFocused() failed

Locator:  getByRole('region', { name: 'Editor settings' }).getByRole('tab', { selected: true })
Expected: focused
Received: inactive
Timeout:  5000ms

Call log:
  - Expect "toBeFocused" getByRole('region', { name: 'Editor settings' }).getByRole('tab', { selected: true }) with timeout 5000ms
  - waiting for getByRole('region', { name: 'Editor settings' }).getByRole('tab', { selected: true })
    14 × locator resolved to <button role="tab" tabindex="0" type="button" data-active="" id="base-ui-_r_p_" aria-selected="true" aria-disabled="false" data-orientation="horizontal" data-composite-item-active="" class="a5fd8814f195aa5e__tab" aria-controls="base-ui-_r_r_" data-activation-direction="none">…</button>
       - unexpected value "inactive"

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/sidebar.spec.js:92:29
should replace selected non-breaking spaces from the toolbar in /test/e2e/specs/editor/various/rich-text.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

  Array [
    Object {
      "attributes": Object {
-       "content": "one two three",
+       "content": " two three",
      },
      "name": "core/paragraph",
    },
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/rich-text.spec.js:1050:41
should replace the selection across blocks with an inline image in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 1
+ Received  + 1

@@ -1,9 +1,9 @@
  Array [
    Object {
      "attributes": Object {
-       "content": "ab",
+       "content": "b",
      },
      "name": "core/paragraph",
    },
    Object {
      "attributes": Object {

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:2372:41
should select all with formatting in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
Error: expect(received).toMatchObject(expected)

- Expected  - 2
+ Received  + 6

  Array [
    Object {
-     "name": "core/paragraph",
+     "attributes": Object {
+       "content": "<strong>b</strong>",
+       "dropCap": false,
      },
-   Object {
+     "clientId": "cd2ffa93-667a-4c80-9d97-6a9db80c26d5",
+     "innerBlocks": Array [],
+     "isValid": true,
      "name": "core/paragraph",
    },
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:2154:5
should select by dragging into nested block in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 1 failed attempt.
TimeoutError: locator.hover: Timeout 10000ms exceeded.
Call log:
  - waiting for locator('[name="editor-canvas"]').contentFrame().getByRole('document', { name: 'Block: Paragraph' }).first()
    - locator resolved to <p role="document" data-empty="false" aria-multiline="true" aria-readonly="false" data-title="Paragraph" contenteditable="true" data-type="core/paragraph" aria-label="Block: Paragraph" data-wp-block-attribute-key="content" id="block-55c5b025-9a86-422e-a94c-ce36597edef4" data-block="55c5b025-9a86-422e-a94c-ce36597edef4" class="block-editor-rich-text__editable block-editor-block-list__block wp-block wp-block-paragraph rich-text">1</p>
  - attempting hover action
    2 × waiting for element to be visible and stable
      - element is visible and stable
      - scrolling into view if needed
      - done scrolling
      - <button id="_r_47_" type="button" tabindex="-1" aria-label="Italic" aria-pressed="false" data-toolbar-item="true" class="components-button components-toolbar-button is-compact has-icon">…</button> from <div tabindex="-1" data-wp-c16t="true" data-wp-component="Popover" class="components-popover block-editor-block-popover block-editor-block-list__block-popover is-positioned is-unstyled">…</div> subtree intercepts pointer events
    - retrying hover action
    - waiting 20ms
    2 × waiting for element to be visible and stable
      - element is visible and stable
      - scrolling into view if needed
      - done scrolling
      - <button id="_r_47_" type="button" tabindex="-1" aria-label="Italic" aria-pressed="false" data-toolbar-item="true" class="components-button components-toolbar-button is-compact has-icon">…</button> from <div tabindex="-1" data-wp-c16t="true" data-wp-component="Popover" class="components-popover block-editor-block-popover block-editor-block-list__block-popover is-positioned is-unstyled">…</div> subtree intercepts pointer events
    - retrying hover action
      - waiting 100ms
    18 × waiting for element to be visible and stable
       - element is visible and stable
       - scrolling into view if needed
       - done scrolling
       - <button id="_r_47_" type="button" tabindex="-1" aria-label="Italic" aria-pressed="false" data-toolbar-item="true" class="components-button components-toolbar-button is-compact has-icon">…</button> from <div tabindex="-1" data-wp-c16t="true" data-wp-component="Popover" class="components-popover block-editor-block-popover block-editor-block-list__block-popover is-positioned is-unstyled">…</div> subtree intercepts pointer events
     - retrying hover action
       - waiting 500ms

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:561:20
should select by dragging in /test/e2e/specs/editor/various/multi-block-selection.spec.js, passed after 2 failed attempts.
Error: expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 0

  Array [
-   1,
    2,
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:532:5
Error: expect(received).toEqual(expected) // deep equality

- Expected  - 1
+ Received  + 0

  Array [
    1,
-   2,
  ]

Call Log:
- Timeout 5000ms exceeded while waiting on the predicate
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/multi-block-selection.spec.js:532:5

Truncated. See the full report.

2a03bf9 Run

@ellatrix ellatrix added No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Type] Feature New feature to highlight in changelogs. labels Oct 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ellatrix
ellatrix marked this pull request as ready for review October 25, 2026 06:32
@ellatrix
ellatrix requested a review from t-hamano as a code owner October 25, 2026 06:32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

No Core Sync Required Indicates that any changes do not need to be synced to WordPress Core [Tool] Env /packages/env [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant