Architecture
What an XPAC plugin is: PHP packages on a shared framework, with blocks compiled under dist/.
An XPAC plugin is an ordinary WordPress plugin — one directory in
wp-content/plugins/, one entry file with a plugin header, activate it and it
runs. What is inside it is more structured than most: the feature code is split
into packages, the packages sit on a shared framework, and the blocks
they register are loaded from compiled metadata under dist/.
Nothing on this page is specific to one plugin. Every XPAC plugin is put together this way, which is why the generated block and hook reference can describe all of them at once.
The four parts
xpac-forms/
├── plugin.php plugin header, then one Bootstrap per package
├── packages/ the PHP packages this plugin ships
├── dist/ compiled JS, CSS and block.json
└── vendor/ Composer autoloader and the XPAC frameworkBelow the plugin header, plugin.php is three statements:
require_once __DIR__ . '/vendor/autoload.php';
\XPACGroup\Plugin\Shared\Admin\MenuRoot::boot();
\XPACGroup\Plugin\Forms\Bootstrap::instance(__FILE__);Load the autoloader, claim the shared admin menu root, then construct each
package's Bootstrap. A plugin that ships three feature packages has three of
those last calls. There is no other entry point — everything else is reached from
a Bootstrap.
What stays the same across plugins
Four conventions hold in every XPAC plugin, and everything else in this section builds on them:
| Convention | Example |
|---|---|
PHP classes live under packages/<PackageName>/ | packages/Forms/core/base/Blocks.php |
The namespace is XPACGroup\Plugin\<PackageName> | XPACGroup\Plugin\Forms\Bootstrap |
Each package has a Bootstrap.php at its root | packages/Forms/Bootstrap.php |
Block metadata is at dist/packages/<pkg>/blocks/<block>/block.json | dist/packages/forms/blocks/form/block.json |
packages/Shared/ is in every plugin. It is the common code that is not part of
any one feature: the admin menu root, the settings UI, and the extensions screen.
The framework underneath
Every Bootstrap extends XPACGroup\PluginFramework\v1_0_12\Plugin, which ships
at vendor/xpac/plugin/Plugin.php. It is a small base class, not an application
container. It gives a package:
- One instance per class. The constructor is
private;Bootstrap::instance(__FILE__)memoises one object per called class (Plugin.php:159), so any component can callBootstrap::instance()later with no argument and get the same object. - Two override points at construction.
init()isabstract protected(Plugin.php:141) and runs immediately;hooks()has a no-op default (Plugin.php:148) and runs straight after. - Lifecycle hooks. The constructor registers
handleActivation,handleDeactivateandhandleUninstall, and addsloadTextDomainonplugins_loaded. - Three managers, resolved through a
__geton the base class (Plugin.php:111):$this->assets(anAssetsManager),$this->settings(SettingsManager::instance()) and$this->pages(PagesManager::instance()). - A shared REST namespace.
getRestNamespace()(Plugin.php:205) returns the constantxpac/v1declared atPlugin.php:19, so every route a package registers through it lands under/wp-json/xpac/v1/.
Plugins extend each other through hooks
Some XPAC plugins add to another one rather than standing alone. They do not call into it. They check that it is present, and then subscribe to the actions and filters it fires — which is the same surface documented in the hook reference, and the same surface your own code would use.
packages/Akismet/Bootstrap.php is the pattern in full:
public function wakeup(): void
{
if (class_exists('\XPACGroup\Plugin\Forms\Bootstrap')) {
$this->registerComponent(Addon::class);
} else {
add_action('admin_notices', [$this, 'renderAdminNotice']);
}
}Forms present, the addon loads. Forms absent, the site gets a notice instead of a fatal.