Caching¶
Give your module's output the cache metadata that keeps one site's version off the other sites, and drops it when the site changes.
What Sites adds for you¶
siteis a required cache context.SitesServiceProvideradds it torenderer.config'srequired_cache_contexts, as Workspaces does forworkspace. Every render array, and so every HTML page, varies by the active site.- Configuration varies per site. A configuration object read through the
config factory carries the
sitecontext and the site's cache tags, soaddCacheableDependency($config)covers per-site configuration overrides. - The anonymous page cache keys by URL. Every hostname and every sub-site path prefix has its own entries.
Add site where the renderer does not¶
The renderer adds site only to what it renders. Add it yourself to:
- access results whose answer differs per site: route, entity, block and field access;
- cacheable responses that are no render array, like a
CacheableJsonResponse, a plain-textCacheableResponseor aCacheableNotFoundHttpException: Dynamic Page Cache adds only the route and the request format to them; - your own cache entries: put the site ID in the cache ID.
The site object is a cacheable dependency (SiteInterface extends
CacheableDependencyInterface): its contexts include site, and its tag is
site:<site_id>, which Sites invalidates when the site or its site type is
saved. Add it as a dependency wherever you read a setting:
#[Hook('block_access')]
public function blockAccess(BlockInterface $block, string $operation, AccountInterface $account): AccessResultInterface {
if ($block->getPluginId() !== 'my_module_banner') {
return AccessResult::neutral();
}
$site = $this->currentSite->getSite();
return AccessResult::allowedIf((bool) $site->getSetting('my_module_banner'))
->addCacheableDependency($site);
}
$site = $this->currentSite->getSite();
$response = new CacheableJsonResponse(['title' => $site->label()]);
$response->addCacheableDependency($site);
$cid = 'my_module:summary:' . $site->id();
Drupal\sites_robotstxt\Controller\SitesRobotsTxtController adds site to a
response that is no render array.
Which tag to carry¶
Sites invalidates these tags; your output carries the narrowest one that
describes what it read. Drupal\sites\Cache\MembershipCacheTags and
Drupal\sites\Cache\SiteAccessCacheability spell them.
| Your output read | Tag | Helper |
|---|---|---|
| A site's settings | site:<site_id> |
MembershipCacheTags::sitePlugin() |
| A site's hostname, aliases or prefix, like a link to it | site_host:<site_id> |
MembershipCacheTags::siteHost() |
| Which sites one entity belongs to | site_content_membership:<type>:<id> |
MembershipCacheTags::entity() |
| Which content belongs to one site | site_content_membership_site:<site_id> |
MembershipCacheTags::site() |
| A list of one entity type on one site | site_content_list:<type>:<site_id> |
MembershipCacheTags::siteList() |
| One account's per-site roles | site_role_assignment:user:<uid> |
SiteAccessCacheability::assignmentTag() |
| Which sites exist | sites_list |
The list tags site_content_relationship_list and site_role_assignment_list
change with any membership or role assignment on any site. They belong on hub
reports; on a site's page they drop the cache for writes on every other site.
Cache contexts and tags lists all tags and when they
are invalidated. For Views, use the Tag based, per site cache plugin
(Queries and Views).
Vary by what the viewer may do: user.sites¶
The user.sites context varies by the viewer's memberships and per-site
roles. Accounts with the same ones share an entry; the user context would
store one entry per account, and Dynamic Page Cache refuses to store a page
that varies by user. Use it with user.permissions:
SiteAccessCacheability::grantContexts() returns user.permissions,
user.roles and user.sites, and SiteAccessCacheability::grant() and
::entityCapability() return the full metadata of a per-site access
decision.
A module that grants per-site access from data of its own adds that data to
the context: a service tagged sites.user_sites_provider that implements
Drupal\sites\Cache\Context\UserSitesProviderInterface. Return a cheap,
stable string; do not evaluate access in it. Sites ships two providers, one
for role assignments and one (in Sites content) for memberships. This is the
first, SiteRoleAssignmentProvider:
public function value(AccountInterface $account): string {
$parts = [];
foreach ($this->siteRoleManager->getRoleMapForUser($account) as $site_id => $role_ids) {
$parts[] = $site_id . '=' . implode(',', $role_ids);
}
return 'roles:' . implode(';', $parts);
}
services:
my_module.user_sites_provider:
class: Drupal\my_module\Cache\MyUserSitesProvider
autowire: true
tags:
- { name: sites.user_sites_provider }
Two more site contexts¶
site follows the active site, which a site switch or a preview changes.
sites_negotiated_sitevaries by the site the request arrived on. Add it where you branch on$current_site->negotiated()->isHub(); addsitewhere you branch on$current_site->isHub().sites_context_sitevaries, on the hub, by the site a request acts in (its?site_id, or the canonical site of the route's entity). The access policy adds it; you rarely do.
Page cache and reverse proxies¶
The anonymous page cache answers before Sites picks the request's site. A site is therefore picked from the hostname and path alone; see Hooks and events. What a reverse proxy or CDN must do is on Caching and performance.
To check a page, set http.response.debug_cacheability_headers: true in
services.yml and read X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags.
Testing and debugging shows a functional test of the page
cache per hostname.