Skip to content

Member directory and stats

Two smaller Moodle syncs: the member/partner directory (companies in the School, with logos, levels, topics and markets) and the headline statistics shown on marketing pages. Unlike the resource sync, the member sync runs daily on WP-Cron.

The “our members” and partner grids are a sales asset and change as companies join or move between membership levels, so they need to track Moodle without an editor copying logos across. The stats (“X members, Y resources, Z users trained”) are the same data summarised, and are cached hard because they appear on the homepage.

PathWhat it does
wp-content/themes/supplychainschool/inc/routing.php:38Rewrite ^update-members → LMSClient::update_members()
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:1977update_members() — resumes from the last page
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:1989iterate_members($page, $count) — the paging worker
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:1483get_stats() — cached stats fetch
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:2230lms_get_stats()
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:2360lms_refresh_stats_cache() — the nightly refresh
wp-content/themes/supplychainschool/inc/classes/LMSClient.php:2934Cron hook registrations
wp-content/themes/supplychainschool/inc/cpt.php:29The member CPT plus member_level and member_tag taxonomies
wp-content/themes/supplychainschool/inc/components/fc-member-grid.phpThe member grid block
wp-content/themes/supplychainschool/inc/components/fc-partners.phpThe partners block
wp-content/themes/supplychainschool/partials/member-card.phpCard markup

Registered at LMSClient.php:509-514 and hooked at :2934:

HookScheduleCallback
scss_import_membersdailyLMSClient::update_members
scss_process_resource_batchsingle events onlyLMSClient::process_resource_batch
lms_refresh_stats_cachedaily at 03:30 locallms_refresh_stats_cache

GET /update-members triggers the member sync on demand and returns JSON (routing.php:134).

update_members() reads the ACF option members_last_imported and starts from the next page, so a partial sync resumes rather than restarting.

iterate_members() then, per page:

  1. Check the failure backoff transient lms_members_failure_backoff. If set, reschedule for +300s and bail.
  2. GET Moodle action=get_directory&page=N.
  3. On failure, increment lms_members_consecutive_failures; at 3 consecutive failures set a 15-minute backoff. Save the current page as members_last_imported and stop.
  4. On success clear both transients.
  5. Stop after 10 pages in one run ($count > 10, :2033).
  6. Store members_last_imported — 0 when the last page is reached, so the next daily run starts over from page 1.
  7. For each member: find the member post by the api_id meta, update or insert, then write ACF fields api_id, description, member_logo, website — array_filter()ed first, so empty values are never written and a cleared field in Moodle keeps its old WordPress value.
  8. Terms: member_level from $member->level (created if missing), then topics and markets from $member->topic_market, and member_tag, each via insert_or_update_term().

Helpers array_flatten() and process_api_arrays() (:2136, :2149) normalise the nested arrays Moodle returns.

Three layers of caching:

  • get_stats() (:1483) returns the lms_stats transient if present.
  • On a miss it calls Moodle action=get_stats and caches for 24 hours.
  • On failure it sets lms_stats_failure_backoff for 15 minutes and returns the stale transient (or null), so a Moodle outage does not mean a hammering loop or a fatal on the homepage.

lms_refresh_stats_cache() (:2360) is the nightly warm-up at 03:30, and lms_schedule_stats_refresh() (:2495) also schedules a one-off refresh +60s in some paths. Both contain a comment about “registered users which has been moved via array_swap function above” (:2326, :2464), suggesting the stat ordering is massaged for display.

lms_user_info() (:2192) is the helper the front end uses for the logged-in user’s own details.

  • LMS_URL per blog — see [[moodle-sso-and-sessions]].
  • ACF option members_last_imported — the resume cursor.
  • Transients: lms_stats, lms_stats_failure_backoff, lms_members_failure_backoff, lms_members_consecutive_failures.
  • WP-Cron must be running for the daily syncs.
  • iterate_members() stops after 10 pages per run. With a daily schedule, a directory of more than 10 pages takes several days to fully refresh. If members appear stale, count the pages before suspecting a bug.
  • $count is passed as 0 from update_members() and incremented per recursion, but the recursive call happens after term assignment — check the tail of iterate_members() before assuming the paging depth is what you expect.
  • array_filter($fields) drops empty values (:2058), so clearing a description or logo in Moodle does not clear it in WordPress. Fields only ever get overwritten with non-empty values.
  • On request failure the current page is saved as the cursor (:2023), so the next run retries that page rather than skipping it — but it also means a permanently failing page blocks all later pages indefinitely.
  • wp_insert_term() returns an array, not an object; iterate_members() reads $level->term_id from it (:2093), which works for get_term_by() but not for a freshly inserted term. A brand new membership level may not be assigned on the run that creates it.
  • Stats are cached for 24 hours and refreshed nightly, so a stats change in Moodle can take a day to appear. Clearing the lms_stats transient is the fast path.
  • The member CPT is 'public' => false, so members have no front-end URL — they only render inside blocks.
  • Member logos are stored as ACF values from a Moodle URL, not sideloaded into the media library.
  • New member field: add it to the $fields array in iterate_members() (:2051) and to partials/member-card.php. Remember the array_filter() behaviour — if the field must be clearable, write it outside that array.
  • New member taxonomy: register it in inc/cpt.php:29 alongside member_level / member_tag, then assign with insert_or_update_term().
  • Changing the page cap: $count > 10 at :2033. Raise it only if the host will tolerate a longer request; the sync is synchronous within one cron run.
  • Stats: change the cache duration at :1510 and the backoff at :1487 together — a short cache with a long backoff means the homepage shows nothing during an outage.
  • Verify by hand: GET /update-members, then check the member posts and members_last_imported. For stats, delete the lms_stats transient and reload a page that shows them.
  • Deliberately not abstracted: the backoff/consecutive-failure pattern is duplicated between the member sync and get_stats(). They tolerate failure differently — members must not skip a page, stats must not block a page render.

None.

Related: [[lms-resource-sync]], [[moodle-sso-and-sessions]], [[acf-flexible-content-blocks]].