# 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.

```php
#[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](../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:

```php
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

```php
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](../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:

```php
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](../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](../development/character-data-providers.md) for a how-to.
