Refactor: Extract CBT_Theme_Save service from rest_save_theme#833
Open
bph wants to merge 4 commits into
Open
Conversation
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
This was referenced May 5, 2026
`! 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.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lift the orchestration logic out of
rest_save_theme()into a newCBT_Theme_Save::run()static service, so the upcomingwp cbt saveCLI 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
includes/create-theme/theme-save.php—CBT_Theme_Save::run( array $options ): true|WP_Error. Verbatimport of the four save branches (fonts, templates, styles, patterns)
includes/class-create-block-theme-api.php—rest_save_theme()shrunk from 50 lines to 14: callrun(),return
WP_Errordirectly, otherwise wrap success in the existingWP_REST_Responseshape.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 asingle
! empty()check:processOnlySavedTemplateswas previously dereferenced without anisset()guard, raising anUndefined indexnotice whensaveTemplateswas passed without it. The current Site Editor UIalways sends both flags, so the notice was only reachable from
non-JS callers — but the upcoming CLI is exactly that.
true ===, rejecting1,"true","1". This is fine for a typed JS client and brittlefor CLI and third-party REST callers.
Scope guarantees
No wire-level behavior change at the REST boundary:
{ status: 'SUCCESS', message: 'Theme Saved.' }).WP_Errorfrom the patterns step is still returned as-is — theREST 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
/saveendpoint. Sibling spots in the same file(
rest_create_variation,rest_reset_theme) keep their existingstrict-
===checks; those endpoints already hadisset()guardsand 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_truetest_run_without_options_does_not_invoke_any_save_steptest_save_templates_without_process_only_flag_does_not_warn—regression test for the
processOnlySavedTemplatesnotice.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_persisttest_run_propagates_wp_error_from_patterns_stepManual smoke (Site Editor → Save Changes panel) covering the four
branches and the
WP_Errorpath:modified templates written.
templates written, no
Undefined indexnotice in the PHPerror log.
theme.jsonupdated, user-sideglobal styles cleared.
pattern_already_existssnackbar error fromWP_Error.Cache invalidation (
wp_get_theme()->cache_delete()) is not assertedin PHPUnit; observing it would require mocking statics, which isn't
worth a new test framework just for one assertion.
Out of scope
CBT_Theme_Fonts,CBT_Theme_Templates,CBT_Theme_JSON,CBT_Theme_Patterns,CBT_Theme_Styles).pattern is there but the WP-CLI track only needs save and export.
WP_Error— separateconcern, 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->mediaread without a property check). Not introduced by this PR; worth a
small follow-up issue.
Refs #828
Closes #829