Repository navigation
Conversation
Add a typed model for multisite sites, identified by a stable, environment-independent key stored as `site_key` blogmeta next to the `orig_*` meta written by `app:tenancy:demap-domains`. - `Tenancy\SiteKey`: DNS-label key; derives from a host using the same leftmost-label rule as DemapDomains, or from a subdirectory path - `Tenancy\Domain`: host and path value object - `Tenancy\SiteMeta`: blogmeta key enum - `Database\Models\Site`: wraps WP_Site and its blogmeta - `Tenancy\SiteRepository`: look up sites by key or ID (singleton) - `site:list`: list sites with key status, or as JSON - `site:assign-keys`: backfill keys, or set one with --id/--key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JvSQuyBbpZMsUNeUjNtWa
… commands
Add `TenancyServiceProvider`, discovered through `extra.acorn.providers`
and inert on single-site installs. From an application `booting` callback
it registers the request site's code and applies its config, so site
providers register after every configured provider (their bindings win)
and the overlay sees package defaults already merged by
`mergeConfigFrom()`.
Providers register least specific first: `app/Sites/{Key}/Providers`, the
active `plugins/{key}/` site plugin, `providers` in
`config/sites/{key}/app.php`, then the parent and child theme's
`composer.json` providers. Deferred providers keep their deferral. Site
view composers, commands, views and routes are discovered too.
Config overlays live in `config/sites/{key}/*.php`, which Laravel loads as
`sites.{key}.*`. They are applied leaf by leaf (maps merge, lists
replace, null removes), reverted and re-applied on every `switch_blog`
after Acorn's own handler, and never overlay `app.providers`. Optional
blogmeta-to-config mapping via `tenancy.meta_config`.
New commands: `site:run`, `site:check`, `site:cache`, `site:clear`
(also run by `optimize`/`optimize:clear`) and `make:site`.
`app:tenancy:demap-domains` becomes `site:demap-domains` (old name kept
as an alias). It now processes more than 100 sites, uses a site's key as
its label when one is set, and runs `wp search-replace` through
symfony/process, since the Process facade's illuminate/process package
is not installed by Acorn. Tenancy commands move from `AppServiceProvider`
to the new provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JvSQuyBbpZMsUNeUjNtWa
Harden multisite support for code that calls switch_to_blog(), especially console commands looping over sites. - Isolate Laravel's cache per blog across switches. Acorn only changes `cache.prefix`, which the file store (Acorn's default) ignores and which never reaches stores already built by CacheManager. CacheBootstrapper gives file stores a per-blog directory, re-prefixes built stores in place, and swaps array stores per blog, so injected repositories follow the switch too. Runs for every blog, keyed or not. - Never let a failure escape the `switch_blog` handler: Core has already switched its globals by then. Bootstrapper, lookup and listener errors are reported through the exception handler and skipped. Discovery or provider-loading failures at boot fall back to plain WordPress. - Detect the request site correctly when Acorn boots mid-switch, and bring state in line with the current blog. - Register the switch handler at initialization rather than in boot(), and fire SiteInitialized after boot so listeners registered in boot() get it. - Site::run() unwinds to its starting depth even if the callback throws or leaves switches unrestored. - Add Console\Concerns\InteractsWithSites::forEachSite() for commands: per-site failures are reported and counted without stopping the loop. - Refresh the site list on wp_initialize_site, wp_update_site and wp_delete_site for long-running processes. - Autoload any site plugin's classes on demand (queued jobs, config values from other sites), with a resolver of our own so it also works under `--classmap-authoritative`. - site:assign-keys stops with instructions when the network has no blogmeta table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JvSQuyBbpZMsUNeUjNtWa
Remove the library's runtime autoloading: the PSR-4 resolver, the cross-site plugin fallback autoloader, and the implicit `src/` mapping for site plugins without a composer.json. Every site, theme and site-plugin class is now autoloaded by Composer on every site (`app/Sites/*` via the project's `App\` mapping; themes and site plugins via wikimedia/composer-merge-plugin). Loading a class has no side effects; per-site behaviour comes only from which providers SiteLoader registers, which is unchanged. SiteManifest now only lists provider, composer and command class names. site:check reports the one way autoloading can have cross-site side effects, `autoload.files` in a theme or site plugin, and PSR-4 prefixes Composer does not know (a missing merge-plugin include). Requirement checks now cover site plugins as well as themes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JvSQuyBbpZMsUNeUjNtWa
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.
Multisite support for Acorn: site keys, per-site providers and config, code discovery, and fleet commands.
Site keys and model (first commit)
Tenancy\SiteKey: stable, environment-independent site identifier, stored assite_keyblogmeta; a lowercase DNS label.fromHost()uses the same leftmost-label rule as DemapDomains;wwwis reserved.Database\Models\Site,Tenancy\SiteRepository(find by key or ID),Tenancy\SiteMeta,Tenancy\Domain.site:list,site:assign-keys(--dry-run;--id=N --key=foo [--force]).Per-site providers and config (second commit)
Tenancy\TenancyServiceProvider, registered viaextra.acorn.providers. It does nothing on single-site installs. On multisite, from an$app->booting()callback (after every configured provider registers, before any boots):app/Sites/{Key}/Providers→ active site pluginplugins/{key}/→providersinconfig/sites/{key}/app.php→ parent theme → child theme (theme providers fromcomposer.jsonextra.acorn|laravel.providers). Deferred providers keep their deferral.app/Sites/{Key}/View/Composers(a composer extending a shared one replaces it via a container binding), registersapp/Sites/{Key}/Console/Commands, prependsresources/views/sites/{key}, and loadsroutes/sites/{key}.php(skipped when routes are cached).config/sites/{key}/mail.phploads assites.{key}.mail(so it is included inconfig:cache) and overlaysmail. Values apply leaf by leaf: maps merge, lists replace,nullremoves. This makesroots/acorn-mail,roots/acorn-post-typesandroots/acorn-prettifyconfigurable per site without changes to them. Timing matters: theirmergeConfigFrom()is a shallow merge inregister(), so the overlay must come after.switch_blogafter Acorn's own handler (so Acorn'scache.prefix/session.cookiesurvive). Providers do not change on a switch.Events:
SiteInitialized,SiteSwitched(also WP actionskleinweb/tenancy/initializedandkleinweb/tenancy/switched). Optionalconfig/tenancy.php:bootstrappers, andmeta_config(blogmeta key → config key).Commands:
site:run "<cmd>"(onewp --url=… acornprocess per site),site:check(keys, orphaned site code/overlays, missing providers, theme requirements vs the root lock, cached routes; non-zero exit),site:cache/site:clear(also run byoptimize/optimize:clear),make:site {key}.SiteQueue::name($site)gives the queue name for per-site workers.Changes to existing code:
app:tenancy:demap-domainsis renamedsite:demap-domains; the old name stays as an alias.wp search-replacethrough symfony/process. TheProcessfacade'silluminate/processisn't installed by Acorn, so that path would have failed at runtime.AppServiceProviderto the new provider.switch_to_blog()safety (third commit)Behaviour mirrors Core: code is per process, data follows the switch.
Per-blog Laravel cache that follows switches. This works around two Acorn gaps:
cache.prefixper blog doesn't reach the file store (FileStore::getPrefix()is'').CacheManagerhas already built never see a later prefix change.CacheBootstrapperuses per-blog file directories, re-prefixes built stores in place, and swaps array stores per blog.Nothing escapes the
switch_bloghandler. Core has already switched its globals by then. Errors, including fromSiteSwitchedlisteners, are reported and skipped. Failures at boot fall back to plain WordPress.Site::run()always unwinds to its starting depth. This holds on exceptions and on callbacks that leave switches unrestored.New
InteractsWithSites::forEachSite()for console commands.Other fixes:
SiteInitializednow fires after boot.site:assign-keyschecks for blogmeta support.Note for deploys: the file cache moves to
{path}/blog_{id}, which causes a one-time cold cache.cache:clearnow clears only the current blog's file cache; usesite:run "cache:clear"for all.Tested in plain PHP:
CacheManager: file, array, prefixed, and own-prefix stores, plus injected repositories.Site::run()unwinding.Class loading (fourth commit)
The library no longer does any autoloading of its own. Composer autoloads every class for every site:
app/Sites/*through the project'sApp\mapping.wikimedia/composer-merge-plugin.Loading a class has no side effects; per-site behaviour comes only from which providers are registered.
site:checkreportsautoload.filesin a theme or site plugin, because those run on every site. It also reports PSR-4 prefixes Composer doesn't know, which means a missing merge-plugin include.Notes for review
null: Laravel's config repository can't unset a top-level key.route:cache;site:checkreports it.switch_to_blog()calls.Verification
Static checks only; not run against a WordPress install.
parallel-lint,php-cs-fixer,phpcs: clean.DemapDomains.php:143error is fixed.SiteKey/DomainandConfigOverlayexercised in plain PHP against a realIlluminate\Config\Repository. Covered: merge, list replace, null removal, revert, and a runtimecache.prefixchange surviving revert.🤖 Generated with Claude Code
https://claude.ai/code/session_014JvSQuyBbpZMsUNeUjNtWa
Generated by Claude Code