Skip to content

Plugin systems

Three plugin managers drive the suite's extension points: DashboardSectionProvider (admin UI), CharacterDataProvider (per-character data sync + page rendering), and SearchableDataProvider (unified search). All follow the same pattern — discovery via PHP 8 attribute, value-object return, no markup in plugins.

DashboardSectionProvider

Purpose: every submodule contributes a section to the admin dashboard at /admin/reports/wow.

Attribute: #[DashboardSectionProvider(id: 'unique_id', label: t('Label'))] Interface: Drupal\wow\DashboardSectionProviderInterface Manager service: wow.dashboard_section_provider_manager

Contract

Plugins return structured value objects (DashboardSection, DashboardRow, DashboardAction) — never HTML. The DashboardRenderer owns all markup, so a future theme change to the dashboard doesn't ripple out to every submodule.

#[DashboardSectionProvider(
  id: 'wow_realm',
  label: new TranslatableMarkup('Realms'),
)]
class RealmStatus extends TaxonomyDashboardProviderBase {
  // Only 5 metadata methods — vocabulary ID, state key prefix,
  // section title, label text, service ID for the sync service.
}

Base classes

Base class When to use
DashboardSectionProviderBase Catalog or custom sections (see AchievementStatus, CharacterStatus)
TaxonomyDashboardProviderBase Reference-data modules — boilerplate for row counts, last-sync timestamps, "Sync now" action

Actions

Each row can declare DashboardAction objects — dropdown entries routed to the plugin's executeAction() method. The DashboardActionController confirms destructive actions via a shared confirm form before executing.

See development/dashboard-providers.md for a how-to.

CharacterDataProvider

Purpose: when a character is synced, every registered provider gets a chance to fetch extra data for that character and save it in its own entity.

Attribute: #[CharacterDataProvider(id: 'provider_id', label: t('Label'))] Interface: Drupal\wow_character\CharacterDataProviderInterface Manager service: wow_character.character_data_provider_manager

Contract

Each provider implements three methods:

public function syncCharacterData(Character $character, string $region): int;
public function deleteCharacterData(Character $character): void;
public function buildPageSections(Character $character): array;

syncCharacterData() is called by CharacterSync::invokeDataPlugins() after the base character entity is saved. Return an integer — how many rows the provider wrote. Exceptions are caught by the sync layer; one provider failing doesn't abort the others.

deleteCharacterData() is called by wow_character_entity_predelete() when a character is deleted. Same isolation guarantee: one provider throwing doesn't block the rest.

buildPageSections() returns an array of CharacterPageSection value objects that contribute render content to the character canonical page. Called by CharacterPageRenderer during page rendering. The base class provides a no-op default — override only if your module has page content to contribute.

Current implementations

Plugin Module Endpoint Junction entity
achievements wow_achievement /profile/wow/character/{realm}/{name}/achievements wow_achievement_progress
mounts wow_mount /profile/wow/character/{realm}/{name}/collections/mounts wow_mount_collection
pets wow_pet /profile/wow/character/{realm}/{name}/collections/pets wow_collected_pet
titles wow_title /profile/wow/character/{realm}/{name}/titles wow_title_award
toys wow_toy /profile/wow/character/{realm}/{name}/collections/toys wow_toy_collection
reputations wow_reputation /profile/wow/character/{realm}/{name}/reputations wow_reputation_standing

GuildDataProvider (parallel pattern)

wow_guild provides an identical plugin manager + interface for guild-scoped data (GuildDataProviderInterface, wow_guild.guild_data_provider_manager). Same contract, same isolation guarantees. No in-tree implementations yet; the extension point is wired and tested.

Character page rendering

The CharacterPageRenderer service (ID: wow_character.page_renderer) collects page contributions from all CharacterDataProvider plugins:

  1. Iterates all providers, calls buildPageSections() on each.
  2. Groups the returned CharacterPageSection objects by slot name.
  3. Sorts within each slot by weight.
  4. Wraps each section in the wow_character_section theme hook.

CharacterPageSection value object

new CharacterPageSection(
  slotName: CharacterPageSlot::COLLECTIONS,
  content: $renderArray,
  weight: 10,
);

CharacterPageSlot constants

Constant Value Page position
TITLE_RIBBON title_ribbon Between the hero banner and stats bar
COLLECTIONS collections Collection summary cards
ACHIEVEMENTS achievements Achievement highlights
EQUIPMENT equipment Gear and equipment display
GENERIC generic Fallback area for sections with no specific position

Custom slot names are also supported — they render in the generic fallback area.

Template suggestions

The wow_character_section theme hook generates suggestions in increasing specificity:

  • wow_character_section__PLUGIN_ID
  • wow_character_section__SLOT_NAME
  • wow_character_section__SLOT_NAME__PLUGIN_ID

See development/character-data-providers.md for a how-to.

SearchableDataProvider

Purpose: a unified search system that exposes WoW entity types to the CKEditor integration, autocomplete endpoints, and future search consumers.

Attribute: #[SearchableDataProvider(id: 'type_id', label: t('Label'), weight: 0)] Interface: Drupal\wow\SearchableDataProviderInterface Manager service: wow.searchable_data_provider_manager

Contract

Plugins implement four methods:

public function getSearchLabel(): string;
public function search(string $query, string $region, int $limit = 10): array;
public function getCanonicalUrl(int $blizzardId): ?Url;
public function importEntity(int $blizzardId, string $region): ?Url;

search() returns SearchResult value objects — readonly structs with blizzardId, name, type, isLocal, iconUrl, typeLabel, description, quality. Plugins search local storage first and optionally fall back to the Blizzard Search API.

getCanonicalUrl() returns the internal URL for an entity's detail page. importEntity() syncs a remote entity on demand and returns its URL.

Orchestration

The SearchOrchestrator service (wow.search_orchestrator) wraps the plugin manager with budget allocation (distributes the result limit across providers for diversity), failure isolation (one provider throwing doesn't abort the rest), and typeLabel stamping.

Current implementations

Plugin Module Remote fallback
items wow_item Yes
creatures wow_creature Yes
achievements wow_achievement No
quests wow_quest No
mounts wow_mount No
pets wow_pet No
toys wow_toy No
titles wow_title No

See development/search-providers.md for a how-to.

Character page extension

The CharacterPageRenderer is the sole extension mechanism for the character canonical page. All page sections come from CharacterDataProvider plugins via buildPageSections(), organized into named slots (TITLE_RIBBON, COLLECTIONS, ACHIEVEMENTS, EQUIPMENT, GENERIC). There is no alter hook.

See development/character-data-providers.md for a how-to.