Lifecycle¶
Install, uninstall, TTL, delete cascades — how state gets created, maintained, and torn down.
Owner projection¶
Owner modules (wow_character, wow_guild, wow_user) project reference fields onto entities owned by OTHER modules.
The flow has two paths that produce the same end state:
Path A — owner installed FIRST¶
wow_character install
↓
wow_character_install() runs — iterates _wow_character_owned_entity_types()
↓
for each entity type that exists: installFieldStorageDefinition('character', ...)
↓
(later) wow_achievement install
↓
wow_achievement_progress schema is built
↓
hook_entity_base_field_info fires → wow_character contributes the 'character' base field
↓
column added automatically during schema creation
Path B — owner installed LATER¶
wow_achievement install
↓
wow_achievement_progress schema built, no 'character' column
↓
(later) wow_character install
↓
wow_character_install() iterates owned types, finds wow_achievement_progress
↓
installFieldStorageDefinition('character', 'wow_achievement_progress', ...)
↓
column added to the existing schema
Both paths are tested in CharacterFieldInjectionOrderingTest, UserFieldInjectionOrderingTest, and GuildFieldInjectionOrderingTest.
Uninstall¶
Uninstalling an owner module drops its projection cleanly:
- Schema columns are removed from every target entity type.
- Rows with the projected owner populated are purged first (so the projection column has nothing to migrate through Drupal's field-storage delete hooks).
- Orphan rows (projection column NULL) survive.
- Catalog entities owned by other modules are untouched.
Uninstalling a catalog module goes further: its own base tables, state keys, and user.data namespace (where applicable) are also removed.
The 30-day TTL¶
Blizzard's Terms of Use require API-derived data to be refreshed or deleted within 30 days. The wow.ttl_sweeper service enforces this:
- Runs daily under
hook_cron, throttled by thewow.ttl_sweep_laststate key. - On demand:
drush wow:sweep-ttl. - Sweeps every content entity type that declares a
last_fetchedbase field — catalogs AND junction entities. - Taxonomy reference data (which uses a
wow_last_fetchedconfigurable field becausetaxonomy_termcan't host arbitrary base fields) is NOT swept; operators keep it fresh via the scheduled sync commands.
Junction entities have their own last_fetched independent of their owning character/guild. Two scenarios make this matter:
- Blizzard's character profile API can return a partial collection on big accounts — untouched rows go stale while the character itself is still fresh.
- When a player un-collects a mount/pet/title, Blizzard stops returning that row; the local junction row sits frozen until swept.
A regression test (TtlSweepCoverageTest) scans every entity class for last_fetched and asserts each is registered in TtlSweeper::SWEEP_ENTITY_TYPES. Adding a new catalog entity without wiring the sweep fails CI.
Delete cascades¶
Three cascades, all using hook_entity_predelete:
Character delete → CharacterDataProvider purges¶
Character::delete()
↓
wow_character_entity_predelete()
↓
for each CharacterDataProvider:
try { deleteCharacterData($character); } catch log & continue
Every registered provider purges its per-character rows. One provider throwing doesn't block the rest; failures are logged with @id, @cid, @name, @msg context.
Guild delete → GuildDataProvider purges¶
Same shape, via wow_guild_entity_predelete() and GuildDataProvider plugins.
User delete / de-link → character purge → downstream cascade¶
Two entry points that converge on wow_user_purge_account_data($uid):
- Account cancellation —
wow_user_entity_predelete()on the user entity. - Battle.net de-link — form_alter + submit handler on
openid_connect_accounts_form, scoped to thebattlenetclient only.
Both call the same purge function which:
- deletes every wow_character row owned by the user (which in turn cascades to all CharacterDataProvider plugins),
- clears user.data under wow_user and wow_battlenet_login.
Rate limiting¶
Two windows, both in Drupal State so all PHP processes share one counter:
wow.api_rate_limit— hourly (36,000 requests)wow.api_rate_limit_second— per-second (100 requests)
Check + increment is serialized under the Drupal lock service (wow:api_rate_limit). If the lock can't be acquired after a few retries, the request is throttled — the client refuses to mutate counters with no guard held.
This closes the last concurrent-process race. See BattleNetClient::reserveRateSlot() and RateLimitIntegrationTest / BattleNetClientRateLimitTest.