Getting started

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 framework

Below 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:

ConventionExample
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 rootpackages/Forms/Bootstrap.php
Block metadata is at dist/packages/<pkg>/blocks/<block>/block.jsondist/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 call Bootstrap::instance() later with no argument and get the same object.
  • Two override points at construction. init() is abstract 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, handleDeactivate and handleUninstall, and adds loadTextDomain on plugins_loaded.
  • Three managers, resolved through a __get on the base class (Plugin.php:111): $this->assets (an AssetsManager), $this->settings (SettingsManager::instance()) and $this->pages (PagesManager::instance()).
  • A shared REST namespace. getRestNamespace() (Plugin.php:205) returns the constant xpac/v1 declared at Plugin.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.

Next

On this page