You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Dev Monitor must be invisible in production. Zero classes loaded, zero hooks registered, zero asset enqueues, zero database reads. The only acceptable cost when {PREFIX}_DEV_MODE is false is a single constant check.
Dev_Loader is the one entry point that bridges plugin boot to the Dev Monitor namespace. The plugin's main Plugin::setup() calls Dev_Loader::maybe_boot(). If the constant is not defined or is false, the call returns immediately. If it is true, the loader registers the panel, the collectors, and the asset enqueues.
This file lands now (Sprint 2) so the collectors that arrive in Sprint 3 have a host to register with.
Expected Outcome
A single boot file plus a unit test verifying the constant gate works in both directions.
namespaceRtCamp\WPToolkit\Dev;
useRtCamp\WPToolkit\Traits\Singleton;
class Dev_Loader {
use Singleton;
/** @var string Constant name to check, e.g. "MYPLUGIN_DEV_MODE". Set via ::set_gate() before maybe_boot(). */privatestring$gate_constant = '';
/** @var array<string, object> Registered collectors keyed by id(). */privatearray$collectors = [];
publicfunctionsetup(): void {}
/** * Tell the loader which constant gates dev mode for the consuming plugin/theme. * Called once at plugin boot, before maybe_boot(). */publicfunctionset_gate( string$constant_name ): void;
/** * Boot the Dev Monitor only if the gate constant is defined and truthy. * Safe to call from production — short-circuits before loading any collector code. */publicfunctionmaybe_boot(): void;
/** * Register a collector. Only callable from inside maybe_boot()'s active path. */publicfunctionregister_collector( object$collector ): void;
/** @return array<string, object> */publicfunctionget_collectors(): array;
}
Boot behaviour:
// In a consuming plugin's Plugin::setup()
Dev_Loader::get_instance()->set_gate( 'MYPLUGIN_DEV_MODE' );
Dev_Loader::get_instance()->maybe_boot();
maybe_boot() does the following, in order, only if the gate is true:
Loads built-in collectors (none yet — added in Sprint 3).
Calls setup() on each registered collector.
Hooks admin_bar_menu to add the panel toggle.
Hooks wp_footer + admin_footer to render the panel HTML.
Hooks wp_enqueue_scripts + admin_enqueue_scripts for panel assets.
If the gate is false, none of those happen. No collectors load, no hooks register.
Self-contained brief. The single most important property here is zero cost when the gate is off.
Step-by-step
Create src/Dev/Dev_Loader.php using the Singleton trait. Empty setup(): void {} body.
Add the set_gate(string $constant_name): void method. Just stores the name in $this->gate_constant. No defined() check here.
Add maybe_boot(): void. First line: if ( '' === $this->gate_constant || ! defined( $this->gate_constant ) || ! constant( $this->gate_constant ) ) { return; }. After that guard, the rest of the method is free to do work.
Add register_collector(Collector_Interface $collector): void. Type-hint the interface. Store keyed by $collector->id() to allow lookup and prevent duplicate registration.
Add get_collectors(): array returning the stored array.
test_maybe_boot_is_noop_when_constant_undefined — set gate_constant to 'NOT_DEFINED_CONST', register a mock collector with a setup spy, call maybe_boot(), assert the spy was never called.
test_maybe_boot_is_noop_when_constant_falsy — define a constant to false, same flow, assert spy not called.
test_maybe_boot_runs_when_constant_truthy — define the constant to true, register mock, call maybe_boot(), assert spy called once.
test_register_collector_rejects_non_interface — pass a \stdClass, assert TypeError (PHP catches this at the type-hint level).
test_get_collectors_returns_registered — register 2 mocks with different ids, assert both returned.
defined() returns true for constants defined to null — the ! constant() check catches that.
A collector registered twice (same id()) silently overrides. Document this as expected behaviour, not a bug.
The test that defines a constant cannot un-define it within the same test process. Use runInSeparateProcess or unique constant names per test (MYPLUGIN_DEV_MODE_TEST_<rand>).
Pre-PR self-check
maybe_boot() returns within the first 3 lines if gate is off (verified by reading the code)
All 5 tests pass
Type-hint enforces Collector_Interface on register_collector
PHPCS, PHPStan level 5, PHPUnit all green
CHANGELOG entry under ## Unreleased
Acceptance Criteria
Dev_Loader uses the Singleton trait
setup() is empty (boot work happens in maybe_boot(), after the gate check)
set_gate() accepts a constant name; storing only, no side effects
maybe_boot() returns immediately if gate constant is undefined or falsy — verified by test (no hooks fire, no collector setup() runs)
maybe_boot() calls every registered collector's setup() when the gate is true — verified by test with a mock collector
register_collector() requires the collector to implement Dev\Interfaces\Collector_Interface — type-hint enforced
PHPCS passes
PHPStan level 5 passes
At least 4 unit tests: gate-off-no-boot, gate-on-boots, register-collector-fails-without-interface, get-collectors-returns-registered
Context
The Dev Monitor must be invisible in production. Zero classes loaded, zero hooks registered, zero asset enqueues, zero database reads. The only acceptable cost when
{PREFIX}_DEV_MODEisfalseis a single constant check.Dev_Loaderis the one entry point that bridges plugin boot to the Dev Monitor namespace. The plugin's mainPlugin::setup()callsDev_Loader::maybe_boot(). If the constant is not defined or isfalse, the call returns immediately. If it istrue, the loader registers the panel, the collectors, and the asset enqueues.This file lands now (Sprint 2) so the collectors that arrive in Sprint 3 have a host to register with.
Expected Outcome
A single boot file plus a unit test verifying the constant gate works in both directions.
Files added:
Class shape:
Boot behaviour:
maybe_boot()does the following, in order, only if the gate is true:setup()on each registered collector.admin_bar_menuto add the panel toggle.wp_footer+admin_footerto render the panel HTML.wp_enqueue_scripts+admin_enqueue_scriptsfor panel assets.If the gate is false, none of those happen. No collectors load, no hooks register.
Verification commands (all must exit 0):
composer install composer phpcs src/Dev/Dev_Loader.php tests/Dev/Dev_LoaderTest.php composer phpstan composer test -- tests/Dev/Dev_LoaderTest.phpImplementation guidance
Step-by-step
src/Dev/Dev_Loader.phpusing the Singleton trait. Emptysetup(): void {}body.set_gate(string $constant_name): voidmethod. Just stores the name in$this->gate_constant. Nodefined()check here.maybe_boot(): void. First line:if ( '' === $this->gate_constant || ! defined( $this->gate_constant ) || ! constant( $this->gate_constant ) ) { return; }. After that guard, the rest of the method is free to do work.register_collector(Collector_Interface $collector): void. Type-hint the interface. Store keyed by$collector->id()to allow lookup and prevent duplicate registration.get_collectors(): arrayreturning the stored array.maybe_boot()after the guard, call$collector->setup()on each registered collector. The actual hook wiring (admin_bar, footer, enqueue) is added in Add Dev Monitor — Workflow Panel, Visual Timeline, Console_Log_Collector #21 — for this PR, just call collector setup.test_maybe_boot_is_noop_when_constant_undefined— setgate_constantto'NOT_DEFINED_CONST', register a mock collector with a setup spy, callmaybe_boot(), assert the spy was never called.test_maybe_boot_is_noop_when_constant_falsy— define a constant tofalse, same flow, assert spy not called.test_maybe_boot_runs_when_constant_truthy— define the constant totrue, register mock, callmaybe_boot(), assert spy called once.test_register_collector_rejects_non_interface— pass a\stdClass, assertTypeError(PHP catches this at the type-hint level).test_get_collectors_returns_registered— register 2 mocks with different ids, assert both returned.Reference patterns
Edge cases
defined()returns true for constants defined tonull— the! constant()check catches that.id()) silently overrides. Document this as expected behaviour, not a bug.runInSeparateProcessor unique constant names per test (MYPLUGIN_DEV_MODE_TEST_<rand>).Pre-PR self-check
maybe_boot()returns within the first 3 lines if gate is off (verified by reading the code)Collector_Interfaceonregister_collector## UnreleasedAcceptance Criteria
Dev_Loaderuses the Singleton traitsetup()is empty (boot work happens inmaybe_boot(), after the gate check)set_gate()accepts a constant name; storing only, no side effectsmaybe_boot()returns immediately if gate constant is undefined or falsy — verified by test (no hooks fire, no collector setup() runs)maybe_boot()calls every registered collector'ssetup()when the gate is true — verified by test with a mock collectorregister_collector()requires the collector to implementDev\Interfaces\Collector_Interface— type-hint enforcedCHANGELOG.mdentry added under## UnreleasedNotes
Collector_Interface)..distignorerule that excludessrc/Dev/from release artifacts is set in the skeleton, not here.release/v1.0.0. Branch:v1.0.0/task/dev-loader. Commit subject:feat(dev): add Dev_Loader (DEV_MODE-gated boot).