Introduction
Multisite handling was removed in #XXX (replace with the cleanup PR number) because the previous implementation was silently broken: the useBasePrefix flag was dead code, and after a switch_blog() call, models targeting network-shared tables (User, UserMeta, Site, Blog, BlogVersion, SiteMeta, Signup, RegistrationLog) generated queries against non-existent per-blog tables (wp_2_users instead of wp_users, etc.).
This issue tracks reintroducing the feature cleanly.
Goals
- Provide working models for the network-shared WordPress tables:
wp_users, wp_usermeta, wp_blogs, wp_blog_versions, wp_site, wp_sitemeta, wp_signups, wp_registration_log.
- Keep per-blog models (
Post, PostMeta, Option, Comment, Term, …) bound to the active blog and follow switch_blog() automatically.
- Behave identically on single-site installs (no regression).
- Cover the behavior with integration tests using
wp-phpunit and WP_UnitTestCase in multisite mode.
Non-goals
- A query DSL that crosses multiple blogs in a single statement.
- Schema migrations across the network.
Proposed approach: two connections
Register two Eloquent connections via the Resolver and let each model pick the right one:
| Connection name |
tablePrefix |
Use case |
default |
$wpdb->prefix |
Per-blog tables (refreshed on switch) |
base |
$wpdb->base_prefix |
Network-shared tables |
Implementation sketch:
Resolver::connection($name) resolves $name === 'base' to a dedicated Database instance built with $wpdb->base_prefix. Default still goes to the per-blog Database instance.
- Re-add a
switch_blog listener that refreshes only the default connection's tablePrefix in place (don't null the singleton — that churns event listeners and breaks transaction state). The base connection is left untouched.
- Reintroduce
Models\Multisite\{Blog, BlogVersion, Site, SiteMeta, Signup, RegistrationLog} and update User, UserMeta to declare protected $connection = 'base'.
- Document the contract in the README: "After
switch_blog($id), the next default-connection query targets the new blog automatically. Shared models always hit the network tables regardless of the current blog."
Open questions
- In-place prefix mutation vs new connection on
switch_blog. Mutating is cheaper but invalidates the prepared-statement cache and any builder that captured the connection. A clean recreate (with a stable base) is safer; benchmark before deciding.
- Transaction safety across
switch_blog. WordPress core does not expect an active DB transaction during a blog switch. We should at least detect and refuse the switch (or warn) when transactionLevel() > 0 on the default connection. To investigate.
HasMetas join queries on shared models. UserMeta lives on base; if a developer joins user meta with per-blog tables (e.g. Post), the current single-connection joinToMeta() will mix prefixes. Decide whether to forbid cross-connection joins or implement a manual cross-database SQL fallback.
global $wpdb switching. switch_to_blog() reassigns $wpdb->prefix but the wpdb object stays the same — confirm that the Database instance can rely on a single $wpdb reference for both connections.
References
Introduction
Multisite handling was removed in #XXX (replace with the cleanup PR number) because the previous implementation was silently broken: the
useBasePrefixflag was dead code, and after aswitch_blog()call, models targeting network-shared tables (User,UserMeta,Site,Blog,BlogVersion,SiteMeta,Signup,RegistrationLog) generated queries against non-existent per-blog tables (wp_2_usersinstead ofwp_users, etc.).This issue tracks reintroducing the feature cleanly.
Goals
wp_users,wp_usermeta,wp_blogs,wp_blog_versions,wp_site,wp_sitemeta,wp_signups,wp_registration_log.Post,PostMeta,Option,Comment,Term, …) bound to the active blog and followswitch_blog()automatically.wp-phpunitandWP_UnitTestCasein multisite mode.Non-goals
Proposed approach: two connections
Register two Eloquent connections via the
Resolverand let each model pick the right one:tablePrefixdefault$wpdb->prefixbase$wpdb->base_prefixImplementation sketch:
Resolver::connection($name)resolves$name === 'base'to a dedicatedDatabaseinstance built with$wpdb->base_prefix. Default still goes to the per-blogDatabaseinstance.switch_bloglistener that refreshes only thedefaultconnection'stablePrefixin place (don't null the singleton — that churns event listeners and breaks transaction state). Thebaseconnection is left untouched.Models\Multisite\{Blog, BlogVersion, Site, SiteMeta, Signup, RegistrationLog}and updateUser,UserMetato declareprotected $connection = 'base'.switch_blog($id), the nextdefault-connection query targets the new blog automatically. Shared models always hit the network tables regardless of the current blog."Open questions
switch_blog. Mutating is cheaper but invalidates the prepared-statement cache and any builder that captured the connection. A clean recreate (with a stablebase) is safer; benchmark before deciding.switch_blog. WordPress core does not expect an active DB transaction during a blog switch. We should at least detect and refuse the switch (or warn) whentransactionLevel() > 0on thedefaultconnection. To investigate.HasMetasjoin queries on shared models.UserMetalives onbase; if a developer joins user meta with per-blog tables (e.g.Post), the current single-connectionjoinToMeta()will mix prefixes. Decide whether to forbid cross-connection joins or implement a manual cross-database SQL fallback.global $wpdbswitching.switch_to_blog()reassigns$wpdb->prefixbut the wpdb object stays the same — confirm that theDatabaseinstance can rely on a single$wpdbreference for both connections.References
126e8f9feat(multisite): shared tables346f30f