Skip to content

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: BattleNetAuth fetches a client-credentials (app-scoped) token and caches it. This is the default used by BattleNetClient::get() when no token is passed.
  • User OAuth: wow_battlenet_login captures a user token during OIDC login and stashes it in user.data. Call sites that need it (currently only AccountProfileApi for /profile/user/wow) pull it from there and pass it explicitly as the $accessToken argument.
  • 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_limit lock, enforced across processes.
  • Retry: only GETs, only on 5xx, capped at 3 attempts with exponential backoff.
  • Conditional: If-Modified-Since handled at the client layer — 304 returns NULL from get().

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 a CharacterPageRenderer that collects CharacterPageSection value objects from CharacterDataProvider plugins via buildPageSections(). Sections are organized by named slots (TITLE_RIBBON, COLLECTIONS, ACHIEVEMENTS, EQUIPMENT, GENERIC).
  • The dashboard at /admin/reports/wow is the only admin-facing UI the suite renders itself.