What it is
MAM_Phone_Data_Pipeline (includes/phone-data/) is the canonical builder of the JSON payload returned to the mobile app. It replaces the legacy 1900-line mam_build_phone_data_array() god function (eliminated in PR #27).
The pipeline runs in 4 phases. State is carried by MAM_Phone_Data_Context — a value object passed between phases. At the legacy filter boundary the context converts to/from an array so existing mam_get_phone_data_before_send subscribers (~70 of them) keep working untouched.
Entry points
| Entry | Use |
|---|---|
AJAX action local_app_get_phone_data |
Primary — what the mobile app calls |
AJAX action mam_get_phone_data |
Modern alias — same handler |
Function mam_get_phone_data($return_data, $single_button_id) |
Direct PHP call (used by some sibling plugins) |
Function local_app_get_phone_data($return_data, $single_button_id) |
Legacy alias — same handler |
When called via AJAX: send a JSON response and exit. When called as PHP with $return_data = true: return the assembled array.
⚠️ mam_get_phone_data() is the most-frozen function in the codebase. Don’t rename it, don’t change its signature, don’t change its return shape. Frozen contract — do not rename.
The four phases
phase_auth
Resolves identity, hydrates MAM_Current_Request. After this phase the pipeline knows who the user is, what role they have, whether they’re cloning, and whether the request is an authenticated app request.
Default subscriber: mam_user_roles_manager::manage_phone_data (priority 10).
phase_settings
Loads app settings (theme, layout, button arrays). Evaluates the cache-bypass decision (cloning admins and users with disable_caching get a fresh build). Fires the mam_local_app_data short-circuit hook — if any subscriber returns a non-empty array, the pipeline returns it immediately and skips the remaining phases.
Default subscriber: mam_app_settings::manage_phone_data (priority 1).
phase_content
The biggest phase. Builds page/button/section data; fires the legacy mam_get_phone_data_before_send filter so ~70 sibling-plugin subscribers can inject their data; injects form_data, nonce, sync_user_meta. The button-rendering loop runs here, calling each content class’s get_data_for_app().
This is where most sibling plugins do their work.
phase_finalize
home_cats injection (the home-screen category structure), output-shaping cleanup (clean_notifications, mam_replace_null_with_empty_string), and final-block overrides (use-case priority-9999 subscribers run here).
Default subscriber: MAM_Main_Manager::manage_phone_data (priority 1000) injects home_cats. Critical contract: must run after every priority < 1000 subscriber.
State carrier — MAM_Phone_Data_Context
A value object holding:
- The accumulated
$data_array - The current request context (user, role)
- Cache flags (
bypass_caching,cursor_value) - Performance tracers
- Final-block overrides
At the legacy mam_get_phone_data_before_send boundary, the context converts to/from an array. Subscribers see exactly the same $data_array shape they always did.
Frozen public contracts in this pipeline
| Contract | Why |
|---|---|
AJAX local_app_get_phone_data |
Mobile apps call it directly |
AJAX mam_get_phone_data |
Modern alias, also in the field |
Function mam_get_phone_data() |
Direct PHP callers across the suite |
Function local_app_get_phone_data() |
Older mobile clients |
Filter mam_get_phone_data_before_send |
~70 subscribers; the main extension point |
Filter mam_local_app_data |
Short-circuit hook — preserved even with no active subscribers |
Filter mam_main_check_initial_form |
Form-state validation pass |
Cache: cursor mechanism
The phone-data response is large (50–200 KB common). To avoid re-sending unchanged data on every app launch, each section is timestamp-tagged. The mobile app stores a cursor (the last timestamp it has) and sends it on the next request. Server compares against per-section timestamps; sections newer than the cursor are sent in full, older sections are sent as null (mobile keeps its cached copy).
JSON_Cursor_Manager::reset_cursor() invalidates the entire per-user cache when a setting change requires a full rebuild (admin saves a button, image upload, etc.).
See Cursor cache mechanism.
Subscriber priority conventions
For mam_get_phone_data_before_send:
- 1 — base settings
- 10 — default cohort (most sibling plugins)
- 99–100 — late-running enrichments
- 199 — form-state validation
- 999+ — niche overrides
- 1000 —
home_catsinjection (MAM_Main_Manager::manage_phone_data) — must run after all <1000 - 1001+ — use-case-specific total overrides
See Priority conventions for phone-data subscribers.
Verification harness
tests/snapshot/snapshot-phone-data.php (Phase 1.4 tool) captures JSON fixtures for diff-based regression checks. Run on every PR that touches the pipeline; non-zero diff means the contract changed.
Gotchas
- Don’t rename anything frozen. Renaming the AJAX action, the function, or the filter immediately breaks customer apps in the field.
- Cache invalidation is hard. Calling
JSON_Cursor_Manager::reset_cursor()defeats the cache for that user; calling it on everyupdate_optiondefeats it globally. Use sparingly. mam_local_app_datashort-circuit has no active subscribers today, but the contract must remain — a future use-case plugin might need to fully replace the response.- Priority 1000 contract —
home_catsmust run last among standard subscribers. Don’t register at priority 999 something that depends onhome_cats.
Related articles
- Phone data pipeline phases
- Cursor cache mechanism
- Mobile JSON shape
- Priority conventions for phone-data subscribers
- AJAX action: local_app_get_phone_data
- Hook: mam_get_phone_data_before_send
- Hook: mam_local_app_data
- Hook: mam_main_check_initial_form
- Frozen public contracts reference
Metadata
| Field | Value |
|---|---|
| Article type | Plugin Overview |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Plugin Reference |
| Audience | PHP developer |
| Last verified | 2026-05-02 |
