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).
The home_cats injection runs here (priority 1000+, built by home-screen-stack-manager.php off the mam_app_settings_get_home_screen_categories filter). Critical contract: it 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 rebuilding it on every app launch, the server keeps a single global cursor token in the mam_json_cursor option. The mobile app stores the cursor returned in the last response and sends it on the next request. In phase_settings the pipeline compares it: if $_REQUEST['cursor'] equals the current server cursor, it short-circuits the whole response with { json_status: 'no_new_data' } (the app keeps its entire cached payload); otherwise it builds the full payload. The comparison is all-or-nothing — there is no per-section delta.
JSON_Cursor_Manager::update_cursor() / reset_cursor() write a fresh uniqid(), invalidating every app’s cache. The token is auto-bumped on save_post, post/user-meta changes, term edits (via setup_hooks()), and from the app-settings save paths.
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 (phase_finalize, viahome-screen-stack-manager.php) — 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.
JSON_Cursor_Manager::reset_cursor()bumps the single global cursor, so it defeats the cache for every app at once. Calling it on everyupdate_optionkeeps the cache permanently cold. 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 |
