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:
- Iterates all providers, calls
buildPageSections()on each. - Groups the returned
CharacterPageSectionobjects by slot name. - Sorts within each slot by weight.
- Wraps each section in the
wow_character_sectiontheme 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_IDwow_character_section__SLOT_NAMEwow_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.