How HyperBlocks initializes itself, and how to configure it when vendored inside another plugin or theme.
HyperBlocks self-initializes via HyperBlocks\WordPress\Bootstrap::init(), which carries a first-to-boot guard (the Carbon Fields pattern). The goal is to let multiple copies of HyperBlocks coexist (a theme and a plugin both ship it) while only one actually bootstraps.
Sequence:
- Composer includes
bootstrap.phpviaautoload.files. bootstrap.phpschedulesWordPress\Bootstrap::init()atafter_setup_theme(priority 0).- The first copy to reach
init()claims the namespace-scoped constantHyperBlocks\WordPress\LOADEDand wins; it setsConfigruntime identity and hooks block registration. - Every later copy sees
LOADEDdefined and returns before doing any work.
The guard is built with __NAMESPACE__, so it is prefix-safe: unprefixed copies share HyperBlocks\WordPress\LOADED and elect one winner; a namespace-prefixed copy (Mozart) lives under a different namespace, defines a different constant, and boots independently with real isolation. First-to-boot wins, not newest. Prefix if you need version determinism across divergent copies.
bootstrap.php also triggers HyperFields' bootstrap from the vendored copy when running standalone, so requiring estebanforge/hyperblocks is the only step needed; you do not bootstrap HyperFields separately.
Step 2 above (scheduling init() at after_setup_theme) is reliable across
every WordPress load order. It does not depend on add_action() being
available when bootstrap.php runs. When the Composer autoloader is pulled in
before wp-includes/plugin.php loads (a drop-in such as object-cache.php, a
must-use plugin, or wp-config.php, common in Bedrock), bootstrap.php writes
the after_setup_theme registration straight into $GLOBALS['wp_filter'] in
the preinitialized-hooks raw-array format; WordPress core converts that into a
real WP_Hook when plugin.php loads (WP_Hook::build_preinitialized_hooks,
since WP 4.7, Trac #38929). The scheduler runs before the ABSPATH guard, so
init() is scheduled whether or not ABSPATH is defined yet, and the
hyperblocks_bootstrap_init callback fires at after_setup_theme priority 0.
The editor-asset enqueue also self-heals when Config::$pluginUrl is empty,
because it resolves its URL from the library's own root via
HyperFields\LibraryBootstrap::resolveContentUrl(). So on a web-reachable
copy the inserter works even if init() were somehow delayed.
If a Bedrock project has HyperBlocks in two places at once (a root vendor/
copy pulled transitively, plus a copy bundled inside a plugin under
wp-content/), the root copy wins Composer's autoload.files race,
because wp-config.php requires the root vendor/autoload.php first. Only the
winning bootstrap's code runs, so an updated bootstrap only takes effect when
the winning copy contains it. To make the plugin-bundled (web-reachable) copy
win, remove the root copy with Composer replace:
- Confirm why it is there:
composer why estebanforge/hyperblocks. - Add to the root
composer.json:"replace": { "estebanforge/hyperblocks": "*" }. composer update estebanforge/hyperblocks --lock. Composer removes the directory itself and drops theautoload.filesand classmap entries.
Never rm the root vendor/ copy. Composer's files-loader does a bare
require $file with no file_exists guard, so deleting the file while the
autoload entry survives fatals every request. replace plus update --lock is
the only safe removal.
Calling \HyperBlocks\WordPress\Bootstrap::init() explicitly after your
autoloader (see Manually triggering initialization below) is still supported
as an optional deterministic override. It is idempotent, election-guarded, and
bypasses the auto-bootstrap entirely. It is no longer required for correctness.
Runtime identity lives on HyperBlocks\Config (prefix-safe), not global constants:
Config::VERSION- semantic version (mirrorscomposer.json)Config::$abspath- library root path with trailing slash, set at initConfig::$pluginUrl- public URL with trailing slash, or''when not web-reachableConfig::$pluginFile- absolute path to the bootstrap file
HyperBlocks defines no HYPERBLOCKS_* constants.
The common case: a plugin requires estebanforge/hyperblocks and loads its own Composer autoloader.
wp-content/plugins/my-plugin/
├── my-plugin.php
└── vendor/
└── estebanforge/
├── hyperblocks/
└── hyperfields/
// my-plugin.php
$autoload = plugin_dir_path(__FILE__) . 'vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
// HyperBlocks bootstrap is included automatically via Composer autoload.files.
// No further initialization is required; blocks may be registered from init onwards.
add_action('init', function (): void {
$block = \HyperBlocks\Block\Block::make('My Block')
->setName('my-plugin/my-block')
->addFields([\HyperBlocks\Block\Field::make('text', 'heading', 'Heading')])
->setRenderTemplateFile('blocks/my-block.hb.php');
\HyperBlocks\Registry::getInstance()->registerFluentBlock($block);
});When your plugin defers setup to a bootstrap class, define constants at the top of the main plugin file so URL resolution works after plugins_loaded.
// my-plugin.php
define('MY_PLUGIN_FILE', __FILE__);
define('MY_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));
add_action('plugins_loaded', function (): void {
require_once MY_PLUGIN_DIR . 'vendor/autoload.php';
MyPlugin\Bootstrap::init();
});// src/Bootstrap.php
namespace MyPlugin;
class Bootstrap
{
public static function init(): void
{
add_action('init', [self::class, 'registerBlocks']);
}
public static function registerBlocks(): void
{
\HyperBlocks\Config::registerBlockPath(MY_PLUGIN_DIR . 'blocks');
}
}In setups where the plugins directory is outside the standard wp-content/plugins path, or where plugin directories are symlinks, asset URLs resolve against the web-accessible WordPress content roots (plugins, mu-plugins, content, active theme dirs). Bootstrap::init() always runs regardless of reachability — it does not gate boot on the URL. When a copy sits outside every content root (e.g. a Bedrock root composer vendor outside the document root), Config::$pluginUrl is empty and the editor-asset enqueue bails gracefully: it logs the "not reachable from any web-accessible WordPress content root" notice and returns, so fluent blocks still render on the front end but will not appear in the inserter. The bootstrap scheduler runs before the ABSPATH guard and handles the early window (see Auto-bootstrap (zero-config)), so a root-vendor copy initializes reliably. But on a dual-copy Bedrock site the root copy wins the autoload.files race, so remove it via Composer replace (never rm) if you want the plugin-bundled copy's bootstrap to win. For the inserter to work, load HyperBlocks from a web-reachable copy bundled inside a plugin under wp-content/.
web/app/plugins/my-plugin/ <- WP registration (may be a symlink)
packages/my-plugin/
├── my-plugin.php
└── vendor/estebanforge/hyperblocks/
// my-plugin.php
require_once plugin_dir_path(__FILE__) . 'vendor/autoload.php';
// bootstrap.php is included by autoload.files; nothing else needed.A single-consumer plugin may skip the after_setup_theme scheduling and call init directly, which runs immediately and is still guarded against double-init by LOADED and Config::isInitialized():
add_action('plugins_loaded', static function (): void {
require_once MY_PLUGIN_DIR . 'vendor/autoload.php';
\HyperBlocks\WordPress\Bootstrap::init();
}, 0);In non-WordPress environments (WP-CLI scripts, testing without Brain Monkey) add_action may not exist. Guard the autoloader load:
if (function_exists('add_action')) {
require_once __DIR__ . '/vendor/autoload.php';
}For PHPUnit tests, define HYPERBLOCKS_TESTING_MODE before loading the autoloader to bypass the direct-access guards in source files:
// tests/bootstrap.php
define('ABSPATH', __DIR__ . '/../');
define('HYPERBLOCKS_TESTING_MODE', true);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/mocks/wp-mocks.php';
\HyperBlocks\Config::reset();
\HyperBlocks\Config::init();HyperBlocks' bootstrap.php includes HyperFields' bootstrap.php from the vendored path when it is not already loaded:
vendor/estebanforge/hyperblocks/bootstrap.php
└── requires vendor/estebanforge/hyperfields/bootstrap.php
HyperFields' own first-to-boot LOADED guard prevents double-initialization. If the HyperFields standalone plugin is active alongside a vendored copy, each is prefix-isolated and boots independently.
Requiring only estebanforge/hyperblocks is sufficient; HyperFields is pulled in transitively and bootstrapped automatically.
use HyperBlocks\Config;
// Has this copy been bootstrapped?
if (Config::isInitialized()) {
// Safe to use HyperBlocks classes; Config::$abspath is set.
}
// Library root path and URL (prefix-safe, empty until init runs).
$root = Config::$abspath;
$url = Config::$pluginUrl;