Skip to content

Multi-plugin setup

The framework is designed for multi-tenant use. Each consuming plugin calls App::boot() with its own prefix; the framework wires up that plugin’s pages, hooks, REST namespace, and option key independently.

Two plugins on the same site:

// Plugin A — plugins/plugin-a/plugin-a.php
add_action('init', function () {
Wireframe\App::boot([
'prefix' => 'plugin-a',
'option_key' => 'plugin_a_settings',
'config' => __DIR__ . '/config/settings.php',
]);
});
// Plugin B — plugins/plugin-b/plugin-b.php
add_action('init', function () {
Wireframe\App::boot([
'prefix' => 'plugin-b',
'option_key' => 'plugin_b_settings',
'config' => __DIR__ . '/config/settings.php',
]);
});

Both plugins bundle their own copy of the framework in vendor/. Every plugin runs on the newest copy, and each boot() call registers its own state.

Each bundled copy registers its version when its autoloader is required. From plugins_loaded on, every plugin uses the classes from the newest copy.

  • Require the autoloader when your plugin file loads, and don’t use Wireframe\ classes before plugins_loaded: a newer copy may not have registered yet. Booting on init is fine. With WP_DEBUG on, early use triggers a _doing_it_wrong notice.
  • Classes resolve through the running copy’s autoload-map.php, so a newer copy can bundle a dependency an older one doesn’t know about.
  • With WP_DEBUG on, copies spanning more than one major version trigger a notice naming each plugin, the version it bundles, and the version running.
  • Wireframe\Versions::registered() lists every copy found with its version and source plugin; Wireframe\Versions::active() is the one in use.
  • A copy prefixed with Strauss gets its own namespace and registry, so it stays isolated.
SurfacePlugin APlugin B
Admin menu slug?page=plugin-a?page=plugin-b
REST namespace/plugin-a/v1/…/plugin-b/v1/…
Hooksplugin-a/settings_saved, plugin-a/action/…plugin-b/settings_saved, plugin-b/action/…
Option keyplugin_a_settingsplugin_b_settings
JS globalpluginASettingsData (camelCased from prefix)pluginBSettingsData
wireframe-admin classapplied only to plugin A’s screen IDsapplied only to plugin B’s screen IDs

Settings::get('plugin_a_settings', …) and Settings::get('plugin_b_settings', …) are entirely independent — no global state to coordinate.

SurfaceNotes
Field type registryCustom field types registered via wp-wireframe/field_types are visible to every plugin.
Package-level filterswp-wireframe/access/resolve, wp-wireframe/save/payload, etc. apply across all consumers.
Compiled JS / CSS bundleOne copy enqueued, from the running (newest) copy.
The Wireframe\ namespaceLoaded once from the newest copy. Singleton classes (App, Plugin) hold per-prefix state internally.

Plugins that want to add a custom field type for their own use can still register globally and just not reference the type from other plugins’ configs.

A pattern for “the framework lives in mu-plugins”

Section titled “A pattern for “the framework lives in mu-plugins””

If you want one canonical copy of the framework instead of N bundled copies, install it as an mu-plugin or shared library:

wp-content/
├── mu-plugins/
│ └── wp-wireframe/ # one shared copy
└── plugins/
├── plugin-a/
│ └── plugin-a.php # just calls App::boot(), no vendor/
└── plugin-b/
└── plugin-b.php

Each plugin then just requires the framework’s autoloader from the shared location:

require_once WPMU_PLUGIN_DIR . '/wp-wireframe/vendor/autoload.php';

The default option_key derives from the prefix ({prefix_snake_cased}_settings), so two plugins with different prefixes never collide. If you set option_key explicitly, make it plugin-specific:

// Good — namespaced
'option_key' => 'plugin_a_settings',
// Bad — generic, will collide with anyone else who picks the same name
'option_key' => 'settings',

For multi-page boots the per-page option key is {boot_option_key}_{page_id} by default. Override per-page if you want a shared row across pages.