Why you must never rename a content class

The short version

A content class name is not an internal implementation detail you are free to rename. It is a public contract that thousands of saved buttons — across every customer site that uses that button type — depend on by name. Rename the class without a migration and every one of those buttons quietly stops rendering. There is no error, no crash, no log entry the customer will notice. The button simply disappears from their app.

This article explains why that happens, so that when you are tempted to clean up a name like local_app_login_button, you understand exactly what breaks and what a safe alternative looks like.


What a content class is

Each content class in mam-main/includes/content-classes/ represents one type of button a customer can place in their app: a Login button, a Map button, a Web URL button, a Phone Call button, and so on. There are roughly two dozen of them, and they are the building blocks customers assemble screens from in the no-code builder.

Every class does two things at file-include time:

  1. It registers itself in the global $local_app_content[] array.
  2. It defines a class implementing the duck-typed content-class interface (app_settings(), get_content_type_form(), get_data_for_app(), and so on).

The registration for the Login button looks like this:

$local_app_content['Login'] = array(
    'content_type' => 'Login Button',          // user-visible label
    'class'        => 'local_app_login_button', // the PHP class name
    'vc_type'      => 'login_button',
);

Three things are bundled together here, and the distinction between them is the whole point of this article:

  • 'Login' — the registry key. This is the value a saved button stores as its type.
  • 'class' — the PHP class name the registry key resolves to.
  • 'content_type' — the human-readable label shown in the admin UI.

How a saved button finds its class

When a customer adds a button in the builder, MAM does not store the class name or the rendering logic with the button. It stores a small, flat record. In the button table, that record carries fields like button_id, name, icon_url, source, and — critically — type, which holds the registry key ('Login').

The serialized button arrays that the suite passes around (resolved through the mam_app_settings_get_buttons filter and persisted as the site’s saved layout) carry the same type value. The button is, in effect, a sticky note that says “I am a button of type Login.” It does not know what a local_app_login_button is. It does not need to — until render time.

At render time, MAM performs a two-step lookup. Given a saved button, it reads the button’s type, looks that key up in the live $local_app_content registry, and pulls the class name back out:

$type_info     = $local_app_content[ $item['type'] ] ?? null;  // 'Login' -> registration array
$content_class = $type_info['class'];                          // 'local_app_login_button'

if ( ! class_exists( $content_class ) ) {
    continue;   // <-- the button is silently skipped
}

$this->content_class = new $content_class();

That continue on a failed class_exists() is the heart of the problem. The class name is a string that gets instantiated dynamically with new $content_class(). If the string no longer names a real class, PHP cannot throw — the code guards itself by skipping the button entirely. The same pattern shows up wherever a button is turned into a form or into app data: the content type is resolved through the registry, and a content type that does not resolve is dropped.

So the dependency chain is:

saved button (type = 'Login')
   -> $local_app_content['Login']['class']
      -> 'local_app_login_button'
         -> new local_app_login_button()

Break any link in that chain and the button stops rendering. The two links you can break by “renaming” are the registry key and the class name.


Why a rename orphans buttons

Imagine you rename local_app_login_button to something tidier — say MAM_Login_Button_Content — and update the registration’s 'class' value to match. Locally, everything looks fine: new buttons you create resolve through the registry to the new class and render correctly.

But every button that customers saved before your rename still carries type = 'Login'. That key still resolves through the registry to whatever you put in 'class'. So renaming the class alone is actually survivable as long as the registry key stays the same and the registration points at the new class name. The class name is reachable only through the registry; saved buttons never name it directly.

The genuine, unrecoverable break is renaming the registry key — the 'Login' string. Saved buttons store that key literally. Rename it to 'LoginButton' and:

  • $local_app_content[ $item['type'] ] returns null for every existing button (their type is still the old 'Login').
  • The render loop hits continue and skips them.
  • The buttons vanish from the app with no error.

This is why the content-classes README states the rule in terms of class names: historically the class name and the registry key moved together, and treating the whole registration identity as frozen is the only safe mental model. The conservative rule — never rename a content class — protects you from getting the subtle distinction wrong under pressure. If you rename the class, the file, and the registry key together in one sweep (a very natural refactor), you will break every saved button, because the registry key is the one piece customers’ data depends on.

And because the data lives in client databases — one row per saved button, per role, per site, across the entire customer base — there is no central place to fix it. The orphaning is distributed across every install.


Why nothing tells you it broke

The silence is what makes this dangerous. Consider what does not happen:

  • No PHP error. class_exists() guards the instantiation, and missing registry keys resolve to null and continue. The render loop is built to be tolerant of unknown button types so that a single bad button can’t take down a whole app.
  • No build failure. The class compiles fine; it is simply never reached for the orphaned buttons.
  • No test signal, unless a test happens to assert on a previously saved button payload rather than a freshly created one.

The first person to notice is usually the customer, weeks later, asking why the login button disappeared from their app. By then the rename is buried in release history.

This is the same family of risk as the suite’s other frozen public contracts — option keys, mobile JSON contract keys — where a value is stored in client databases or consumed by deployed apps and therefore cannot change without a coordinated migration.


The safe alternatives

You are not stuck with an ugly name forever. You have two real options.

1. Leave the registration identity alone and refactor around it. You can clean up almost everything inside a content class — method bodies, helpers, even split logic into traits or collaborators — without touching the registry key or the class name customers reach. Internal cleanups are free. The frozen surface is small: the registry key and the class name it resolves to.

2. Rename with a migration. If you genuinely must change the registry key or class name, ship a migration that rewrites every saved button’s type from the old value to the new one, across all roles and locations, before (or atomically with) the code that expects the new value. You may also keep a backward-compatibility alias in the registry — register both the old key and the new key pointing at the same class — so that un-migrated buttons keep resolving while the migration rolls out. The README’s guidance is explicit: rename internals only when there’s a migration to rewrite serialized arrays.

The bar for option 2 is high precisely because the migration has to reach every customer database, and a partial migration leaves a long tail of orphaned buttons that nobody will notice until a customer complains.


How this connects to the rest of the suite

The content-class registry key is one instance of a recurring pattern in MAM Suite: stored data references behavior by a stable name that ends up in a customer’s database or a deployed app. App-setting option keys and the mobile JSON contract are two more instances of the same shape. The single rule that governs all of them — a name in the field is a contract, not a label; rename it only behind a migration — is documented in full in Concept: Frozen public contracts and the wrap-don’t-rename discipline. This article is the content-class case of that rule.


  • Concept: Frozen public contracts and the wrap-don’t-rename discipline (concepts-frozen-public-contracts-wrap-dont-rename-discipline) — the general rule this article is one case of
  • How-to: Build your first custom content class (extending-mam-suite-build-first-custom-content-class) — the registry key, class, and vc_type registration in practice
  • Reference: The content-class interface’s six methods (extending-mam-suite-content-class-interface-seven-methods)
  • content-classes README — the registry and the full list of content types (mam-main/includes/content-classes/README.md)
Was this article helpful?
Contents

    Need Support?

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