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:
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):
- Create a junction entity in
src/Entity/using the#[ContentEntityType]attribute. Declare NO owner field —wow_characterprojectscharacterin at runtime. Do declarelast_fetchedas a base field; the TTL sweeper andTtlSweepCoverageTestdepend on it. - Create a
CharacterDataProviderplugin insrc/Plugin/CharacterDataProvider/that fetches the profile sub-endpoint and upserts junction rows. ImplementdeleteCharacterData()too — it runs on character delete for ToS-cascade compliance. - Register the new entity type ID in two places that must stay in sync:
- the
matchexpression inwow_character.module'shook_entity_base_field_info()(runtime projection), and _wow_character_owned_entity_types()inmodules/wow_character/wow_character.install(install/uninstall lifecycle).- Register the entity type ID in
TtlSweeper::SWEEP_ENTITY_TYPES. - If the same junction is guild-ownable, add it to
wow_guild.moduleand_wow_guild_owned_entity_types()the same way. - Add tests: standalone entity round-trip, owner-injection ordering (paths A and B), full
CharacterDataProvidersync/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.
Testing conventions¶
- PHPUnit attributes (
#[CoversClass],#[Group]) — no docblock annotations. #[RunTestsInSeparateProcesses]on all kernel tests.// cspell:disable-next-linebefore WoW proper nouns in test fixtures.- US English spelling only.
See also¶
- Character data providers — full contract + page-alter hook for per-character integrations.
- Dashboard providers — how the dashboard section you plug in is rendered.
- API compliance — the
last_fetched+ TTL-sweep requirements every new entity must honor.