Add Bricks support on a shared builder-provider layer - #1287
Draft
danieliser wants to merge 3 commits into
Draft
Conversation
…layer Popup Maker's page builder integrations each reimplemented the same lifecycle: recognize a builder request, authorize it, restore the otherwise non-public popup query, render an isolated canvas, and load assets for popups that are not the main query post. The Elementor work expressed that lifecycle as a base controller plus traits, which fit Elementor but assumed Elementor's mechanics. Adding Bricks made those assumptions visible. Bricks ships as a theme, gates editing on a site-owner setting, returns markup from its renderer where Elementor returns a string from a different API, self-gates its one-time template bootstrap so it cannot be re-run, and emits no per-element document CSS after wp_head. A shared "re-run the builder's asset handler" trait cannot express both builders. Model builders on the existing form-provider architecture instead: a registered provider contract, a provider registry, thin builder adapters, and a coordinator that owns the request lifecycle. - Add BuilderProvider plus five optional capability interfaces, so a builder declares only what it can do. Divi implements two of five and gains no dead code. - Add a BuilderProviders registry and a Builders coordinator owning authorization, query restoration, canvas selection, content routing, and asset batching. Providers answer questions and perform one operation each. - Extract signed preview URLs into a stateless BuilderPreviewUrl service. The nonce binds popup ID and provider key, so a signature cannot be replayed across popups or builders. - Narrow Previews to the core editor's popup_preview flow and delegate builder questions to the coordinator, keeping preview handling and frontend asset loading as separate concepts. - Remove the builder compatibility controllers and the BuilderPreview and AssetBatching traits they shared. Behavior changes beyond the refactor: - Draft popups are queryable by an authorized editor. Builders open unsaved documents, which would otherwise 404. - Builder canvases receive no triggers, so an auto-open or time-delay trigger cannot reopen the canvas mid-edit. - Elementor widgets are marked once initialized, so reopening a popup no longer attaches duplicate handlers. Verified against Bricks 1.12.4 and Elementor 4.1.3: the canvas renders only the popup with its theme applied and no theme shell, generated Bricks element CSS reaches the browser, a Bricks page and two Bricks popups coexist without duplicating #brx-content, anonymous requests to popup permalinks and builder URLs return 404 or redirect without leaking content, and six builder popups finalize assets in one pass rather than six. PHPUnit 875 tests / 1800 assertions pass. PHPCS clean. PHPStan unchanged at the 40-error baseline. ESLint clean. Production build succeeds.
- Scope Bricks widget reinitialization to the opened popup. Each registered BricksFunction accepts a `parentNode` override and tracks the elements it has already initialized, so reinitializing through that API avoids reprocessing the whole page on every popup open. Verified at runtime: 33 run() calls, all scoped to the opened popup, none unscoped. - Restore the global $post in a finally block, so a throwing element or filter cannot leave the host page pointed at the popup. - Correct the Bricks stub's render_data() return type. It returns markup rather than echoing, and the stub claimed void. - Correct two stale cells in the discovery capability matrix.
…state Adversarial review of the Bricks provider found four defects in how it shares request-scoped state with Bricks. All are verified against Bricks 1.12.4 source and covered by new tests that run against the real theme. Assets::$inline_css['popup'] is Bricks' shared popup bucket, not ours: Bricks appends its own native popup CSS to it (assets.php:474) and reads it back in two places (assets.php:572, popups.php:928) without ever clearing it. Reading and clearing the whole bucket could therefore emit Bricks' CSS a second time, or discard CSS Bricks had not yet emitted. Capture only the delta this provider generates, hand the bucket back unchanged, and buffer our own CSS locally. render_data() overwrites Frontend::$elements and $area with the tree it is rendering and does not restore them. This render can be nested, because preloading a popup discovered from a popmake-ID trigger happens while the host page's element tree is still rendering, so returning with a clobbered element map could drop the host page's remaining children. Snapshot and restore both, the way Bricks does around its own nested template renders (templates.php:263-268). Bricks generates page-level CSS only for post IDs in Assets::$page_settings_post_ids (assets.php:857), so a popup's own Bricks page settings were skipped. Register the popup ID during collection. The popup post type was injected into Bricks' settings on read. Bricks has read-modify-write paths that get_option() those settings and update_option() the whole array back, such as its Instagram token refresh (integrations/instagram/instagram.php:95-101), which would have persisted the injection and left it behind after deactivation. Strip it again on write, while preserving the value when the site owner enabled it themselves. Also bail early when the popup post cannot be loaded, rather than rendering with a null global post and restoring the wrong state. Testing: add Bricks_Provider_Test with twelve regression tests, and let the suite run against a real theme via PUM_TEST_THEME. Bricks strips element data saved by users who cannot execute code, so those fixtures run as an administrator. 887 tests pass with Bricks active (1843 assertions) and with Bricks absent, where the Bricks tests skip. PHPCS clean. PHPStan unchanged at the 40-error baseline.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Issue
Popup Maker's page builder integrations each reimplemented the same lifecycle:
recognize a builder request, authorize it, restore the otherwise non-public popup
query, render an isolated canvas, and load assets for popups that are not the main
query post. #1271 expressed that lifecycle as a base controller plus traits, which
fit Elementor but encoded Elementor's mechanics as if they were universal.
This PR adds complete Bricks support and uses it to find the real common
denominator. Bricks was chosen as a deliberate architecture test.
What Bricks broke about the previous abstraction
Discovery is written up in
docs/page-builder-discovery.md, with the runtimeevidence for each claim. The load-bearing differences:
get_builder_content_for_display()returns a stringFrontend::render_data()returns markup via a different API;render_content()echoes and wraps in<main id="brx-content">Database::set_active_templates()self-gates; re-running is impossible and re-initializing by hand corrupts the host pageFrontend::enqueue_styles()window.bricksFunctionsThe
AssetBatchingtrait assumed "re-run the builder's asset handler." Bricks hasno such handler, so no shared trait can express both. What is shared is when to
flush and how to deduplicate — which belongs to a coordinator, not a trait.
Architecture
Modeled on Popup Maker's existing form provider architecture, as suggested:
PUM_Interface_Integration::enabled()BuilderProvider::is_available()PUM_Integrations::$integrationsBuilderProvidersregistrypum_integrationsfilterpopup_maker/register_builder_providersget_enabled_form_integrations()BuilderProviders::available()Intentionally not mirrored: form providers put optional behavior on the
abstract class as no-op methods. Builders differ far more, so optional behavior is
five capability interfaces. A provider that does not implement one is never asked.
Capability coverage
EditsPopupsSupportsPopupPostTypeRendersDocumentsthe_contentLoadsDocumentAssetsProvidesPreviewUrlDivi is the third builder (chosen over a throwaway Beaver spike because it is
shipped code with real users). It implements 2 of 5 and gains no dead code, which
is the evidence the base contract is not Elementor-shaped.
What proved genuinely shared
edit_poston that specific popup.post_statusfor drafts.What stayed builder-specific
did_actionvs theme-or-plugin).Removed / merged from #1271
Compatibility/Builder/Concerns/BuilderPreview.php(trait)Services\BuilderPreviewUrl+ coordinatorCompatibility/Builder/Concerns/AssetBatching.php(trait)LoadsDocumentAssetsCompatibility/Builder/Elementor.phpBuilders\ElementorCompatibility/Builder/Divi.phpBuilders\DiviIntegration/Builder/Bricks.php(legacy, disabled)Builders\BricksPage_Builder_Preview_Test.php(tested the trait)Builder_Providers_Test(tests real contracts)PUM_Previewskeeps working: deprecated methods forward, and the two that moved(
allow_builder_preview_request,use_builder_preview_template) forward to thecoordinator.
Previewsis now scoped to the core editor'spopup_previewflowonly — preview handling and frontend asset loading are no longer conflated.
Request lifecycle: before → after
Previews+ 2 per builder traitBuilders, 1 per providerwp_enqueue_scripts:12+wp_footer:0per builderpopup_maker/builder_preview_idfilter chainMeasured overhead (Bricks, warm)
Six popups finalize once, not six times — asserted in tests, not just measured.
Verification
Browser-tested against Bricks 1.12.4 + Elementor 4.1.3 on WordPress 7.0.
Bricks canvas (
?bricks=run&brickspreview=1):#brx-header/#brx-footerabsent)color: rgb(204,0,0),font-size: 32px)background: rgb(249,249,249),padding: 18px)triggers: []— no live triggers in the canvasaria-disabled="true"Bricks page + 2 Bricks popups + 1 non-Bricks popup on one request:
#brx-contentappears exactly onceBricksFunction.run()calls on open, 0 unscoped, 0 JS errorsSecurity (anonymous):
?elementor-preview→ 404?pum-builder-preview→ 404?bricks=run→ 302 to home with no content leaked (Bricks' own guard)Results
Bricks absent (Bricks tests skip → no-op requirement proven).
Local AI review
CodeRabbit and Codex (gpt-5.6-sol, high) both reviewed the branch. Codex could not
refute the access-control design. Seven valid findings were fixed and covered by
regression tests:
Assets::$inline_css['popup']bucket,which Bricks also writes to and reads back — could double-emit or discard
Bricks' own CSS. Now captures only its own delta.
render_data()clobbersFrontend::$elements/$areawithout restoring; thisrender can be nested. Now snapshotted, as Bricks does for its own nested
templates.
Assets::$page_settings_post_ids, so its Bricks pagesettings CSS was skipped.
read-modify-write option paths and survive deactivation. Now stripped on write,
while respecting a genuine owner opt-in.
$postrestoration was not exception-safe.Remaining architectural concern
Two capability couplings a fourth builder would expose, documented rather than
speculatively abstracted:
LoadsDocumentAssetsis only reached afterRendersDocumentsreturns markup. Abuilder that renders through its own
the_contenthooks but still needssecondary-document CSS would have to implement
RendersDocumentsto get there.EditsPopups::get_requested_popup_id(),so a REST/admin-only builder wanting signed previews must implement
EditsPopups.Also assumed: a
p/post_typequery rewrite, one main-query popup, a binaryshell-vs-canvas split, and two asset boundaries. A Breakdance-style builder with a
distinct editor route would need these loosened. I would rather loosen them against
a real fourth builder than guess now.
Test URLs
wp-env on isolated ports (
.wp-env.override.jsonis gitignored and local-only):npx wp-env start # http://localhost:8981 (admin/password)/?post_type=popup&p=15&bricks=run&brickspreview=1/bricks-page-with-popup/PUM_TEST_THEME=bricks vendor/bin/phpunit -c tests/php/phpunit.xml🤖 Generated with Claude Code