Architecture overview: mam-main

Summary

mam-main organizes into 22 subsystems across ~145 PHP files. The shape splits cleanly into three layers:

  1. Cross-cutting infrastructure (Phase 1 of the refactor) — request-context, account-code, migrations, repositories. Load-bearing for every other subsystem.
  2. Pipelines (Phase 2) — phone-data (mobile JSON), notifications-manager/dispatcher (email/SMS/push). Single entry points for the platform’s two highest-traffic operations.
  3. Feature subsystemsapp-settings, forms-manager, content-classes, user-roles, push-notification-manager, setup-wizard, publish-app, setcron-manager, plugin-update-manager, plus the smaller subsystems.

Every sibling plugin extends mam-main exclusively through filters and actions. mam-main never imports sibling-plugin classes.


Cross-cutting abstractions

MAM_Current_Request (includes/request-context/)

Per-request context object. Replaces the legacy $mam_user / $mam_user_id / $mam_user_role / $is_cloning globals. Accessed via mam_current_request(). Initialized once per request near the top of the bootstrap; subsystems read identity state from it instead of mutating shared globals.

$req = mam_current_request();
$user_id    = $req->user_id();
$role       = $req->user_role();
$is_cloning = $req->is_cloning();

MAM_Account_Code_Manager (includes/account-code/)

Centralized owner of the local-app-account_code option (frozen public contract — customer enrollment ID). All reads/writes route through this class. Includes a transient cache and graceful-degradation hooks for enrollment-server unreachability.

Encryption-at-rest is not yet implemented; the option remains plaintext.

MAM_Migration_Tasks (includes/migrations/)

Framework for safe option/table renames. Used at boot time only to keep legacy callers (in unmaintained customer-site plugin copies) reading the old keys via alias filters.

Helper Purpose
register_option_alias($old, $new) pre_option + pre_update_option filter pair so legacy reads continue to work
register_option_renames($pairs) Batch alias registration
rename_option($old, $new) Copy + delete with idempotency guard
rename_table($old, $new) ALTER TABLE RENAME with collision check
rename_postmeta_key($old, $new) Bulk postmeta UPDATE

Repositories (includes/repositories/)

DB abstraction layer. One repository per owned table. All $wpdb calls against owned tables route through repositories.

Repository Owned table
MAM_Mail_Queue_Repository wp_mam_mail_queue
MAM_Mail_Attachments_Repository wp_mam_mail_attachments
MAM_Sms_Queue_Repository wp_mam_sms_queue (unified PN+SMS)
MAM_Notification_History_Repository wp_mam_notification_history
MAM_Phone_Token_Repository wp_mam_phone_tokens
MAM_Debug_Items_Repository wp_mam_debug_items

Pipelines

Phone Data Pipeline (includes/phone-data/)

Entry points:

  • AJAX: local_app_get_phone_data (frozen public contract)
  • Function: mam_get_phone_data() (delegates to MAM_Phone_Data_Pipeline::run())

Phases:

phase_auth     →  phase_settings  →  phase_content  →  phase_finalize
identity         load settings,     fire             home_cats injection,
hydration        cache decision,    mam_get_         output cleanup,
                 short-circuit      phone_data_      apply final-block
                 check              before_send,     overrides
                                    inject form_data,
                                    nonce, etc.

State is carried by MAM_Phone_Data_Context. At the legacy filter boundary it converts to/from an array so existing mam_get_phone_data_before_send subscribers keep working untouched.

See Phone data pipeline overview and Phone data pipeline phases.

Notification Dispatcher (includes/notifications-manager/dispatcher/)

do_action('mam_notification_send_message', $msg)
                │
                ▼
   MAM_Notification_Dispatcher::dispatch()
                │
   ┌────────────┼────────────┐
   ▼            ▼            ▼
Email        SMS          Push
Sender       Sender       Sender
   │            │            │
wp_mail()    Twilio       APNs / FCM
or queue     or queue     via MAM_PushDispatcher

Channel routing is driven by the mam_notification_list filter (68+ subscribers). Each registered type declares which channels it supports plus per-channel templates. Queueing decision: count > 10 OR global cron enabled.

See Notifications overview.


Boot order

MAM_Main_Manager::__construct() calls 15 register_*() methods in a fixed order (mam-main.php:503–517):

  1. register_core — request context, account code, migrations
  2. register_database — repositories, table-existence checks
  3. register_app_users — user-roles bootstrap
  4. register_user_management — login, profile, phone-code
  5. register_forms — Gravity Forms integration
  6. register_tab_bar — tab manager filter wiring
  7. register_app_settings — admin UI
  8. register_push_notifications — APNs/FCM senders, token registration
  9. register_notifications — dispatcher hook + admin UI
  10. register_publishing — app-submit pipeline
  11. register_cron — mail/PN/SMS cron + custom cron registration
  12. register_setup_wizard — onboarding + enrollment
  13. register_admin_ui — top-level menu + dashboard widgets
  14. register_plugin_updates — software entitlements + update checks
  15. register_entitlements — per-plugin licensing

The constructor itself contains zero direct add_action/add_filter calls. The crash-reporter loads earlier still, before plugins_loaded, via a top-level include in mam-main.php so it can capture errors during boot.


Dependency graph

─── Cross-cutting (Phase 1) ─────────────────────────────
request-context     ← used by ~everything
account-code        ← enrollment-aware code
migrations          ← boot-time only
repositories        ← consumed by mail-cron, notifications, push,
                      helper-classes, grid-classes

─── Pipelines (Phase 2) ─────────────────────────────────
phone-data          ← called by app-connect; depends on request-context,
                      app-settings, content-classes, forms-manager, user-roles
notifications-manager → repositories, push-notification-manager,
                        mail-cron-manager

─── Feature subsystems ──────────────────────────────────
app-connect            → phone-data
app-settings           → content-classes, helper-classes
user-roles             → app-connect, app-settings, notifications,
                          forms-manager
push-notification-manager → request-context, MAM_Phone_Token_Repository
forms-manager          → app-settings, user-roles, content-classes
setup-wizard           → app-settings, user-roles, plugin-update-manager,
                          account-code
plugin-update-manager  → setup-wizard, support-manager, account-code
publish-app            → app-settings, helper-classes
grid-classes           → repositories, request-context
helper-classes         → app-settings, user-roles, content-classes
content-classes        → app-settings, app-connect, user-roles

Sibling-plugin extension surface

The primary surfaces a sibling plugin reaches for:

Hook Purpose Typical subscriber count
mam_get_phone_data_before_send Inject content into the mobile JSON payload ~70
mam_notification_send_message Fire a notification (email/SMS/push) ~30 fire sites
mam_notification_list Register a notification type with templates 68+
mam_tab_manager Enrich a button definition ~43
mam_app_settings_get_setting Read a setting (with fallback chain) 100+ call sites
mam_cron_manager Register a custom cron task 25+

Priority conventions for mam_get_phone_data_before_send:

  • 1 — base settings
  • 10 — default cohort (most sibling plugins)
  • 99–100 — late-running enrichments (faceted-search, localize, payment gateways)
  • 199 — form-state validation
  • 999+ — niche overrides (weather, delivery, payment processors)
  • 1000MAM_Main_Manager::manage_phone_data (home_cats injection); must run after all priority-<1000 subscribers
  • 1001+ — use-case-specific total overrides

See Priority conventions for phone-data subscribers.


Where to add a feature

Adding… Goes in…
A new content-type handler (button class) includes/content-classes/ + register in content_class_manager
A new notification type Hook mam_notification_list — templates auto-loaded
A new mobile-app screen element Hook mam_get_phone_data_before_send at appropriate priority
A new admin grid Extend WP_List_Table in includes/grid-classes/
A new DB table mam-main owns Add a repository under includes/repositories/
A new global identity field Add accessor to MAM_Current_Request
A new cross-cutting setting Access via the mam_app_settings_get_setting filter

If your feature crosses subsystem boundaries, the answer is almost always “add a new filter and let subscribers register against it” — that’s how the entire plugin scales.


Verification

This article was last verified against:

  • Plugin: mam-main v2.1.11
  • mam-main/ARCHITECTURE.md
  • mam-main/REFACTOR_PLAN.md
  • mam-main/mam-main.php (boot order)

Re-verify whenever a new subsystem is added, a register_*() method is added or reordered in MAM_Main_Manager::__construct(), or a cross-cutting abstraction’s public interface changes.


  • Plugin: mam-main
  • Phone data pipeline overview
  • Notifications overview
  • Settings cascade overview
  • Frozen public contracts reference
  • Extending mam-main: developer guide

Metadata

Field Value
Article type Plugin Overview
Plugin slug mam-main
Applies to plugin version 2.1.11+
Category Getting Started
Audience PHP developer
Last verified 2026-05-02
Contents

    Need Support?

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