Why read mam-geodirectory as a reference
Most MAM Suite features follow the same shape: the app shows a list of things, the user taps one, and the app shows a detail screen for that one thing — usually with action buttons and one or more forms attached. A directory of businesses, a feed of events, a list of staff to manage: structurally they are the same feature with different data.
mam-geodirectory is the largest, most complete implementation of that shape in the suite. It bridges the third-party GeoDirectory CMS into the MAM mobile pipeline, and in doing so it touches almost every extension point MAM Main exposes — content-type registration, content sections, tab-bar buttons, the phone-data injection hook, Gravity-Forms-backed mobile forms, and push notifications. If you understand how those pieces fit together here, you understand how to build (or extend) any list-with-detail feature in the suite.
This article is a map of the assembly, not a line-by-line reference. Each piece has its own detailed article in the mam-geodirectory plugin docs; the goal here is to show how they connect and why the architecture is shaped this way. The detailed articles are linked at the end of each section.
The reference file is content-classes/local-app-geodirectory-v2-class.php, plus the includes/ classes it delegates to.
The core idea: one content class, many surfaces
The single most important architectural decision is that one content class drives every surface. At the top of local-app-geodirectory-v2-class.php, the plugin registers five entries into MAM Main’s global $local_app_content array:
| Content type slug | vc_type |
What the app renders |
|---|---|---|
GeoDirectory |
list |
Scrolling list of listings |
GeoDirectory_admin_approvals |
list |
The same list, restricted to pending listings, admin-only |
GeoDirectory_offdir |
offDirectory |
Off-directory feed layout |
GeoDirectory_Map |
list_map |
The same listings as map markers |
GeoDirectory_calendar |
calendar |
Events feed |
All five point at the same class value — local_app_geodirectory, which is a class_alias of mam_geodirectory_content. They differ only in the vc_type value, which is a hint the mobile app reads to pick a layout. The backend pipeline is identical; the “list view” and “map view” of a directory are the same query and the same per-listing data, rendered two ways on the device.
This is the pattern worth internalizing: a content type is a thin registration entry that names a class and a layout. The class produces a normalized array of records. The vc_type tells the app how to draw them. When you build a new list feature, you are writing one content class and registering one or more vc_type variants against it.
See Content type: GeoDirectory list and map for the per-surface behavior and the admin category picker.
The list half: from request to a sorted array of records
The list side of the feature is the method get_data_for_app(). Conceptually it does five things in order:
- Resolve what to query. From the admin’s picked category it resolves the GD post type and the detail table, and decides whether this is admin-approval mode (the synthetic category id
999999for an admin user), an events-only query, or a standard listing query. - Resolve where the user is. It reads
lat/lonfrom the request and decides whether geofiltering applies — matching the device location against configured cities. (Geofiltering and admin-approval are mutually exclusive: an admin reviewing the pending queue sees the whole queue, not just nearby rows.) - Fetch and gate the rows. It queries the detail table, then runs
should_include_listing()on each row to drop listings the current user is not allowed to see — non-published listings that aren’t the viewer’s own, category mismatches, or rows vetoed by themam_geodirectory_skip_recordhook. - Finalize each surviving row. This is where a raw database row becomes a mobile record. A chain of
finalize_*helpers runs per listing: base data (renamingpost_id→id,latitude/longitude→lat/lon), content, marker/map info, images, categories, social platforms, claim status, and — forgd_eventposts — event dates and venue links. - Sort and return. Distance-sorted when geofiltering is active and the distance-display setting is on; otherwise alphabetically by title using a natural, case-insensitive sort.
The output is an array of associative arrays, each one a frozen mobile contract. Renaming a key here breaks deployed apps, because the app reads those exact key names. The same record shape is returned whether the app asked for a list, a map, an off-directory feed, or a single detail screen — the detail screen is just one of these records, selected by a single_listing parameter.
Two design notes worth carrying to your own features:
- Empty-string keys are stripped before return (
clean_empty_strings()), so a key the app relies on must carry a non-empty default (often a single space) rather than''. - The record shape is request-dependent. Logging in, gaining manager access, or moving the device location changes which keys and which rows come back on the next fetch. There is no static “schema” — the array is computed fresh per request.
See Mobile listing data shape for the complete field map, and Content type: GeoDirectory list and map for the sort and gating rules.
The detail half: content sections
When the user taps a listing, the app shows a detail screen composed of content sections. A section is a small record — title, content payload, a type discriminator, and a show_title flag — and the detail screen is an ordered list of them.
mam-geodirectory contributes seven section types (address, post content, business hours, social media, events, event details, and a favorite/check-in widget) through the mam_content_section_* filter family owned by MAM Main. The section definitions are declared in the content class’s default_content_sections(); the section payloads are built by mam_gd_content_blocks in includes/mam_gd_content_blocks.php.
The important architectural points:
- Sections are filters, not hard-coded markup. Because each section subscribes to a MAM Main filter, other plugins can add their own sections to the same detail screen — a chat section from
mam-chat-manager, a deals section frommam-special-offers— withoutmam-geodirectoryknowing about them. The detail screen is assembled cooperatively. - Sections are admin-orderable. Admins enable, disable, reorder, and rename sections per content type in the App Settings layout UI. The code only defines the default set and default order; the per-app arrangement is configuration.
- Sections are conditional and role-aware. A section can omit itself:
post_contentis skipped when the content is too short,business_hoursonly appears when hours are set, and the favorite widget is hidden for owners/managers/staff (who get management UI instead). The role resolution comes from the sharedmam_gd_user_rolestrait.
This is the same pattern as content types, one level down: a section is a typed record contributed via a filter; the app renders it by type; the admin arranges the set.
See Listing detail content sections for each section’s payload and the conditions that gate it.
The actions: tab-bar buttons
Above or below the detail content, the listing screen shows action buttons — Call, Website, Map, Share, Favorite for consumers; Edit Listing, Edit Staff, Send Notification, Approve, Claim for owners and admins. mam-geodirectory registers fourteen of these.
The mechanism mirrors content sections. Each button corresponds to a mam_main_add_tab_bar_item_* filter owned by MAM Main; the plugin subscribes and, for the current user/listing pair, decides whether to emit the button. The button set is declared in the content class’s tab_bar_manager_buttons(); the per-button behavior is built by mam_gd_tab_bar_buttons and mam_gd_claim_listing_manager.
The defining characteristic of the button layer is role-based visibility. A single skip filter removes the consumer-style buttons (Call, Website, Favorite, Share, Map, Chat) when the viewer is the listing’s owner, manager, staff, or admin — those users get owner-style buttons instead. On top of that, individual buttons carry their own gates:
- Quota gates — Send Notification disappears once the listing hits its monthly push quota from the pricing package.
- Capacity gates — Edit Staff disappears once the staff count reaches the package cap.
- State machines — the Claim button renders one of three states (Claim Pending, Request to Claim, Claim Listing) depending on the user’s claim status.
- Plugin-presence gates — the Chat button only exists when
mam-chat-manageris active and the listing actually has someone to chat with.
The lesson for your own features: buttons are not a static toolbar. They are a per-request computation over (user role × listing state × installed plugins × package limits). Many of these gates are silent — when a quota or cap is hit, the button simply vanishes — so the UX needs onboarding copy to explain limits the UI won’t.
See Listing tab bar buttons for the full button table and each gate.
The injection point: phone-data hook
The pieces above answer requests for specific screens. But a feature also needs to seed app-wide state at launch — flags, the user’s favorites, default city names, and forms that must appear immediately. mam-geodirectory does this through MAM Main’s mam_get_phone_data_before_send hook, in mam_geodirectory_phone_data.
This is the injection layer: a single hook where a plugin writes top-level keys into the mobile JSON payload that the app receives on boot. The geodirectory plugin uses it to set frozen flags like track_views, report_favorites, and data_has_search_index; to supply the user’s favorite post IDs; to seed the home-screen category scroller; and — notably — to force an initial form on launch when the user has a pending staff invite (has_initial_form_to_display / initial_form_to_display).
Conceptually, mam_get_phone_data_before_send is the difference between “data the app pulls when it navigates somewhere” and “data the app needs to know before the user does anything.” A list-with-detail feature usually needs both.
See Plugin: mam-geodirectory (the “Mobile JSON contract — flags and feeds” section) for the full key list.
The write path: Gravity-Forms-backed forms
Everything so far is read-only — the app displays listings. The write path is forms. mam-geodirectory registers seven mobile forms (Add/Edit Listing, Send Notification, Add Users, Manage Invite, Approve Listing, Claim Listing, Add QR Code). Six are backed by Gravity Forms through mam-gravity-forms-manager and managed in includes/mam_gd_form_manager.php; Claim Listing is the exception — a native claim/purchase flow (a three-state button opening a package screen fed by the Stripe or In-App Purchase rail into a GeoDirectory subscription), not a Gravity Form.
The architectural points that generalize:
- A tab-bar button opens a form. The Edit Listing button’s payload is a reference to a configured Gravity Form, pre-populated with the listing’s current values. The button is the entry; the form is the write surface. This is why the button and form layers are designed to hand off to each other.
- Forms are configured, not coded. An admin maps each Gravity-Forms-backed form to a Gravity Form ID on the plugin’s Settings page. The plugin supplies the field wiring and pre-fill; the form design lives in Gravity Forms. Developers extend field sets via filters (e.g.
mam_geodirectory_add_edit_form_fields) rather than editing form markup. - Submission flows back through MAM Main. Form results return a status/message/type-shaped response the app uses to dismiss-and-refresh, the same contract the contact-form plugin uses. After a successful submission the plugin can run side effects — and a successful Send Notification submission fires a push.
See Form: Add/Edit Listing, Form: Send Notification, and the other
04*form articles for the per-form pre-fill and submission contracts.
The outbound signal: notifications
The final piece is push notifications — the feature’s way of reaching users who aren’t currently looking at the app. mam-geodirectory registers its notification types via the mam_notification_list filter and sends through MAM Main’s mam_notification_send_message contract.
The notifications close the loop on the write path: a listing gets approved, a staff invitation goes out, a claim invite is sent, a venue owner sends an announcement from the Send Notification form. Each is a typed notification whose recipient list can be overridden by hooks (mam_geodirectory_send_notification_users) and whose available types can be extended by hooks (mam_gd_send_notification_types).
For your own features, the takeaway is that notifications are owned by the feature but delivered by MAM Main — you register what events exist and who receives them; the platform handles transport.
See GeoDirectory notification types and Form: Send Notification.
How the pieces connect
Putting it together, a list-with-detail feature in MAM Suite is assembled from six cooperating layers, each plugging into a MAM Main extension point:
- Content types — thin registrations in
$local_app_contentnaming a class and avc_type. One class, many surfaces. - The content class — produces a normalized, frozen array of records (
get_data_for_app()and itsfinalize_*chain). The list, the map, and each detail record are all this same array. - Content sections — typed records contributed via
mam_content_section_*, arranged by the admin, that compose the detail screen cooperatively across plugins. - Tab-bar buttons — per-request action buttons via
mam_main_add_tab_bar_item_*, gated by role, state, quota, and installed plugins. - The phone-data injection hook —
mam_get_phone_data_before_send, where the feature seeds app-wide flags, favorites, and launch forms. - Forms and notifications — the write path (Gravity-Forms-backed forms opened from buttons) and the outbound path (
mam_notification_list/mam_notification_send_message).
The thread running through all six is the same: MAM Main owns the extension points and the mobile contract; the plugin owns the data and the rules. mam-geodirectory is large not because the architecture is complicated, but because it implements every layer fully. A smaller feature — mam-contact-form, for instance — uses the same skeleton with only the form and injection layers fleshed out.
When you read or extend any list-with-detail feature, look for these six layers. They will be there, in some combination, every time.
Where to go next
- To build a feature, start from the layer you need: Content type: GeoDirectory list and map (the list), Listing detail content sections (the detail), Listing tab bar buttons (the actions).
- To understand the frozen record contract: Mobile listing data shape.
- For the write and outbound paths: the
04*form articles and GeoDirectory notification types. - For a minimal version of the same skeleton: Plugin: mam-contact-form.
Verification
This article was last grounded against:
- Plugin:
mam-geodirectoryv2.1.5 - Source:
content-classes/local-app-geodirectory-v2-class.php— content-type registrations,get_data_for_app(),default_content_sections(),tab_bar_manager_buttons(), thefinalize_*chain, and theclass_aliastolocal_app_geodirectory - Source:
includes/mam_gd_content_blocks.php,includes/mam_gd_tab_bar_buttons.php,includes/mam_gd_form_manager.php,includes/mam_geodirectory_phone_data.php - Plugin docs:
user_docs/01,03a–03d, the04*form set, and06-notification-types.md
Re-verify whenever the content-type registrations change, the finalize_* chain or the frozen record keys change, the section/button filter families owned by MAM Main change, or the form/notification registration moves.
