# Adding New Submodules

This guide walks through creating a new taxonomy-backed submodule. Follow the pattern established by `wow_playable_class` (the simplest reference).

## File checklist

```
modules/wow_<entity>/
├── wow_<entity>.info.yml          # Module metadata + dependencies
├── wow_<entity>.install           # Uninstall hook (state cleanup)
├── wow_<entity>.services.yml      # API + Sync services
├── drush.services.yml             # Drush command service
├── README.md                      # Admin-facing quick reference
├── config/
│   ├── install/
│   │   └── taxonomy.vocabulary.wow_<entity>.yml
│   └── optional/
│       ├── field.field.taxonomy_term.wow_<entity>.wow_blizzard_id.yml
│       ├── field.field.taxonomy_term.wow_<entity>.wow_last_fetched.yml
│       └── field.field.taxonomy_term.wow_<entity>.wow_<custom>.yml
├── src/
│   ├── Api/<Entity>Api.php
│   ├── Sync/<Entity>Sync.php
│   ├── Commands/<Entity>Commands.php
│   └── Plugin/DashboardSectionProvider/<Entity>Status.php
└── tests/
    └── src/
        ├── Unit/
        │   ├── Api/<Entity>ApiTest.php
        │   └── Sync/<Entity>SyncTest.php
        └── Kernel/
            └── Plugin/DashboardSectionProvider/<Entity>StatusTest.php
```

## Base classes

The core module provides base classes that eliminate most boilerplate:

| Your class | Extends | What you implement |
|---|---|---|
| `<Entity>Api` | `GameDataApi` | `getIndex()`, `get<Entity>()` — just the endpoint paths |
| `<Entity>Sync` | `TaxonomySyncBase` | `$vocabularyId`, `$stateKeyPrefix`, `mapFields()` |
| `<Entity>Commands` | `TaxonomySyncCommandBase` | `stateKeyPrefix()`, `entityLabel()`, the `#[CLI\Command]` attribute |
| `<Entity>Status` | `TaxonomyDashboardProviderBase` | 5 metadata methods: vocabulary ID, state key, title, label, service ID |

## Shared field storages

`wow_blizzard_id` and `wow_last_fetched` field storages are shared across all taxonomy submodules. Do NOT create new `field.storage.taxonomy_term.wow_blizzard_id.yml` files — only create `field.field.*.yml` instances for your vocabulary.

If you need a new custom field (e.g., `wow_role`), create both the storage and instance YAMLs in `config/optional/`.

## Install hook

Use the core helper to clean up state keys on uninstall:

```php
function wow_<entity>_uninstall(): void {
  wow_uninstall_state_keys('wow.last_modified.<entity>_index');
}
```

## Adding character integration

If your data type has per-character tracking (like mounts, titles, reputation):

1. Create a junction entity in `src/Entity/` using the `#[ContentEntityType]` attribute. Declare NO owner field — `wow_character` projects `character` in at runtime. Do declare `last_fetched` as a base field; the TTL sweeper and `TtlSweepCoverageTest` depend on it.
2. Create a `CharacterDataProvider` plugin in `src/Plugin/CharacterDataProvider/` that fetches the profile sub-endpoint and upserts junction rows. Implement `deleteCharacterData()` too — it runs on character delete for ToS-cascade compliance.
3. Register the new entity type ID in **two** places that must stay in sync:
   - the `match` expression in `wow_character.module`'s `hook_entity_base_field_info()` (runtime projection), and
   - `_wow_character_owned_entity_types()` in `modules/wow_character/wow_character.install` (install/uninstall lifecycle).
4. Register the entity type ID in `TtlSweeper::SWEEP_ENTITY_TYPES`.
5. If the same junction is guild-ownable, add it to `wow_guild.module` and `_wow_guild_owned_entity_types()` the same way.
6. Add tests: standalone entity round-trip, owner-injection ordering (paths A and B), full `CharacterDataProvider` sync/delete, and a uninstall-lifecycle test.

See `wow_title` as the reference implementation for the single-owner case; `wow_achievement` for a junction that is both character- and guild-ownable.

All character page contributions go through `CharacterDataProvider` plugins via `buildPageSections()`. See [character data providers](character-data-providers.md#building-page-sections).

## Testing conventions

- PHPUnit attributes (`#[CoversClass]`, `#[Group]`) — no docblock annotations.
- `#[RunTestsInSeparateProcesses]` on all kernel tests.
- `// cspell:disable-next-line` before WoW proper nouns in test fixtures.
- US English spelling only.

## See also

- [Character data providers](character-data-providers.md) — full contract + page-alter hook for per-character integrations.
- [Dashboard providers](dashboard-providers.md) — how the dashboard section you plug in is rendered.
- [API compliance](api-compliance.md) — the `last_fetched` + TTL-sweep requirements every new entity must honor.
