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.phpadd_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.phpadd_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.
Which copy runs
Section titled “Which copy runs”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 beforeplugins_loaded: a newer copy may not have registered yet. Booting oninitis fine. WithWP_DEBUGon, early use triggers a_doing_it_wrongnotice. - 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_DEBUGon, 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.
What’s isolated per prefix
Section titled “What’s isolated per prefix”| Surface | Plugin A | Plugin B |
|---|---|---|
| Admin menu slug | ?page=plugin-a | ?page=plugin-b |
| REST namespace | /plugin-a/v1/… | /plugin-b/v1/… |
| Hooks | plugin-a/settings_saved, plugin-a/action/… | plugin-b/settings_saved, plugin-b/action/… |
| Option key | plugin_a_settings | plugin_b_settings |
| JS global | pluginASettingsData (camelCased from prefix) | pluginBSettingsData |
wireframe-admin class | applied only to plugin A’s screen IDs | applied 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.
What’s shared (intentionally)
Section titled “What’s shared (intentionally)”| Surface | Notes |
|---|---|
| Field type registry | Custom field types registered via wp-wireframe/field_types are visible to every plugin. |
| Package-level filters | wp-wireframe/access/resolve, wp-wireframe/save/payload, etc. apply across all consumers. |
| Compiled JS / CSS bundle | One copy enqueued, from the running (newest) copy. |
The Wireframe\ namespace | Loaded 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.phpEach plugin then just requires the framework’s autoloader from the shared location:
require_once WPMU_PLUGIN_DIR . '/wp-wireframe/vendor/autoload.php';Avoiding option-key collisions
Section titled “Avoiding option-key collisions”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.