Data flow¶
From Blizzard's edge to Drupal storage, and then out to the UI.
1. Authentication¶
Every API call flows through BattleNetClient.
BattleNetAuth BattleNetClient Blizzard
───────────── ─────────────── ────────
getAccessToken() → (cached per region)
│
pcov / lock ← reserveRateSlot()
│
GET /data/... / /profile/...
→
← 200 / 304 / 5xx
(5xx → retry w/ backoff)
│
['data' => ..., 'last_modified' => ...]
- Token acquisition:
BattleNetAuthfetches a client-credentials (app-scoped) token and caches it. This is the default used byBattleNetClient::get()when no token is passed. - User OAuth:
wow_battlenet_logincaptures a user token during OIDC login and stashes it inuser.data. Call sites that need it (currently onlyAccountProfileApifor/profile/user/wow) pull it from there and pass it explicitly as the$accessTokenargument. - Per-character / per-guild Profile calls use the app token today — no per-request user consent required.
- Rate limit: per-hour + per-second counters in Drupal State, serialized under the
wow:api_rate_limitlock, enforced across processes. - Retry: only GETs, only on 5xx, capped at 3 attempts with exponential backoff.
- Conditional:
If-Modified-Sincehandled at the client layer — 304 returnsNULLfromget().
2. Sync orchestration¶
Three sync shapes, all living in src/Sync/:
Catalog sync (taxonomy-backed reference data)¶
TaxonomySyncBase::sync($region)
↓
fetchIndex → state[wow.last_modified.X.region] → If-Modified-Since
↓
for each item: findOrCreate(wow_blizzard_id)
↓
mapFields() → $term->save()
↓
update state[wow.last_modified.X.region]
Subclasses only implement mapFields(). Result: created / updated / skipped counts.
Catalog sync (queue-backed content entities)¶
Used when the entity set is large (~9,000 achievements). Instead of a single long sync:
enqueue(region)
↓
fetchConditional → for each item:
Queue::enqueueJob(new Job('wow_X_sync', [blizzard_id, region]))
↓
(later, out-of-band) queue worker runs syncSingle(region, blizzard_id)
↓
api->get{Type}(id) → syncEntity() → $entity->save()
Queue workers run via drush cron or a dedicated runner.
Profile sync (per-character / per-guild)¶
CharacterSync::lookup(region, realm, name)
↓
/profile/wow/character/{realm}/{name} → Character entity
↓
/profile/wow/character/{realm}/{name}/character-media → avatar + inset
↓
invokeDataPlugins()
├─ AchievementData → /achievements → wow_achievement_progress rows
├─ MountData → /collections/mounts → wow_mount_collection rows
├─ PetData → /collections/pets → wow_collected_pet rows
├─ TitleData → /titles → wow_title_award rows
├─ ToyData → /collections/toys → wow_toy_collection rows
└─ ReputationData → /reputations → wow_reputation_standing rows
Each provider is isolated: one throwing doesn't block the others, and the base character still saves.
Guild sync mirrors this through GuildSync + GuildDataProvider plugins (only an interface ships — no implementations at present, but the extension point is wired and tested).
3. Storage layout¶
Reference data → taxonomy_term (vid = wow_*)
+ wow_blizzard_id, wow_last_fetched
Catalogs → wow_{achievement,mount,pet,...}
+ last_fetched (base field)
Per-owner data → wow_{entity}_collection / _progress / _award / _standing
+ owner columns projected in by owner modules
+ last_fetched (base field)
Characters / Guilds → wow_character, wow_guild
+ uid / is_main (when wow_user is enabled)
4. TTL sweep¶
Daily cron job (wow_cron → wow.ttl_sweeper → sweep()) scans every content entity type with a last_fetched field and deletes rows older than DATA_TTL_DAYS (30). Taxonomy reference data is kept fresh by the scheduled sync commands, not the sweeper. See lifecycle.
5. UI output¶
The suite ships no Views, no field formatters, no custom themes. UI is left to each site:
- Views can query every entity directly.
- The character canonical page (
/wow/character/{region}/{realm}/{name}) ships aCharacterPageRendererthat collectsCharacterPageSectionvalue objects fromCharacterDataProviderplugins viabuildPageSections(). Sections are organized by named slots (TITLE_RIBBON,COLLECTIONS,ACHIEVEMENTS,EQUIPMENT,GENERIC). - The dashboard at
/admin/reports/wowis the only admin-facing UI the suite renders itself.