Extension points¶
Add a row to the launch checklist, a card to the site dashboard, a per-site admin section, an entry in the Sites status report, or a token.
| To add | Ship | Read |
|---|---|---|
| A launch checklist row | a #[LaunchStep] plugin |
sites/src/Plugin/LaunchStep/FrontPage.php |
| A per-site admin section | a #[SiteAdminSection] plugin |
sites_redirect, sites_robotstxt, sites_analytics |
| A dashboard card | the section's dashboard_card, or hook_site_dashboard_alter() |
sites_redirect's RedirectSection |
| A status report entry | hook_sites_status() |
sites_menu's SitesMenuStatusHooks |
| A token | a setting key, or hook_tokens() on current-site |
SitesTokensHooks |
The plugin managers and hooks are listed on
Services, plugins, hooks, events. The dashboard, the
checklist and the status report render in sites_ui; the APIs live in
sites.
Launch steps¶
A launch step is one row of the checklist on a site's dashboard. The manager
plugin.manager.sites_launch_step (LaunchStepPluginManagerInterface)
discovers #[LaunchStep] plugins in src/Plugin/LaunchStep/, keeps those
whose applies() answers TRUE for the site and the viewer, and orders them by
weight, then id. LaunchStepBase answers applies() with TRUE and has no
link; a step implements isSatisfied(), which reads the site's current state.
NULL means the state cannot be read: the row shows and counts toward
neither side of the progress. The dashboard is cached with the tags of the
sites, the site's stored overrides, memberships and role assignments. A step
whose isSatisfied() reads anything else (a config object, an entity, a
probe that is good for a while) implements CacheableLaunchStepInterface
and returns that dependency from
stateCacheability(SiteInterface $site): CacheableDependencyInterface; the
checklist card carries it, together with the access results of the rows'
links:
public function isSatisfied(SiteInterface $site): bool {
return (bool) $this->configFactory->get('newsletter.sent')->get($site->id());
}
public function stateCacheability(SiteInterface $site): CacheableDependencyInterface {
return $this->configFactory->get('newsletter.sent');
}
A plain step (LaunchStepInterface) links elsewhere with url() and
action(), like dns. A guided step (GuidedLaunchStepInterface) adds
fields and a submit that sites_ui wraps in a dialog at
/admin/sites/{site}/launch-step/{step}, gated by Manage site settings:
<?php
declare(strict_types=1);
namespace Drupal\sites_maintenance\Plugin\LaunchStep;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\sites\Attribute\LaunchStep;
use Drupal\sites\LaunchStep\GuidedLaunchStepInterface;
use Drupal\sites\LaunchStep\LaunchStepBase;
use Drupal\sites\LaunchStep\LaunchStepInterface;
use Drupal\sites\Section\SettingLayer;
use Drupal\sites\SiteInterface;
use Drupal\sites\SiteSetting\SiteSettingsInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Writes the site's maintenance message from the dashboard.
*/
#[LaunchStep(
id: 'maintenance_message',
label: new TranslatableMarkup('Write the maintenance message'),
weight: 50,
)]
final class MaintenanceMessage extends LaunchStepBase implements GuidedLaunchStepInterface, ContainerFactoryPluginInterface {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
private readonly SiteSettingsInterface $settings,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('sites.site_settings'));
}
/**
* {@inheritdoc}
*/
public function isSatisfied(SiteInterface $site): bool {
return trim((string) $site->getSetting('maintenance_message')) !== '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, SiteInterface $site): array {
$form['message'] = [
'#type' => 'textarea',
'#title' => $this->t('Maintenance message'),
'#default_value' => $site->getSetting('maintenance_message'),
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state, SiteInterface $site): void {
$layer = SettingLayer::base((string) $site->id());
$values = ['maintenance_message' => (string) $form_state->getValue('message')];
if ($errors = $this->settings->writeErrors('sites_maintenance', $layer, $values)) {
$form_state->set(LaunchStepInterface::OUTCOME, implode(' ', $errors));
return;
}
$this->settings->write('sites_maintenance', $layer, $values, 'Maintenance message set.');
$form_state->set(LaunchStepInterface::OUTCOME, $this->t('The maintenance message is set.'));
}
}
$form holds only the step's elements; the form object, the actions and the
dialog belong to sites_ui. The string under LaunchStepInterface::OUTCOME
shows as a message after the submit. A step that does not apply to a site has
no reachable form there. hook_sites_launch_step_info_alter() alters the
definitions.
Site admin sections¶
A section is an admin surface of one site under /admin/sites/{site}/…. A
#[SiteAdminSection] plugin in src/Plugin/SiteAdminSection/ extending
SiteAdminSectionBase declares everything on its attribute; the docblock of
Drupal\sites\Attribute\SiteAdminSection describes each argument. Three
shapes cover the shipped sections:
| Shape | Declares | Example |
|---|---|---|
| Setting section | setting_plugin, section_form: SiteSettingSectionForm::class |
sites_analytics (tutorial) |
| Clone of a core admin page | section_routes (SectionRoute(base, path, core_gate)), collection_route |
sites_robotstxt, sites_menu |
| Entity section | entity_type, scoped_entity_access, scoped_views, collection_view, route_form_overrides, entity_handlers |
sites_redirect |
Every section route carries _sites_admin_section_access: <id>; the cloned
routes get it from Sites. The check allows an account holding any of the
section's permissions on the site, platform-wide or through a per-site role.
core_gate: FALSE drops the core route's own _permission from a clone.
The collection_route becomes the entry point of the section. On a site's
host it serves the current site's section. On the hub it sends a delegate of
one site into that site's section and offers a delegate of several sites a
site chooser. A platform administrator gets the aggregate list of a content
section, and for a configuration section (config_surface: TRUE) the chooser
plus Global, the platform's own page. A configuration section of config
entities (menus, vocabularies) implements ownsConfigEntity(), which decides
which of them are the site's. hook_site_admin_section_info_alter() alters
the definitions.
Dashboard cards¶
A section's card is its dashboard_card: a SectionCard with the route it
opens, a DashboardGroup (Content, Structure, Search, Regional,
People, System, Settings, Reports), a weight and a description. The
dashboard shows the card when the viewer may open the route. A section that
implements SectionSummaryInterface adds a line under the title from
dashboardSummary(); the render array carries the cache tags of what it read.
Anything else on the dashboard goes through hook_site_dashboard_alter(array
&$build, SiteInterface $site). $build holds site (the Site panel),
launch_checklist and cards.
Status report entries¶
The Sites status report at /admin/sites/reports/status collects
hook_sites_status(): entries in core's requirements format, keyed by a
module-prefixed string, with a RequirementSeverity case.
hook_sites_status_alter() changes another module's entries.
<?php
declare(strict_types=1);
namespace Drupal\sites_maintenance\Hook;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Reports the maintenance mode on the Sites status report.
*/
final class SitesMaintenanceStatusHooks {
use StringTranslationTrait;
public function __construct(
private readonly StateInterface $state,
) {}
/**
* Implements hook_sites_status().
*/
#[Hook('sites_status')]
public function sitesStatus(): array {
$on = (bool) $this->state->get('system.maintenance_mode');
return [
'sites_maintenance_mode' => [
'title' => $this->t('Maintenance mode'),
'value' => $on ? $this->t('On: every site shows its maintenance message.') : $this->t('Off'),
'severity' => $on ? RequirementSeverity::Warning : RequirementSeverity::OK,
],
];
}
}
Tokens¶
Each setting key is a token without code: [site-settings:KEY], and
[current-site:settings:KEY] for the site the page is on. The key's label
names it; a sensitive key has none. A token computed from more than one
value is core's hook_token_info() and hook_tokens() on the current-site
type. Read the site from $data['site']; without it, read current_site and
add the site cache context to the bubbleable metadata, as
SitesTokensHooks::tokens() does. The token list is
generated from the installed modules.