Repository navigation
Step aside one piece at a time if core adopts it #733
Description
#710's last step says that if core adopts a piece, the plugin's newer copy takes over through hooks rather than stepping aside. Today it steps aside: presence-api.php returns early with a notice if $wpdb->presence is already registered. And it can't do anything else yet, because the plugin declares core's own names. If core ships wp_get_presence(), loading the plugin's wp_get_presence() is a fatal "cannot redeclare".
Decided (2026-10-08): B for the API's functions, C one piece at a time for the features. The open tasks wait for core's patch (decision, parked).
This needs deciding before the first Trac patch, because it decides what the plugin's functions are called.
How Gutenberg does it
Gutenberg's plugin copies are named for the plugin, and lib/compat/wordpress-X.Y/ swaps them in by unhooking core's callback and hooking its own (remove_action( … core … ); add_action( … gutenberg_… );). New public functions that core may also ship are wrapped in if ( ! function_exists( … ) ).
Options
| Public functions | Hook callbacks | Cost | |
|---|---|---|---|
| A. Gutenberg's split | wp_ names, wrapped in function_exists(), so core's copy wins once it exists |
Renamed to gutenberg_-style plugin names (for example presence_api_), so the plugin can unhook core's and hook its own |
Every callback renamed, and the Trac patch renames them back |
| B. Guard everything | Every function wrapped in function_exists() |
Core's callbacks run; the plugin's newer behavior can't replace them | No renames, but the plugin can't take over, only fill gaps |
| C. Step aside per piece | As today | As today | Keep the early return, but per piece: a piece core has merged stops loading. Simple, but the plugin can't ship fixes to a merged piece |
A is what #710 asks for. B and C are cheaper and give up the takeover.
To decide
- Pick A, B or C
- Whether
$wpdb->presencealready being registered still stops the whole plugin, or only the schema piece
Tasks
- Wrap each public API function in
function_exists(), so core's copy wins if core adopts it - When
$wpdb->presenceis already registered, step aside only the schema piece while core's schema version is at least the plugin's, instead of returning early for the whole plugin (waits for core's patch) - When core adopts a feature, a release removes its registration and files
- Wrap
cron.php,privacy.php,lifecycle.php,heartbeat.php,site-health.phpandrest-api.phpinfunction_exists()/class_exists(), since Core-bound code can't be lifted out of the plugin one ticket at a time #554 lands them in separate tickets - Drop the
cli/guard, since the CLI goes to WP-CLI rather than core
Part of #710.
Activity
josephfusco commentedon Oct 8, 2026
Decided: B for the API's functions and C, one piece at a time, for the features.
function_exists(), so core's copy wins and the API has one ownercorein its registration, so its folder no longer loads and its switch is hidden; features core has not adopted keep working$wpdb->presencealready registeredA is left out because renaming every callback to a plugin prefix goes against naming the code as core would, and the Trac patch would rename them back; a fix to a merged piece goes to core as a patch. If one merged piece ever needs a plugin-side fix, A can be used for that piece alone.
[-]Decide how the plugin takes over from core once core merges a piece[/-][+]Step aside one piece at a time once core merges it[/+][-]Step aside one piece at a time once core merges it[/-][+]Step aside one piece at a time if core adopts it[/+]huzaifaalmesbah commentedon Oct 9, 2026
I'd like to take the first task, wrapping the public API functions in
function_exists(). A plan to check before I start:if ( ! function_exists( 'wp_get_presence' ) )per function rather than one guard per file, so a partial core merge still lets the plugin fill in what core left out.@access privatehelpers likewp_presence_write_row()under the same names, so the redeclare fatal applies to them as well; I'd guard all 57 inpresence.php, not only the documented ones.class_exists()for the REST controllers and the CLI command (the widget classes belong to features, which thecoremarker already stops loading), and.defined()for constants likeWP_PRESENCE_MAX_KEY_LENGTH, the wayWP_PRESENCE_DEFAULT_TTLalready isIs that the scope you meant, or only the functions in the README for now? I'll leave the schema and
core-marker tasks to #799.Update: #805 follows this plan except for the constants, since a
defined()guard lets a site set them inwp-config.php, and a key length above the column's 191 would bring back the silent truncation it prevents (details).i-am-chitti commentedon Oct 10, 2026
@josephfusco The scope question above is still open, so I'd like to settle it before reviewing #805:
cron,privacy,lifecycle,heartbeat,rest-apiandsite-healthship in core's first ticket (Core-bound code can't be lifted out of the plugin one ticket at a time #554), but nothing in the plan covers them. They aren't public API, they aren't features, and they aren't the schema. Should we guard them, or skip them along with the schema?cli/goes to WP-CLI, not core (Core-bound code can't be lifted out of the plugin one ticket at a time #554), so its guard can be dropped.josephfusco commentedon Oct 12, 2026
scripts/private-calls.txtalready lists each of those calls for review when that piece's ticket lands.cli/guard.josephfusco commentedon Oct 12, 2026
Parking the schema step-aside until core's patch exists. The early return already keeps the plugin from fataling if core registers the table, and how the plugin reads core's schema version depends on what that patch ships. This issue stays on Core patch 1 for that task, and the
coremarker lands in #818.josephfusco commentedon Oct 12, 2026
#805 wraps the API's functions and the REST controllers. Dropping the
cli/guard and wrappingcron.php,privacy.php,lifecycle.php,heartbeat.php,site-health.phpandrest-api.phpwait with the schema task for core's patch.josephfusco commentedon Oct 12, 2026
Reworded the last task, since #818 closed without the
coremarker: when core adopts a feature, a release removes its registration and files, as Gutenberg does. The open tasks all wait for core's patch.josephfusco commentedon Oct 20, 2026
Decided and split: the schema step-aside is in #859, the remaining guards are in #860, and removing an adopted feature's files is now a line in #550.