Metadata
| Field | Value |
|---|---|
| Article type | How-to (Developer) |
| Plugin slug | mam-gravity-forms-manager |
| Applies to plugin version | 2.3+ |
| Category | Extending MAM Suite |
| Audience | PHP developer |
| Estimated time | 30 minutes |
| Depends on | MAM Main, Gravity Forms, MAM Gravity Forms |
| Hooks used | mam_gf_get_form_settings, mam_for_gravity_forms_web_form_result_form_{form_id} |
| Last verified | 2026-06-03 |
Goal
You have a Gravity Form on your WordPress site that visitors submit from the web — not from inside the app. You want each submission to land as a post in a custom post type (CPT), with the submitted values stored as post meta, so that a downstream MAM consumer plugin (or the app itself) can read that content. After this how-to you will have a working “edit on web, read in app” path: a web submission creates or updates a CPT post, saves each field as meta keyed by your own slugs, and fires a per-form hook where you finish the job.
This is the same path mam-special-offers and mam-geodirectory use to back their flows, so once it works your content shape matches what those consumers already expect.
How the bridge works (read this first)
The bridge lives in mam-gravity-forms-manager (includes/submit-web-form.php, in mam_gf_submit_web_form::after_submission()). It listens to Gravity Forms’ gform_after_submission. On every web submission it:
- Gates on a subscriber. It checks
has_filter( 'mam_for_gravity_forms_web_form_result_form_' . $form_id ). If nothing is registered for that form ID, the whole body short-circuits. No subscriber means no post is created — the hook is the trigger, not a post-processing afterthought. - Reads your mapping. It calls
apply_filters( 'mam_gf_get_form_settings', [] )and finds the entry whoseidoption resolves to this submission’s form ID. That entry tells it which CPT to write to, which slugs build the title, which build the content, and the slug-to-GF-field mapping. - Maps entry values out of the GF
$entryarray into a values array keyed by your slugs. - Inserts or updates the post (
wp_insert_post/wp_update_post),post_statuspublishby default,post_authorthe current user. - Saves meta with
update_post_meta( $post_id, $slug, $value )for every mapped slug. - Dispatches
mam_for_gravity_forms_web_form_result_form_{form_id}with$form_idand$post_id.
The pivotal data structure is the mam_gf_get_form_settings entry. Get its shape right and the rest follows.
Prerequisites
- The form is already wired into the app per Recipe: Add a Gravity Form to your app (the app side and the web side share the same
mam_gf_get_form_settingsmapping). - You can write and deploy a small custom plugin or a
functions.phpsnippet — this is a developer task, not an admin one. - A target CPT already registered, or you are comfortable registering one. (The bridge does not register it for you; it only writes to it via
post_type.) - The form’s Form ID (e.g.
42). - A list of the GF field IDs you want to capture, paired with the post-meta slugs you want them stored under.
Steps
1. Register your form mapping
Add a callback to mam_gf_get_form_settings. It returns an array of entries, each describing one form your code knows about. The entry’s data block is what the bridge consumes.
add_filter( 'mam_gf_get_form_settings', 'my_app_register_offers_form' );
function my_app_register_offers_form( $settings ) {
$root = 'my_app_offers_';
$slug = 'offers_form';
$fields = [
[ 'id' => 1, 'slug' => 'offer-title', 'title' => 'Offer Title', 'type' => 'text' ],
[ 'id' => 2, 'slug' => 'description', 'title' => 'Description', 'type' => 'textarea' ],
[ 'id' => 3, 'slug' => 'start-date', 'title' => 'Start Date', 'type' => 'date' ],
[ 'id' => 5, 'slug' => 'post-id', 'title' => 'Offer ID', 'type' => 'hidden' ],
];
$settings[] = [
'category' => 'general',
'title' => __( 'My App: Offers', 'my-app' ),
'variable' => 'none',
'type' => 'gravity_form',
'id' => $root . $slug, // option key holding the GF form ID
'environment' => 'backend',
'data' => [
'cpt' => 'offer', // the CPT the bridge writes to
'post_title' => [ 'offer-title' ], // which slugs build post_title
'post_content' => [ 'description' ], // which slugs build post_content
'fields' => $fields,
],
];
return $settings;
}
Two things that trip people up:
idis an option key, not a form ID. The bridge readsget_option( $entry['id'] )and compares the result to the incoming submission’s form ID. Whoever installs your plugin must set that option to the real GF form ID for this site.- Each field’s GF index is stored in its own option, keyed
{id}_{slug}(e.g.my_app_offers_offers_form_offer-title). The bridge readsget_option( $root . $slug )to find which GF field index a slug maps to on this install. Theidvalue inside thefieldsarray is your own reference number; the live mapping comes from the option.
2. Set the form ID and field indexes for this install
If you ship this as a feature plugin, expose an admin settings page so the site admin can pick the form and map fields. For a single site, set the options directly in an install routine:
update_option( 'my_app_offers_offers_form', 42 ); // GF form ID
update_option( 'my_app_offers_offers_form_offer-title', 1 ); // GF field IDs
update_option( 'my_app_offers_offers_form_description', 2 );
update_option( 'my_app_offers_offers_form_start-date', 3 );
update_option( 'my_app_offers_offers_form_post-id', 5 );
The values on the right are the actual Gravity Forms field IDs in the form on this site. If a slug’s option does not resolve to a field present in the submitted form, the bridge skips that slug.
3. Subscribe to the result hook (this is the trigger)
Add a callback to the dynamic hook mam_for_gravity_forms_web_form_result_form_{form_id}, substituting your real form ID. Registering any callback at any priority is what flips the gate in step 1 and enables the whole post-creation flow.
add_filter( 'mam_for_gravity_forms_web_form_result_form_42', 'my_app_offer_submitted', 10, 2 );
function my_app_offer_submitted( $form_id, $post_id ) {
// By now the post exists and meta is saved by mam-gravity-forms-manager.
if ( ! get_post( $post_id ) ) {
return $form_id;
}
// Read a value back out of the meta the bridge just stored.
$title = get_post_meta( $post_id, 'offer-title', true );
// Do the work that depends on the post existing: set status, geocode,
// send a custom email, mark the user as having submitted, etc.
do_action( 'my_app_offer_submitted', $post_id, $title );
return $form_id;
}
The hook is declared with apply_filters() even though it is used for side effects. The plugin ignores your return value, but you must still return something — by convention return $form_id — so that other subscribers in the chain receive a value rather than null. Treat it as an action you are forced to spell add_filter.
4. (Optional) Let the form update an existing post
If a submission should edit a post that already exists — an admin re-edit flow, for example — give the bridge a post ID. Two ways:
- Add a hidden GF field mapped to the
post-idslug. The bridge reads the post ID out of the mapped value. - Serve the form with
?post_id=123on the URL. The bridge runsabsint()on$_REQUEST['post_id']and uses it.
Note the precedence: when $_REQUEST['post_id'] is present, it wins over the mapped post-id field.
Supplying a post ID only switches the bridge from wp_insert_post() to wp_update_post() when the submitter is authorized to edit that post. The bridge runs an ownership-and-type check (mam_user_can_edit_post_of_type, filterable via mam_gf_web_form_can_update_post) before overwriting an existing post; if the check fails it inserts a new owned post instead of overwriting someone else’s. So an admin re-edit updates in place, but an unauthorized or guest submission with a stray ?post_id= will not clobber the target.
5. Verify
- Submit the GF form on the WordPress site (not in the app).
- Open the CPT list in WP admin and confirm a new post appears, titled from your
post_titleslugs and bodied from yourpost_contentslugs. - Open the post’s Custom Fields panel and confirm each mapped slug is stored as a meta key with the submitted value.
- Confirm your result callback ran — check its side effect, or drop a temporary
mamdebugline. - Re-submit with
?post_id={existing-id}on the URL and confirm the existing post updates instead of a new one being created.
Common gotchas
- No subscriber, no post. The most common surprise: the result hook is the trigger. If posts are not being created, first confirm your
add_filter()formam_for_gravity_forms_web_form_result_form_{form_id}actually runs and uses the correct form ID. idis an option key, not a form ID. This is the single biggest confusion when reading consumer code. Search for theupdate_option( $root . $slug, ... )calls to see how the form ID and field indexes get populated.mam_gf_get_form_settingsis read twice per submission. The bridge applies it once for the field mapping and again for the CPT/title/content composition. Keep your callback idempotent and cheap.update_post_metaruns for every slug, includingpost-id. The post ID gets stored in its own meta too. Harmless, but worth knowing if you grep for orphan meta.- App submissions do not trigger this flow. They go through
mam_form_manager_send_notifications, a different code path. To react to both, subscribe to each. - Default status is
publishand author is the current user. For an unauthenticated submission the author is0. If your CPT should not auto-publish, setpost_statustopendinginside your callback. Decide explicitly what guest submissions should do. - Do not rely on
$entryin the callback. The signature is($form_id, $post_id)— no entry array. Read values back viaget_post_meta(). The raw entry is also in the$mam_form_entryglobal, but leaning on a global is brittle; prefer the meta path.
Related
- Recipe: Route a web submission to a custom post type — the longer reference recipe this how-to condenses.
- Hook:
mam_for_gravity_forms_web_form_result_form_{form_id}— full reference for the result hook, with email/geocode/status examples. - Hook:
mam_gf_get_form_settings— the provider hook the bridge reads to learn how to map an entry to a post. - Recipe: Add a Gravity Form to your app — the app-side counterpart that shares the same mapping.
What’s next
Once submissions land in your CPT, point a MAM consumer at that content: register a content class that reads the CPT and its meta (Build your first custom content class), or inject the meta into the mobile JSON payload so the app can render it. If different forms should share one handler, register the same callback against each form’s hook and branch on $form_id.
Verification
This draft was grounded against:
- Plugin:
mam-gravity-forms-managerv2.3 - Source:
mam-gravity-forms-manager/includes/submit-web-form.php(mam_gf_submit_web_form::after_submission()) - Reference docs:
04-recipe-route-web-submission.md,05-hook-mam-for-gravity-forms-web-form-result-form-id.md - Reference consumers:
mam-special-offers,mam-geodirectory
Re-verify whenever the order of post creation, meta save, and result dispatch in after_submission() changes; the mam_gf_get_form_settings entry shape (id, data.cpt, data.post_title, data.post_content, data.fields) changes; the $_REQUEST['post_id'] precedence over the mapped post-id field changes; or the dispatcher moves from apply_filters() to do_action() (planned, TD-GF-005).
