Skip to content

Hooks and events

Find the hook or event your module implements to react to sites, their content and their settings.

Services, plugins, hooks, events lists every hook and event with its module.

Hooks

Each module documents its hooks in its *.api.php file: sites.api.php, modules/sites_content/sites_content.api.php and modules/sites_ui/sites_ui.api.php in the Sites project, and the files of Sites clone, Sites content overrides, Sites Metatag and Sites Pathauto. Implement them as OOP hooks, a #[Hook] method on a class in src/Hook.

To Implement Module
Let sites hold an entity type your module defines hook_sites_content_entity_types_alter() sites
Show its entities that no site holds on every site hook_sites_shared_pool_content_entity_types_alter() sites
Serve a route on a site, or answer 404 there hook_site_access_alter(), hook_site_SITE_ID_access_alter() sites
Keep a setting from passing to sub-sites hook_sites_inheritance_ignore_alter() sites
Delete your data of a site while the site is deleted hook_sites_site_delete_operations_alter() sites
Mark sites in the list of an entity's sites hook_sites_entity_site_badges_alter() sites
Scope a view built at runtime as a content overview hook_sites_overview_views_alter() sites
Change discovered plugins hook_site_info_alter(), hook_site_setting_info_alter(), hook_sites_launch_step_info_alter(), hook_site_admin_section_info_alter() sites
Report a problem on the Sites status report hook_sites_status(), hook_sites_status_alter() sites_ui
Add to a site's dashboard what is no section card hook_site_dashboard_alter() sites_ui
Keep entities your module holds back, like trashed ones, out of a site's content counts and reports hook_sites_content_inventory_query_alter() sites_content

The hooks of Sites content overrides are on Satellite APIs, the content entity type hooks on Site-aware entity types, and section cards on Extension points.

hook_site_info_alter() and the two content entity type hooks run while Sites builds the site definitions and settings: do not load sites in them.

The site access hooks pass the result by reference. orIf() and andIf() return a new object, so assign it:

#[Hook('site_access_alter')]
public function siteAccessAlter(AccessResultInterface &$access, AccountInterface $account, array &$context): void {
  if ($context['route_match']->getRouteName() === 'my_module.status') {
    $access = $access->orIf(AccessResult::allowed());
  }
}

Delete data your module keeps per site in the delete batch, one request per operation. The site entity is deleted after your operations run. Keep hook_ENTITY_TYPE_predelete() for site as the fallback for a plain $site->delete(); SitesContentOverridesSiteCleanupHooks in Sites content overrides does both.

#[Hook('sites_site_delete_operations_alter')]
public function siteDeleteOperationsAlter(array &$operations, SiteInterface $site): void {
  $operations[] = [[MySiteDataBatch::class, 'purge'], [$site->id()]];
}

Events

Subscribe with a service tagged event_subscriber.

Event Constant Dispatched
sites.initialize InitialSiteEvent::EVENT_NAME Once per main request and before each Drush command, to pick the site.
sites.settings_build SiteSettingsEvent::BUILD Once per site instance, when its settings are resolved.
sites.content.add, sites.content.remove ContentEvent::ADD_EVENT_NAME, ContentEvent::REMOVE_EVENT_NAME After an entity joins or leaves a site.
sites.content.canonical ContentEvent::CANONICAL_EVENT_NAME After an entity gets its first or a new canonical site.

InitialSiteEvent

The first subscriber that calls setInitialSite() wins; a request nobody claims runs on the hub. Sites claims sub-sites by path prefix at priority 10 and sites by hostname at 0. Subscribe above 0 to take precedence over the hostname, and return when a site is set. The event runs at kernel request priority 266, before routing: there is no route match yet.

Pick the site from the hostname and the path only. The anonymous page cache answers before this event, and keys its entries by URL.

final class HostMapSubscriber implements EventSubscriberInterface {

  public function __construct(
    private readonly RequestStack $requestStack,
    private readonly SitePluginManagerInterface $siteManager,
  ) {}

  public static function getSubscribedEvents(): array {
    return [InitialSiteEvent::EVENT_NAME => ['onInitialSite', 20]];
  }

  public function onInitialSite(InitialSiteEvent $event): void {
    $host = $this->requestStack->getCurrentRequest()?->getHost();
    $site_id = Settings::get('my_module_hosts', [])[$host] ?? NULL;
    if ($event->getInitialSite() === NULL && $site_id !== NULL && ($site = $this->siteManager->getSite($site_id))) {
      $event->setInitialSite($site);
    }
  }

}

For further hostnames of one site, its hostname aliases need no code; see Hostnames and sub-sites.

SiteSettingsEvent

The event runs after the stored, translated, environment and config-target values are merged. Sites subscribes twice: main-site inheritance at priority 120 and the settings.php values at 0. The last subscriber wins, so subscribe above 0 to leave settings.php the last word.

The event carries no cache metadata. Compute values from the site and from what changes only with a deployment: output that reads a setting carries the cache metadata of the site alone.

public static function getSubscribedEvents(): array {
  return [SiteSettingsEvent::BUILD => ['onSettingsBuild', 10]];
}

public function onSettingsBuild(SiteSettingsEvent $event): void {
  if (empty($event->getSetting('my_module_contact'))) {
    $event->setSetting('my_module_contact', 'info@' . $event->getSetting('hostname'));
  }
}

For fixed defaults, a setting plugin or the site type is the place.

ContentEvent

The membership service (sites.membership) dispatches it with the site and the entity. sites.content.add follows a new membership, sites.content.remove a deleted one. For sites.content.canonical, getSite() is the new canonical site and getPreviousSite() the old one, NULL for the first.

Only memberships stored as relationship rows dispatch. Entity types that carry their site on the entity, like a site_id field, dispatch nothing: their save is the event. A site plugin whose dispatchContentEvents() returns FALSE dispatches nothing either.

final class SitemapQueueSubscriber implements EventSubscriberInterface {

  public function __construct(
    private readonly QueueFactory $queueFactory,
  ) {}

  public static function getSubscribedEvents(): array {
    return [
      ContentEvent::ADD_EVENT_NAME => 'onMembershipChange',
      ContentEvent::REMOVE_EVENT_NAME => 'onMembershipChange',
    ];
  }

  public function onMembershipChange(ContentEvent $event): void {
    $entity = $event->getEntity();
    $this->queueFactory->get('my_module_sitemap')->createItem([
      'site_id' => $event->getSite()->id(),
      'entity_type_id' => $entity->getEntityTypeId(),
      'entity_id' => $entity->id(),
    ]);
  }

}
services:
  my_module.sitemap_queue_subscriber:
    class: Drupal\my_module\EventSubscriber\SitemapQueueSubscriber
    autowire: true
    tags:
      - { name: event_subscriber }

AliasCleanupOnRemovalSubscriber in Sites path alias and AliasGenerationOnAddSubscriber in Sites Pathauto are real subscribers.