Content versions¶
Load, save and query the version of content a site serves, and reach the canonical version where your code means it.
Needs Sites content overrides
Part of Sites content overrides.
A site can keep its own version of content whose canonical site is another
site: an override (Content overrides).
It is a revision of the entity carrying the site's id in site_id and the
fields it pins in site_override_fields. The canonical version is the
default revision, without a site_id. Each override revision is a full
snapshot of the entity, so storage grows with every site that overrides it.
Which version code gets¶
sites.content_version_context
(Drupal\sites\ContentVersionContextInterface) decides it for every load,
save and query: versionSite() returns the site whose version applies, or
NULL for the canonical one.
| Where the code runs | Version |
|---|---|
| A site's host, content the site overrides | the site's override |
| A site's host, any other content | canonical |
| The hub | canonical |
| JSON:API and REST requests | canonical (CANONICAL_REQUEST request attribute) |
| Batch API operations, cron | canonical |
Inside executeCanonical() |
canonical, until a site switch inside it |
Code that means the content as such, on any host, runs in the canonical scope:
$versions = \Drupal::service('sites.content_version_context');
$node = $versions->executeCanonical(fn () => $node_storage->load($nid));
A site switch inside the callback brings that site's version back for as long as the switch lasts.
Load¶
Where versionSite() names a site, load() and loadMultiple() deliver the
revision the site serves: its newest override revision, unless
hook_sites_content_overrides_effective_override_alter() picks another or
none. The content moderation submodule uses that hook to keep serving the
published override while a draft is in progress.
Override revisions are the newest revisions by number, yet what core calls
the latest revision (getLatestRevisionId(), isLatestRevision(), an entity
query with latestRevision()) skips them: it is the default revision or a
pending draft of it. Code that edits the latest revision, like JSON:API's
working copy or tool_belt:entity_update, edits the canonical entity, and
the sites keep what they pin. To read what a site serves, load through
entity.repository->getActive(), which answers for the site in context.
The resolver (sites_content_overrides.site_aware_entity_resolver,
SiteAwareEntityResolverInterface) answers the other questions:
| Method | Returns |
|---|---|
loadDefaultRevision($entity_type_id, $id) |
the canonical version, wherever the code runs |
loadSiteOverrideRevision($entity, $site) |
the newest override revision, the one editing continues on |
loadEffective($entity, $site) |
what $site serves |
servedRevisionIds($entity_type_id, $ids, $site) |
the served override revision ids, without loading |
Save¶
On the host of a member site that is not the content's canonical site,
$entity->save() writes that site's override and creates it when the site
has none. Only the fields the site may override keep the values the code
set; every other field takes the canonical value. On the hub, on the
canonical site's host and for content the site does not hold, a save is
canonical. To change the canonical version from a site's host, save inside
executeCanonical().
Query¶
Entity queries and views on a site's host filter, sort and count on the
version the site serves, the version a load delivers: a site that
unpublishes its override does not list the item. A query for revisions
(allRevisions()) keeps the canonical tables.
The swap runs as the last alter of entity_query and views queries,
after node access and the latest-revision alters. It replaces only what the
site's overrides pin, each with a derived table of the same columns:
- the columns of pinned fields in the data table (a pinned
moderation_statealso swaps the published flag); - the dedicated tables of pinned fields.
Every other table and column stays canonical, indexes included. Only revisionable SQL entity types with integer ids are swapped.
The served-revision table¶
The swap joins sites_content_overrides_served, which holds the revision
each site serves per entity:
| Column | Content |
|---|---|
entity_type_id |
the entity type, like node |
site_id |
the site that serves an override |
entity_id |
the entity id |
revision_id |
the revision the site serves |
The primary key is (entity_type_id, site_id, entity_id). There is no
row for the canonical site, for a site without an override, or for an
override the site does not serve (a draft). An insert, update or revision
delete of an overridable entity rewrites its rows once the root transaction
commits, at the latest before the next read or at the end of the request.
Your own SQL¶
A query that is not an entity query or a view reads the canonical tables. To
read what site globex serves, join the table the way the swap does:
$query = $database->select('node_field_data', 'd');
$query->leftJoin('sites_content_overrides_served', 's',
"[s].[entity_type_id] = 'node' AND [s].[site_id] = :site AND [s].[entity_id] = [d].[nid]",
[':site' => 'globex']);
$query->leftJoin('node_field_revision', 'r',
'[r].[vid] = [s].[revision_id] AND [r].[langcode] = [d].[langcode]');
$query->addField('d', 'nid');
$query->addExpression('COALESCE([r].[title], [d].[title])', 'title');
Overridable entity types¶
sites_content_overrides.settings:override_entity_types lists the entity
types sites may override. Taking a type off the list uninstalls its
site_id and site_override_fields fields and deletes every override of the
type. A configuration import that would do so while overrides of the type
exist fails validation.