Skip to content

Step aside one piece at a time if core adopts it #733

Closed

Description

@josephfusco

#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->presence already 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->presence is 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.php and rest-api.php in function_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

  1. added
    [Type] EnhancementA suggestion for improvement of an existing feature
    Needs DecisionBlocked on a maintainer call, not on more work
    Public APIAdds a hook, route, constant, or global that third parties can depend on
    on Oct 5, 2026
  2. added this to the n.e.x.t milestone on Oct 5, 2026
  3. josephfusco commented on Oct 8, 2026

    @josephfusco
    CollaboratorAuthor

    Decided: B for the API's functions and C, one piece at a time, for the features.

    If core adopts it
    Public API functions Wrapped in function_exists(), so core's copy wins and the API has one owner
    A feature Marked core in its registration, so its folder no longer loads and its switch is hidden; features core has not adopted keep working
    $wpdb->presence already registered Only the schema piece steps aside: the plugin uses core's table while core's schema version is at least its own, and keeps today's notice otherwise

    A 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.

  4. changed the title [-]Decide how the plugin takes over from core once core merges a piece[/-] [+]Step aside one piece at a time once core merges it[/+] on Oct 8, 2026
  5. changed the title [-]Step aside one piece at a time once core merges it[/-] [+]Step aside one piece at a time if core adopts it[/+] on Oct 8, 2026
  6. huzaifaalmesbah commented on Oct 9, 2026

    @huzaifaalmesbah
    Member

    I'd like to take the first task, wrapping the public API functions in function_exists(). A plan to check before I start:

    • Each function on its own. One 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.
    • Private helpers too. Core would ship @access private helpers like wp_presence_write_row() under the same names, so the redeclare fatal applies to them as well; I'd guard all 57 in presence.php, not only the documented ones.
    • Classes and constants in the same pass. class_exists() for the REST controllers and the CLI command (the widget classes belong to features, which the core marker already stops loading), and defined() for constants like WP_PRESENCE_MAX_KEY_LENGTH, the way WP_PRESENCE_DEFAULT_TTL already is.
    • A test that keeps it that way. It would read each file's tokens and fail if a new top-level function isn't guarded, so the next function added can't skip it.

    Is 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 in wp-config.php, and a key length above the column's 191 would bring back the silent truncation it prevents (details).

  7. i-am-chitti commented on Oct 10, 2026

    @i-am-chitti
    Collaborator

    @josephfusco The scope question above is still open, so I'd like to settle it before reviewing #805:

    1. Should we guard all functions, or only the public ones? If core's copy of a private helper wins, the plugin's code calls core's version, which can change without notice.
    2. cron, privacy, lifecycle, heartbeat, rest-api and site-health ship 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?
    3. 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.
  8. josephfusco commented on Oct 12, 2026

    @josephfusco
    CollaboratorAuthor
    1. Guard all of them, because an unguarded private helper hits the same redeclare fatal once core ships it. The drift you describe only reaches features that call a private helper, and scripts/private-calls.txt already lists each of those calls for review when that piece's ticket lands.
    2. Guard them the same way, since Core-bound code can't be lifted out of the plugin one ticket at a time #554 lands them in different tickets (cron and privacy with the table, heartbeat with post locks in An open editor makes every cached post query stale on each Heartbeat tick #548, REST after that), so stepping them aside with the schema would drop heartbeat and REST before core has them.
    3. Agreed, drop the cli/ guard.
  9. josephfusco commented on Oct 12, 2026

    @josephfusco
    CollaboratorAuthor

    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 core marker lands in #818.

  10. josephfusco commented on Oct 12, 2026

    @josephfusco
    CollaboratorAuthor

    #805 wraps the API's functions and the REST controllers. Dropping the cli/ guard and wrapping cron.php, privacy.php, lifecycle.php, heartbeat.php, site-health.php and rest-api.php wait with the schema task for core's patch.

  11. josephfusco commented on Oct 12, 2026

    @josephfusco
    CollaboratorAuthor

    Reworded the last task, since #818 closed without the core marker: when core adopts a feature, a release removes its registration and files, as Gutenberg does. The open tasks all wait for core's patch.

  12. josephfusco commented on Oct 20, 2026

    @josephfusco
    CollaboratorAuthor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Public APIAdds a hook, route, constant, or global that third parties can depend on[Area] InfrastructureIssues for the repository infrastructure[Type] EnhancementA suggestion for improvement of an existing feature

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions