Skip to content

Refactor: Extract CBT_Theme_Save service from rest_save_theme#833

Open
bph wants to merge 4 commits into
trunkfrom
refactor/extract-theme-save-service
Open

Refactor: Extract CBT_Theme_Save service from rest_save_theme#833
bph wants to merge 4 commits into
trunkfrom
refactor/extract-theme-save-service

Conversation

@bph
Copy link
Copy Markdown
Contributor

@bph bph commented May 5, 2026

Summary

Lift the orchestration logic out of rest_save_theme() into a new
CBT_Theme_Save::run() static service, so the upcoming wp cbt save
CLI command (#830) can share the same code path. The REST endpoint
becomes a thin wrapper.

This is the first PR in the WP-CLI track tracked in #828.

Changes

  • New: includes/create-theme/theme-save.php
    CBT_Theme_Save::run( array $options ): true|WP_Error. Verbatim
    port of the four save branches (fonts, templates, styles, patterns)
    • theme cache invalidation.
  • Modified: includes/class-create-block-theme-api.php
    rest_save_theme() shrunk from 50 lines to 14: call run(),
    return WP_Error directly, otherwise wrap success in the existing
    WP_REST_Response shape.
  • New: tests/test-theme-save.php — 13 PHPUnit cases.

Bug fixes that fall out of the refactor

The orchestration logic had two latent issues that get resolved by
the new normalize_flags() helper, which routes every flag through a
single ! empty() check:

  1. processOnlySavedTemplates was previously dereferenced without an
    isset() guard, raising an Undefined index notice when
    saveTemplates was passed without it. The current Site Editor UI
    always sends both flags, so the notice was only reachable from
    non-JS callers — but the upcoming CLI is exactly that.
  2. Flags were compared with strict true ===, rejecting 1,
    "true", "1". This is fine for a typed JS client and brittle
    for CLI and third-party REST callers.

Scope guarantees

No wire-level behavior change at the REST boundary:

  • URL, HTTP method, permission callback unchanged.
  • Response shape unchanged ({ status: 'SUCCESS', message: 'Theme Saved.' }).
  • WP_Error from the patterns step is still returned as-is — the
    REST framework converts it to a JSON error response with the same
    status code as before.

The looser truthy semantics from the bug fix are isolated to the
/save endpoint. Sibling spots in the same file
(rest_create_variation, rest_reset_theme) keep their existing
strict-=== checks; those endpoints already had isset() guards
and never produced a notice. If we want consistency across the file,
that's worth a separate small PR.

Test plan

PHPUnit (added in this PR):

  • test_run_with_empty_options_returns_true
  • test_run_without_options_does_not_invoke_any_save_step
  • test_save_templates_without_process_only_flag_does_not_warn
    regression test for the processOnlySavedTemplates notice.
    Matcher narrowed to the key name so unrelated downstream notices
    do not fail it.
  • test_truthy_save_fonts_values_trigger_persist (data provider:
    true, 1, "1", "true")
  • test_falsy_save_fonts_values_skip_persist (data provider:
    false, 0, "", null)
  • test_missing_save_fonts_key_skips_persist
  • test_run_propagates_wp_error_from_patterns_step

Manual smoke (Site Editor → Save Changes panel) covering the four
branches and the WP_Error path:

  • Save Fonts only — activated font moves into theme.
  • Save Templates with Process Only Modified on — only
    modified templates written.
  • Save Templates with Process Only Modified off — all
    templates written, no Undefined index notice in the PHP
    error log.
  • Save Style Changes only — theme.json updated, user-side
    global styles cleared.
  • Save Patterns with a name conflict — UI shows the
    pattern_already_exists snackbar error from WP_Error.

Cache invalidation (wp_get_theme()->cache_delete()) is not asserted
in PHPUnit; observing it would require mocking statics, which isn't
worth a new test framework just for one assertion.

Out of scope

  • Any change to the underlying utility classes (CBT_Theme_Fonts,
    CBT_Theme_Templates, CBT_Theme_JSON, CBT_Theme_Patterns,
    CBT_Theme_Styles).
  • The CLI command itself (Feature: Add wp cbt save CLI command #830).
  • Extracting the other REST endpoints (variation/reset) — same
    pattern is there but the WP-CLI track only needs save and export.
  • Promoting silent filesystem failures to WP_Error — separate
    concern, intentionally not broadened here.

Pre-existing bug noticed during testing (separate)

The strict error handler in the regression test surfaced an undefined
property access at theme-templates.php:233 ($template->media
read without a property check). Not introduced by this PR; worth a
small follow-up issue.

Refs #828
Closes #829

Move the orchestration logic out of `rest_save_theme()` into a new
`CBT_Theme_Save::run()` static service so a future `wp cbt save`
CLI command can share the same code path. The REST endpoint becomes
a thin wrapper; URL, permissions, and response shape are unchanged.

Two latent bugs in the orchestrator are fixed by the same one-line
helper that normalizes the save flags:

- `processOnlySavedTemplates` was dereferenced without an `isset()`
  guard, raising an `Undefined index` notice when `saveTemplates` was
  passed without the companion flag.
- Flags were compared with strict `true ===`, rejecting `1` /
  `"true"` / `"1"` from non-JS callers (CLI, third-party REST clients).

A single `! empty()` pass through `normalize_flags()` collapses both
into one place.

New PHPUnit coverage in `tests/test-theme-save.php`:
- empty-options no-op + side-effect check
- `processOnlySavedTemplates` no-notice regression (matcher narrowed
  to the key name so unrelated downstream notices do not fail it)
- truthy/falsy flag normalization data providers
- missing-key skip
- `WP_Error` propagation from the patterns step

Refs #828
Closes #829
bph added 2 commits May 6, 2026 08:34
`! empty()` treats the string "false" as truthy because it is
non-empty, so a caller sending `saveFonts=false` over a query string
or form-encoded body would have its disabled flag silently flipped
to enabled. The strict `true ===` check it replaced did not have
this problem in the falsy direction.

`wp_validate_boolean()` is WordPress's idiomatic helper for the
common forms of bool-ish input: it accepts `true`, `"true"` (case-
insensitive), `1`, `"1"` as truthy and `false`, `"false"`, `0`,
`"0"`, `null`, `""` as falsy.

The null-coalesce `?? false` keeps the helper from receiving a
PHP-8.x undefined-index notice when a key is missing.

Three new cases added to the falsy data provider: `"false"`,
`"FALSE"` (case-insensitivity), and `"0"`.

Found in Codex review of #833.
Three documentation tweaks for #833 reviewer onboarding, no behavior
change:

- File-level docblock on `theme-save.php` describes the service's
  role and explicitly notes that callers — not the service itself —
  are responsible for capability and input sanitization checks.
- `run()` docblock expands on the same point with concrete examples
  (`current_user_can( 'edit_theme_options' )`, downstream services
  that consume non-flag option values), so a future CLI implementer
  is not left guessing what the REST framework was doing on their
  behalf.
- `test-theme-save.php` class-level docblock notes that cache
  invalidation is intentionally not asserted, and why, so a future
  contributor does not file it as a coverage gap.
@bph bph marked this pull request as ready for review May 6, 2026 09:58
@bph bph requested review from MaggieCabrera and mikachan May 6, 2026 09:59
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.

Refactor: Extract CBT_Theme_Save service from rest_save_theme

1 participant