The concept: a button type is a class that answers six questions
In MAM Suite, every kind of button a customer admin can drop onto a screen — Login, Map, Web URL, Phone Call, Favorites, Onboarding — is backed by a single PHP class called a content class. There are 21 of these bundled in mam-main/includes/content-classes/, plus more contributed by sibling plugins (mam-chat-manager, mam-geodirectory, mam-special-offers, mam-reviews-manager, and others register their own).
What makes the system coherent is that all of these classes implement the same informal interface. There is no PHP interface declaration enforcing it — it is duck-typed: the builder and the mobile-data pipeline call a fixed set of methods by name, and any class that supplies those methods participates. Introducing a formal interface declaration is a known refactor target, but the contract itself is stable and is the thing you are coding against when you add a new button type.
The reason this matters: the six methods are the seam between MAM’s two worlds. Roughly the first half describe how the button presents and configures itself inside the WordPress admin builder, and the back half describe what the button becomes on the device. Understand that split and the interface stops looking like an arbitrary list and starts looking like a pipeline.
The canonical worked example for registering a class is Recipe: Register a content class; the catalog of all 21 is Content classes overview. This article is the explanation behind both — what each method is for and why the set is shaped the way it is.
The two halves of the contract
| # | Method | Half | Answers the question |
|---|---|---|---|
| 1 | __construct() |
(wiring) | “Do I need to wire any hooks or AJAX endpoints when I load?” |
| 2 | app_settings_categories() |
Admin | “What setting tabs do I add to the per-button UI?” |
| 3 | app_settings() |
Admin | “What setting fields live on those tabs?” |
| 4 | get_blank_content_html() |
Admin | “What does an empty, just-added instance of me look like in the builder?” |
| 5 | get_content_type_form($id, $content_source) |
Admin | “What does the edit form for an existing instance look like?” |
| 6 | get_data_for_app($category) |
Device | “What payload do I hand to the mobile app at request time?” |
Methods 2–5 run in the WordPress admin while an admin is assembling an app. Method 6 runs later, when a device asks the server for its app definition. Method 1 is just construction-time plumbing.
A practical consequence of duck typing: a class only needs the methods it actually uses. The Make Phone Call class (local_app_phone_call) implements get_blank_content_html, get_content_type_form, and get_data_for_app, but not app_settings_categories or app_settings — it has no global settings to expose, so those methods simply don’t exist on it. The Login class (local_app_login_button) implements all of them because it carries a tab full of authentication and theming settings. “The six methods” describes the full contract; any individual class implements the subset its behavior requires.
Method by method
1. __construct()
Usually empty. In the bundled classes it is almost always a no-op:
public function __construct() {
}
Some richer classes use the constructor to wire AJAX endpoints or add_action/add_filter listeners that the button needs at runtime. Treat it as the place for instance-time plumbing, not for returning data — nothing reads a return value from it.
2. app_settings_categories()
Returns an array of category definitions, each a small array with title and slug. These become the tabs in the per-button settings UI. From the Login class:
public function app_settings_categories() {
$cat_array[] = array(
'title' => 'Login Settings',
'slug' => 'login',
);
return $cat_array;
}
The slug is the join key: every field returned by app_settings() names a category that must match one of these slugs. A class with no admin-facing settings omits this method entirely.
3. app_settings()
Returns an array of field definitions — the actual inputs shown under the tabs from app_settings_categories(). Each entry is a schema describing one field:
array(
'category' => 'login', // matches a slug from app_settings_categories()
'title' => __( 'Require Login?', 'mam-suite' ),
'variable' => 'require_login', // option-key suffix the value is stored under
'type' => 'yes-no', // which input widget to render
'id' => 'tsl-require_login', // DOM id
'environment' => 'global', // 'global' = site-wide; 'per-button' = per-instance
)
The fields worth understanding:
typeselects the input widget. Common values seen in the bundled classes includeyes-no,text,textarea,select,color,image, andnumber. The widget-rendering infrastructure lives underapp-settings/.environmentis the important distinction.globalsettings are stored once per site (the Login button’s auth toggles apply app-wide).per-buttonsettings are stored per instance, so two buttons of the same type can differ.variableis the suffix the stored value lives under, andidis the DOM hook. Both feed the settings storage and retrieval path — see Hook: mam_app_settings_get_setting for how a class reads a stored value back at payload time.
These keys end up in customer databases, so the same freezing discipline that applies to class names applies to setting variables: renaming one orphans stored values.
4. get_blank_content_html()
Returns the HTML shown the instant an admin adds a fresh, unconfigured button of this type in the builder. It ranges from a literal placeholder string to a real input. Login returns nothing to fill in:
public function get_blank_content_html() {
return 'None';
}
Make Phone Call returns the input the admin will type a number into:
public function get_blank_content_html() {
return '<input placeholder="xxx-xxx-xxxx" type="text" ... class="local-app-content-source" data-type="phone_call" data-id="BLANK_ID" value="">';
}
Note the class="local-app-content-source" and the data-type / data-id attributes — those are the hooks the builder’s JavaScript reads when it saves the field. The literal BLANK_ID token is replaced with a real id once the instance is saved, which is why the blank and the edit form (method 5) are two different methods.
5. get_content_type_form($id, $content_source)
Returns the edit-form HTML for an already-saved instance. It receives the instance $id and the currently stored $content_source value so it can render the field pre-filled:
public function get_content_type_form( $id, $content_source ) {
return '<input ... class="local-app-content-source" data-type="phone_call" data-id="' . $id . '" value="' . $content_source . '">';
}
Compare with the blank version: the placeholder BLANK_ID becomes the real $id, and value="" becomes value="$content_source". For a button type with nothing to configure, this can be as simple as return 'No settings required'; (Login does exactly that).
A useful mental model: get_blank_content_html() is the “create” form and get_content_type_form() is the “edit” form for the same field. Many simple classes keep them nearly identical apart from the id/value substitution.
6. get_data_for_app($category)
The payload method, and the one that actually reaches the device. When a phone requests its app definition, the data pipeline (MAM_Phone_Data_Pipeline, in its content phase) loops the role’s buttons, resolves each button’s class from the registry, and calls get_data_for_app() on it. The return is merged into the response sent to the app.
The shape of the return is entirely up to the class and is dictated by what the mobile client expects:
- Make Phone Call simply passes through the stored number:
return $source; - Login passes through its category payload:
return $category; - A richer class (a scanner, a list with a detail screen) returns a structured array — a
typetoken plus whatever fields the client needs to render and behave.
This is where the duck-typing cuts both ways. A class that omits get_data_for_app() will appear in the admin builder but render nothing on the device — there is no enforcement to catch it. And the type token a class emits has to be one the mobile client already recognizes; adding a new token server-side without matching client support is a silent no-op. Coordinating that token with the client is a required step, not an optional one.
How the methods connect to the rest of MAM
The interface only makes sense alongside two things outside the class:
The registry. Every content-class file starts by appending itself to the global $local_app_content[] array with three keys — content_type (the admin-facing label), class (the PHP class name), and vc_type (selects which of the app’s fixed, shipped views renders this type’s data — list, offDirectory, calendar, favorites, html, etc.; the clients cannot render a value they don’t ship, so always reuse an existing one). content-class-manager.php is the loader that require_onces each class file, which is what populates that global at include time. The pipeline uses the registry to map a saved button’s metadata back to the class whose get_data_for_app() it should call. Adding a class from a sibling plugin means appending to the same global after mam-main has loaded — see Recipe: Register a content class.
The frozen surfaces. The class identifier and the setting variable keys are written into customer databases (local-app-button-array* and the app-settings options). Once a customer site has saved instances, those names are a frozen public contract — renaming them orphans real stored data and requires a coordinated migration of every site. The interface is stable; the names you choose while implementing it are effectively permanent.
So the six methods are not six unrelated callbacks. They are one object answering, in order, “how do I configure?” and then “what do I become?” — with the registry deciding which object answers and the frozen-contract rules deciding what you’re allowed to call things.
Related
- Recipe: Register a content class — the step-by-step for adding a new button type
- Content classes overview — the catalog of all 21 bundled classes
- Hook: mam_app_settings_get_setting — how a class reads a stored setting value back at payload time
- Hook: mam_get_phone_data_before_send — the device-payload pipeline the sixth method feeds into
- Frozen public contracts reference — why class names and setting keys can’t be renamed casually
Verification
This article was last verified against:
mam-main/includes/content-classes/content-class-manager.php(the include/registry loader)mam-main/includes/content-classes/local-app-login-class.php(full six-method implementer)mam-main/includes/content-classes/local-app-phone-call-content-class.php(settings-free subset)mam-main/includes/content-classes/local-app-onboarding-content-class.phpandpunch-out-content-class.php(method-signature confirmation)mam-main/user_docs/03-content-classes-overview.mdand10c-recipe-register-content-class.md
Re-verify whenever a method is added to or removed from the duck-typed interface, when a formal PHP interface is introduced, when the $local_app_content registry keys change, or when get_data_for_app()‘s position in the phone-data pipeline changes.
