The phone-data pipeline phases: auth, settings, content, finalize

The concept, and why it matters

Every time a MAM app launches, it asks the server one question: what should I look like right now? The answer is a single large JSON payload — theme, buttons, tabs, menus, forms, notifications, the home-screen layout, all of it — and assembling that answer is the most important thing mam-main does. It is THE primary mobile API.

That payload is built by the phone-data pipeline (MAM_Phone_Data_Pipeline, in mam-main/includes/phone-data/). The pipeline replaced a ~1900-line god function (mam_get_phone_data() / the old mam_build_phone_data_array()) and split the build into ordered phases. Conceptually there are four:

phase_auth  →  phase_settings  →  phase_content  →  phase_finalize

This matters to you as a developer for one practical reason: almost everything a sibling plugin contributes to the app is injected during one of these phases, and the order is not arbitrary. It is driven by data dependency. Auth runs first because everything downstream needs to know who the user is and what role they have. Settings runs next because it reads role-aware options and makes the cache decision that can cut the whole expensive build short. Content does the heavy lifting and is where your code almost always runs. Finalize shapes and seals the output and must run last.

If you understand the phase order, you understand why your hook fires when it does, what is already on the response by the time you see it, and what you are allowed to assume.


The state carrier: MAM_Phone_Data_Context

A phase is not a function that takes an array and returns one. Each phase is a static method on MAM_Phone_Data_Pipeline that mutates a shared context object, MAM_Phone_Data_Context. The runner creates one context, threads it through every phase, and exports it at the end.

The context is a value object wrapping an associative array (the response under construction) plus a few build-control flags. The accessors you will care about:

Accessor Carries
get($key) / set($key, $value) / has($key) Read or write one response key
merge($overlay) Overlay an array of keys (overlay wins on collision)
replace($data) Wholesale-replace the whole response (used after a filter pass reshapes it)
to_array() Export the current response as a plain array
short_circuit($payload) Replace the response and stop the pipeline early
is_short_circuited() Whether a phase asked to stop
is_return_data() Whether the caller wants the array back (direct PHP) vs. a JSON response (AJAX)
get_single_button_id() A partial-payload mode: build just one button
set_time_start() / set_actual_link() / set_allow_caching() Per-request build bookkeeping read back in finalize

Two things about the context are load-bearing.

First, it is passed by handle. Because PHP passes objects by reference, a phase (or a subscriber hooked to a phase action) mutates the same context every later phase sees. You do not return anything; you write to the context.

Second, it converts to and from a plain array at filter boundaries. The legacy extension filter — mam_get_phone_data_before_send — was written against an array, and roughly seventy subscribers across mam-suite, mam-suite-hold, and mam-use-case still expect an array. So at that boundary the pipeline calls to_array(), runs the filter, and replace()s the context with the filtered result. Your subscriber sees exactly the array shape it always saw; the value object is invisible to it.


The four phases, in order

phase_auth — resolve who is asking

Auth resolves the app user’s identity and role and hydrates the per-request context (MAM_Current_Request, read via mam_user_id(), mam_user(), mam_user_role()). It walks the WordPress roles that MAM is configured to manage and sets the matching role on the current request, so that every later phase — and the legacy build — sees the resolved value when it queries the user’s role.

It also fires the early request-lifecycle signals in the same order the legacy function did: a tracer start, mam_start_main_json_loop, mam_update_current_user, mam_initial_phone_data, push-token registration (mam_add_pn_ids()), and request-location normalization.

After this phase you can rely on identity being correct. Nothing here writes content keys to the response; it sets up the world the rest of the pipeline runs in.

phase_settings — load settings and decide on the cache

Settings does two jobs, and either can stop the pipeline.

The cache decision. It reads the role-aware tsl-setting-disable_caching setting via the mam_app_settings_get_setting filter. If caching is disabled for the role, it resets the cursor so the next call gets a fresh build. Then it compares the caller’s cursor against the server’s current cursor. If they match, the app already has up-to-date data, so the phase short-circuits with a small json_status: no_new_data response and the expensive content build never runs. (For background on what the cursor is, see Cursor cache mechanism.)

The short-circuit hook. It fires mam_local_app_data, the contract that lets a subscriber replace the entire response. Per the legacy semantics, this override only takes effect when the caller asked for the data to be returned directly (a PHP call with $return_data = true), not on a normal AJAX request. There are no active subscribers today, but the contract is frozen — a future use-case plugin might need it.

If neither path short-circuits, the pipeline continues.

phase_content — build the response (this is where you run)

Content is the biggest phase and the one almost every sibling plugin cares about. It assembles the body of the response and, near its end, hands that body to the extension filter that the rest of the suite hooks into. In rough order, it:

  1. Sets per-request flagsallow_caching (from mam_main_allow_cache_json_elements), the request link, the build start time — and stows them on the context for finalize to read back.
  2. Writes identity-tied keys — cloning-state affordances when an admin is previewing as another role, and a per-request nonce (mam_nonce) tied to the requester’s pid.
  3. Merges helper blockssync_user_meta (via mam_sync_user_meta_from_request), social data, image data.
  4. Runs the button loop — for each configured home-screen button, it serves a cached copy if caching is on, otherwise builds it fresh with mam_build_nav_item(). The result becomes homePageIcons. (A single_button_id mode can build just one button and short-circuit — a partial-payload path used mainly for testing.)
  5. Builds the left menu and injects the ajax_url the app POSTs back to.
  6. Fires mam_get_phone_data_before_send — the big one. The context is exported to an array and passed to the ~70 subscribers. Critically, the filter fires after the core keys are already merged, so subscribers see the full response under construction — exactly the shape they saw before the build was split into phases.
  7. Runs mam_main_check_initial_form (form-state validation) and injects accumulated form_data.

If you are writing a sibling plugin and you want to add or modify content in the app response, this is your phase, and mam_get_phone_data_before_send at priority 10 is almost certainly your hook. See Hook: mam_get_phone_data_before_send and Priority conventions for phone-data subscribers.

phase_finalize — shape and seal the output

Finalize takes the full merged response and gets it ready to send. It:

  1. Normalizes the outputmam_replace_null_with_empty_string() (older mobile parsers reject null in string fields), mam_clean_subcategories(), clean_notifications().
  2. Adds the final blockdata: 'update', the performance tracer map, ajax_start, the computed exec_time, and finally json_status: 'new_data' plus the current cursor.
  3. Writes the end-of-request debug logs so the timing window matches the legacy code.

In the conceptual model, finalize is also where home_cats (the home-screen category structure) is injected and where total-override subscribers run. Those subscribers register on the legacy filter at high priority — home_cats at priority 1000, use-case overrides above it — which is why priority 1000 has a hard contract: it must run after every priority-<1000 subscriber. Don’t register something at priority 999 that depends on home_cats existing.

After finalize, the runner exports the context with to_array() and the response is sent (or returned).


Where your subscriber runs in the sequence

You have two ways to participate, and which one you choose decides your place in the order.

The legacy filter (the common path). Register on mam_get_phone_data_before_send. It fires once per build, at the end of the content phase, with the full array. Your priority decides your slot relative to everyone else:

Priority Slot
1 Base settings
10 Default cohort — most sibling plugins
99–100 Late enrichments (run after the default cohort)
199 Form-state validation
1000 home_cats injection — must run after all <1000
1001+ Use-case-specific total overrides

The phase-tagged actions (for new code). The pipeline also fires a per-phase action that hands you the context object directly:

do_action( 'mam_phone_data_phase_auth',     $context );
do_action( 'mam_phone_data_phase_settings', $context );
do_action( 'mam_phone_data_phase_content',  $context );
do_action( 'mam_phone_data_phase_finalize', $context );

These let new plugins opt into a specific phase rather than landing in the undifferentiated priority space of the legacy filter — for example, hooking phase_auth to override identity for the whole build, or phase_finalize to do a last-pass cleanup. New code should prefer these; the legacy filter stays fired at the content boundary indefinitely as a frozen contract.

A quick map of intent to hook:

If you want to… Hook
Override identity for the whole pipeline mam_phone_data_phase_auth
Short-circuit and return a complete response mam_local_app_data
Inject content (the common case) mam_get_phone_data_before_send, priority 10
Run after the default cohort but before home_cats mam_get_phone_data_before_send, priority 99
Validate or clean the final structure mam_get_phone_data_before_send, priority 1001+, or mam_phone_data_phase_finalize

Why short-circuiting changes your assumptions

The single most important consequence of the phase order is that earlier phases can stop later ones from running at all. The settings phase returns early on a cache hit; it can also be short-circuited by mam_local_app_data; the content phase can short-circuit in single-button mode. After each phase the runner checks is_short_circuited() and breaks the loop if set.

So: do not put critical setup in phase_content and assume it always runs. On a cache hit, content never executes — the app gets a no_new_data marker and keeps its cached copy. Anything that must happen on every request belongs in phase_auth, which runs before any short-circuit can fire. Anything that must happen on every full build is safe in content, because a full build by definition got past the settings short-circuit.

A second consequence: the content-phase filter fires once per build with the whole array — not once per button. Per-button work happens inside the button loop through per-button hooks, not through mam_get_phone_data_before_send. A list of fifty items calls the per-button path fifty times, so cache expensive lookups inside those callbacks.


A note on the implementation vs. the model

The conceptual four-phase model — auth, settings, content, finalize — is the one the architecture docs and this article describe, and it is what you should reason with. The class itself defines a few additional phase slots between content and finalize (notifications, forms, custom) that exist as ordered stubs while logic is migrated out of the legacy function PR by PR. They do not yet move work, and they do not change the order you experience as a subscriber: your filter still fires at the content boundary, and home_cats still lands last. Treat the four-phase model as the stable contract; the extra slots are an implementation detail of an in-progress refactor.


How this connects to the rest of the Codex

  • The pipeline is the builder; the cursor is the cache that decides whether the builder runs at all — see Cursor cache mechanism.
  • The shape of the array every phase mutates is documented in Mobile JSON shape — the frozen mobile contract.
  • The extension points named above each have their own reference: Hook: mam_get_phone_data_before_send, Hook: mam_local_app_data, Hook: mam_main_check_initial_form.
  • Slotting your subscriber correctly is the subject of Priority conventions for phone-data subscribers.
  • The AJAX entry point the app actually calls is AJAX action: local_app_get_phone_data.
Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!