Skip to content

The current site

Read the site your code runs as, run code as another site, and give Drush, batch and queue work the site it belongs to.

Which site?

Question Ask
Which site does this code run as? current_site
Which site did the request arrive on? current_site->negotiated()
Which site does the route's {site} or the ?site_id query name? sites.pointed_site
Which sites hold an entity, and which is its canonical site? sites.membership->sitesOf(), canonicalSiteOf()

The request negotiates its site once, before routing; see Architecture and, for the matching rules, How a request finds its site. Sub-requests keep the site of the main request.

Read the current site

Inject current_site as Drupal\sites\SiteProxyInterface. It extends SiteInterface and answers every method (id(), label(), isHub(), getSetting(), getBaseUrl(), getCacheTags(), ...) as the active site:

use Drupal\sites\SiteProxyInterface;

final class RegionBadge {

  public function __construct(
    private readonly SiteProxyInterface $currentSite,
  ) {}

  public function build(): array {
    return [
      '#plain_text' => (string) $this->currentSite->getSetting('region_key'),
      '#cache' => ['tags' => $this->currentSite->getCacheTags()],
    ];
  }

}

Read through the proxy at the point of use. A site object you keep from getSite() stays the same site after a switch.

Negotiated and active site

  • negotiated() is the site the request arrived on: the site its host and path prefix name, or the hub. Nothing changes it for the rest of the request. "Did this request arrive on the hub?" is negotiated()->isHub().
  • getSite(), and every SiteInterface method on the proxy, is the active site: whose configuration, content and URLs apply. It is the negotiated site, except inside a switch. A preview switches for the whole request: it arrives on the hub and renders the previewed site.
  • isReady() is FALSE until a site was negotiated: early bootstrap, a Drush command before its pre-command hook, a kernel test that sets no site. The proxy then answers as the hub. Guard code that must not act on a site nobody chose.

Never call setInitialSite() or setSite(): they are the negotiation's and the switcher's. Tests set the site with SiteContextTrait; see Testing and debugging.

Run code as another site

SiteSwitcherInterface::call() (service site_switcher) makes a site active, runs the callable, switches back even when the callable throws, and returns the callable's result. It takes a SiteInterface or a site ID; an unknown ID is an error.

use Drupal\Core\Url;

$front = $this->siteSwitcher->call('acme', static fn (): string => Url::fromRoute('<front>')->setAbsolute()->toString());
// https://acme.example.com/

Inside the callable, the active site's configuration overrides, site cache context, query scoping, entity access and URL host apply. negotiated(), the request's theme and its languages stay as they were: the theme is negotiated once per request, and the switcher negotiates every language type before it leaves the request's site, so no language is first negotiated under the other site. Switches nest, and isSwitched() tells whether one is open. To link to another site without switching, pass the site_id URL option: Url::fromRoute('<front>', [], ['site_id' => 'acme']).

Drush, batch and queue

  • Drush. --uri negotiates like a request: drush --uri=https://acme.example.com php:eval "echo \Drupal::service('current_site')->id();" prints acme; without --uri the command runs on the hub. A path that is exactly a sub-site's prefix on that host runs the command in the sub-site (--uri=https://acme.example.com/sub). Any other path stays the base path of a Drupal installed in a subdirectory. A command that bootstraps below the full bootstrap negotiates no site.
  • Batch. Each batch request negotiates its site from the host the batch was started on. An operation that works on another site takes the site as an argument and switches with call(), as sites_clone's SiteCloneBatch::batchProcess() does.
  • Queue and cron. Cron runs at the end of whatever request triggers it, on the hub for drush cron without --uri. Put the site ID into the queue item and switch in the worker:
public function processItem($data): void {
  $this->siteSwitcher->call($data['site_id'], function () use ($data): void {
    // The site's email address, from its configuration overrides.
    $site_mail = $this->configFactory->get('system.site')->get('mail');
    $this->mailManager->mail('my_module', 'digest', $site_mail, 'en', $data);
  });
}

sites_simple_sitemap's QueueWorker::generateResultsFromElement() does this for each sitemap. To work on every site, loop over plugin.manager.site->getSites() and switch to each. Cron tasks and batch operations load the canonical version of content, and a site's version inside a switch; see Content versions.

The site cache context

The site cache context is the active site's ID and follows a switch. Sites adds it to the renderer's required cache contexts, so every render cache entry varies by site without declaring it. Declare it where you cache outside the render system, for example on a cacheable JSON response, and key a cache entry of your own by current_site->id(). A decision about the negotiated site varies by sites_negotiated_site instead.

Tag output with the site's cache tags (site:{id}) to clear it when the site changes. How to cache site-aware output: Caching; every context and tag: Cache contexts and tags.