Forms architecture — providers and extensibility

What it does

mam-main treats forms as a pluggable provider model. Multiple form sources can coexist behind one set of mobile-app-facing concepts. The forms-manager subsystem doesn’t care where a form definition came from — it just needs a provider that can answer: what fields does this form have, how do I render it for the mobile app, how do I process a submission against it.

Two providers ship today:

Provider Where it lives Maturity
Gravity Forms mam-gravity-forms-manager (separate plugin) + GF-aware code in mam-main/includes/forms-manager/ Mature; most customer sites use this
Programmatic forms Plugin code that registers forms via filter Lightweight; used for things like the user profile form, geodirectory invitations, listing claim forms

The architecture is extensible to additional providers (e.g., Ninja Forms) by hooking the same extension surface. None of the mobile-app rendering or submission code is GF-specific at the API level — only the implementation layer behind the hooks is.


The two hook namespaces

mam-main has two parallel form-related hook surfaces. This is a historical artifact: the system was originally Gravity-Forms-only, so the first generation of hooks were named mam_gf_* and mam_for_gravity_forms_*. As the abstraction generalized, a mam_form_manager_* namespace was added. Both still fire today.

Modern generic surface — mam_form_manager_* (prefer for new code)

Hook Type Role
mam_form_manager_get_forms_from_plugins Filter Primary registration hook. Sibling plugins return their custom (non-GF) forms here
mam_form_manager_get_from_plugins Filter Per-plugin retrieval helper
mam_form_manager_form_submitted_<form-id> Filter (dynamic) Per-form submission handler
mam_form_manager_send_notifications Action Submission post-process — subscribers fire do_action('mam_notification_send_message', …) here
mam_form_manager_precached_field Filter Per-field pre-cache hook
mam_form_manager_process_field_type_<type> Filter (dynamic) Per-field-type custom processor — sibling plugins implement custom field types here
mam_form_manager_content_class_<class> Filter (dynamic) Per-content-class form integration

Legacy Gravity-Forms-flavored surface — mam_gf_* / mam_for_gravity_forms_* (still load-bearing)

These names predate the provider abstraction. They still fire and many sibling plugins subscribe to them, so they cannot be renamed without a coordinated migration. Use them when you need data the modern generic surface doesn’t yet expose.

Hook Type Role
mam_gf_get_form Filter Single form fetch by id
mam_gf_get_all_forms Filter All known forms
mam_gf_get_form_data Filter Form data block (fields + values)
mam_gf_get_form_settings Filter Form settings block
mam_gf_get_custom_form Filter Single custom-form fetch
mam_gf_get_custom_forms_list Filter All custom forms
mam_gf_get_cpt_for_form_id Filter Resolve associated post type
mam_gf_processing_form Action Fired when a submission begins processing
mam_gf_populate_form_data_with_post_meta Filter Post-meta hydration
mam_gf_settings_tab_tabs_list Filter Inject custom tabs into the GF settings page
mam_gf_settings_tab_content Filter Render content for a custom tab
mam_gf_user_profile_submit_title Filter Submit-button title on profile form
mam_for_gravity_forms_form_submitted_<form-id> Filter (dynamic) Per-form GF submission handler
mam_for_gravity_forms_form_result_form_<form-id> Filter (dynamic) Per-form result builder
mam_for_gravity_forms_form_result / _v2 Filter Generic result builder
mam_populate_gravity_form Filter Initial transformation pass
mam_populate_gravity_form_final Filter Final transformation pass
mam_gravity_forms_after_form_processed_<form-id> Filter (dynamic) Per-form post-process
mam_update_form_before_sending Filter Last-chance pre-send mutation (already generic-named)

Frozen public contracts. The legacy mam_gf_* and mam_for_gravity_forms_* hook names are public API. Customer-site sibling plugins subscribe to them. They cannot be renamed in mam-main without breaking customer installations. Any rename has to come with a deprecation cycle and a sweep of every external repo (mam-suite-hold, mam-use-case, customer-site copies). Tracked as post-launch refactor work.


When writing new code that needs to interact with forms in mam-main:

Goal Use this hook
Register your plugin’s custom (non-GF) forms mam_form_manager_get_forms_from_plugins
Handle a specific form’s submission mam_form_manager_form_submitted_<form-id>
Send notifications after a submission do_action('mam_form_manager_send_notifications', ...) (then dispatcher routes to email / push / SMS)
Process a custom field type at build time mam_form_manager_process_field_type_<type>
Fetch a form definition (id-keyed) mam_gf_get_form (no generic counterpart yet)
Fetch all known forms mam_gf_get_all_forms (no generic counterpart yet)
Mutate a form payload before send mam_update_form_before_sending (already generic)

Where the matrix says “no generic counterpart yet”, the legacy hook is the only option. This is a known gap; closing it is straightforward (a small alias layer in mam-main.php) and tracked as a pre-launch nice-to-have.


Adding a new form provider

The minimal contract: a provider registers its forms via mam_form_manager_get_forms_from_plugins, and provides handlers that respond when one of its forms is fetched, submitted, or processed.

Skeleton

add_filter( 'mam_form_manager_get_forms_from_plugins', function ( array $forms ): array {
    // Append your provider's forms to the registry. Each entry is keyed by
    // form id and carries enough metadata for the mobile-app pipeline to
    // know the form exists.
    foreach ( my_provider_get_forms() as $form ) {
        $forms[ $form['id'] ] = array(
            'id'       => $form['id'],
            'title'    => $form['title'],
            'provider' => 'my-provider',
            'fields'   => my_provider_normalize_fields( $form['fields'] ),
        );
    }
    return $forms;
} );

// Handle this provider's forms when fetched by id.
add_filter( 'mam_gf_get_form', function ( $form, $form_id ) {
    if ( $form ) {
        return $form; // Already resolved by another provider — defer.
    }
    return my_provider_lookup_form( $form_id ); // Returns array or null.
}, 10, 2 );

// Handle a submission against one of this provider's forms.
add_filter( 'mam_form_manager_form_submitted_' . MY_PROVIDER_FORM_ID, function ( $result, $submission ) {
    return my_provider_process_submission( $submission );
}, 10, 2 );

Field types

If the provider introduces custom field types the mobile app should render, register a per-type processor:

add_filter( 'mam_form_manager_process_field_type_my-custom-field', function ( $field, $args ) {
    // Convert the provider's field shape into the mobile-app render shape.
    return my_provider_render_field( $field, $args );
}, 10, 2 );

Notifications

After processing a submission, fire the generic notifications hook so admin overrides can intercept and the configured channels (email, SMS, push) all dispatch consistently:

do_action( 'mam_form_manager_send_notifications', $submission_result, $form_id );

Do not fire mam_notification_send_message directly from form-handling code. Always go through mam_form_manager_send_notifications so the dispatch flow stays uniform across providers.


How discovery resolves across providers

When the phone-data pipeline asks “what forms exist,” the resolution order is:

  1. Gravity Forms native discovery (mam-gravity-forms-manager plugin)
  2. mam_form_manager_get_forms_from_plugins — every registered provider appends its forms
  3. mam_add_prog_form / mam_generated_forms — programmatic forms registered by user-roles, geodirectory, etc.
  4. Caching layer reads from mam-form-cache-* options to skip repeated discovery

A form id collision between providers is undefined behavior — each provider should namespace its form ids (e.g., nf-123, prog-user-profile) to avoid stepping on others.


Why the asymmetry exists

When mam-main shipped, Gravity Forms was the only form integration. Hooks were named mam_gf_* because there was nothing else to be generic about. As Ninja Forms support, programmatic forms, and the broader provider model came in, a parallel mam_form_manager_* namespace was added — but the original mam_gf_* hooks couldn’t be renamed without breaking every sibling plugin that already subscribed to them.

The result: two namespaces that mostly agree on what each hook does, with the legacy one having more coverage and the modern one being the recommended target for new code. Closing the gap is a post-launch cleanup task that requires:

  • Adding mam_form_manager_* aliases for the missing legacy hooks
  • Sweeping ~300 touch points across mam-suite, mam-suite-hold, mam-use-case, and customer-site plugin copies
  • A deprecation cycle (_deprecated_hook() warnings for one release)

Frozen public contracts in this article

Hook Why it cannot be renamed
mam_gf_get_form, mam_gf_get_all_forms, mam_gf_get_form_data, mam_gf_get_form_settings, mam_gf_get_custom_form, mam_gf_get_custom_forms_list, mam_gf_get_cpt_for_form_id Sibling plugins subscribe to fetch form data
mam_gf_processing_form Sibling plugins hook submission start
mam_for_gravity_forms_form_submitted_<id> Per-form submission contract; customer plugins subscribe by form id
mam_gravity_forms_after_form_processed_<id> Per-form post-process contract
mam_populate_gravity_form / mam_populate_gravity_form_final Customer plugins use these to mutate form payloads

When in doubt: wrap, don’t rename.


See also

  • 04a-forms-manager-overview.md — what forms-manager is and how it bridges into the mobile app
  • 04b-form-submission-lifecycle.md — the end-to-end submission flow
  • 04c-custom-field-types.md — registering a custom field-type processor
  • 04d-form-cache-and-invalidation.md — how cached form definitions are kept fresh
  • mam_notification_send_message (notifications hook reference) — what mam_form_manager_send_notifications ultimately dispatches into

Metadata

Field Value
Article type Architecture Reference
Plugin slug mam-main
Applies to plugin version 2.1.11+
Category Plugin Reference
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!