Queries and Views¶
Scope your own queries and views to a site, add the Sites filter and column to a view, and know what a site's host does to queries without your help.
The site builder's view of the same options is Blocks and views.
What a site's host does on its own¶
- Entity queries. Where the site's Filter listings by the active
site setting (
filter_views) is on,SitesEntityQueryHooksscopes every entity query of a relationship type withaccessCheck(TRUE), JSON:API collections included, to the site's content. The hub andaccessCheck(FALSE)queries stay unscoped. - Views follow their Filter by site option.
- Content versions: with Sites content overrides, entity queries and views read the version the site serves (Content versions).
Results that depend on the site vary by the site cache context; a listing
of one site's content depends on MembershipCacheTags::site($site_id). See
Caching.
QueryableSiteInterface¶
A Site plugin that can narrow a query to its content implements
Drupal\sites\QueryableSiteInterface. SiteWithContentBase implements it
with no-op defaults; ContentSite joins the site_content_relationship
rows.
| Method | Does |
|---|---|
queryAlter($query, $entity_type) |
keeps the rows of the site's content |
supportsCanonicalQuery(), canonicalQueryAlter($query, $entity_type) |
keeps the content whose canonical site this is |
supportsMultiSiteQuery(), multiSiteQueryAlter($query, $entity_type, $sites, $canonical_only) |
scopes to several sites of the same class in one join |
The methods add joins and conditions only, never ORDER BY or a range:
Views combines several sites with UNION, and a UNION branch takes neither.
A plugin that overrides queryAlter() keeps multiSiteQueryAlter() in step
or answers FALSE in supportsMultiSiteQuery(). ContentSite needs the
entity's data table or base table in the query and leaves other queries
unchanged.
Scope your own entity query¶
To scope a query whatever the site's setting says, tag it and call the active site in the tag's alter hook:
$nids = \Drupal::entityQuery('node')
->accessCheck(TRUE)
->condition('type', 'event')
->addTag('my_module_site_scoped')
->execute();
namespace Drupal\my_module\Hook;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\sites\QueryableSiteInterface;
use Drupal\sites\SiteProxyInterface;
final class MyModuleQueryHooks {
public function __construct(
private readonly SiteProxyInterface $currentSite,
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
#[Hook('query_my_module_site_scoped_alter')]
public function scopeToSite(AlterableInterface $query): void {
$site = $this->currentSite->getSite();
if ($query instanceof SelectInterface && $site instanceof QueryableSiteInterface) {
$site->queryAlter($query, $this->entityTypeManager->getDefinition($query->getMetaData('entity_type')));
}
}
}
On the hub the active site is no QueryableSiteInterface and the query stays
unscoped. A database query on the entity's tables takes the same call.
The Filter by site query option¶
The query extender SitesContentSql (#[ViewsQueryExtend(id:
'sites_content')]) needs the contrib
Views Query module, which runs
every extender on every SQL view. It adds Filter by site to a view's
Query settings, stored per display:
display:
default:
display_options:
query:
type: views_query
options:
sites_content: enabled
| Value | Scope |
|---|---|
'' (Site default) |
On a site's host: the site's content when its filter_views setting is on, and always for the admin overviews of sites.settings:overview_views. On the hub: the sites chosen in the Sites filter; for an account without a real global administer sites, its own sites. |
enabled |
always the active site, or on the hub the chosen or the account's sites |
disabled |
never |
Site default scopes only views of a relationship type. Where scoping
applies and no site resolves, for example a forged filter value, the query
gets 1 = 0 and lists nothing. Several sites become one join per Site plugin
class (multiSiteQueryAlter()) or UNION legs, with DISTINCT against
duplicates. The extender adds the site, user.permissions, user.roles,
user.sites and url.query_args:canonical_only cache contexts and the
membership tags of the sites in reach.
The cache plugin Tag based, per site (sites_tag) drops a cached
listing of one site's content only when that site's content changes.
The Sites filter and field¶
SitesScopedViewsHooks::viewsDataAlter() adds two handlers to the base table
of every entity type sites can hold (node, users, media, ...):
- Sites filter (
Drupal\sites\Plugin\views\filter\Sites, idsites): carries the site choice and adds no SQL of its own; the extender reads it. Exposed on the hub, it offers the sites the viewer can access, as asite_selectelement (Forms), under the identifiersite_filter; on a site's host the host is the scope. Expose canonical site checkbox (expose_canonical) adds Originating content only, which switches the scoped sites tocanonicalQueryAlter(). - Sites summary field (
Drupal\sites\Plugin\views\field\Sites, idsites): renders the canonical site and a count of the other sites, with the list behind it. It readssites.membershipand adds no SQL.
Both handlers hide themselves off the hub (hide_if_not_default) and for a
viewer who can access one site only (hide_if_single); both options are on
by default.
The base tables of site-field types (redirects, path aliases) get the filter
only: their admin section scopes the listing on the
site_id column.