Skip to main content

Troubleshooting

Symptom → cause for the failures the framework produces. Three shapes, and which one you get is deliberate:

  • Exceptions for programming errors that must not be survivable (a missing dependency, an unresolvable service).
  • _doing_it_wrong() notices for developer mistakes WordPress convention says to report rather than throw on. These are only visible when WP_DEBUG is on — if a call "does nothing" in production, re-run it with WP_DEBUG enabled before assuming the framework is silent.
  • Silent no-ops where absence is a legitimate state (a template that isn't there, an unregistered feature flag).

Registration & loading

SymptomCauseFix
RuntimeException: Cannot call get_shared() before load() has been called.get_shared() called on a loader whose load() hasn't run yet — often a service reaching for a shared instance during construction, while the loader is still mid-loop.Retrieve on a hook that fires after the load, not in the constructor.
RuntimeException: Instance "Acme\Foo" is not registered in the container.The class isn't Shareable, or it wasn't in the list this loader loaded, or a later load() on the same loader replaced the container.Implement Shareable; call get_shared() on the loader that actually loaded the class (a module's services live on the module, not on Main); use one complete class list per loader.
ArgumentCountError: Too few arguments to function …::__construct()A loaded class has a required constructor parameter. The loader always calls new $class_name().Make every parameter optional, or construct the object yourself and pass it in.
A class's hooks never fireIt is ConditionallyRegistrable and can_register() returned false — the instance is still constructed, only register_hooks() is skipped.Check the condition (feature flag, is_admin(), WP_CLI).
Hook body runs twiceThe same behavior is registered from two different classes, or one class is loaded by two different loaders. Duplicates within one class list are already de-duplicated.Load the class from exactly one place.
LogicException: Acme\Controller::register_routes() must be overridden. on a REST requestAbstractRESTController::register_routes() was not overridden. The base throws instead of being abstract, so this surfaces when rest_api_init fires, not at class load.Implement register_routes().
Two singletons return the same objectA class and its subclass both resolved through get_instance(); the trait's static::$instance is one storage slot for both.Don't call get_instance() on a subclass. Give each singleton its own use Singleton;, or use Shareable + get_shared(). See upgrading.md.
Error: Access to undeclared static property …::$instanceRunning framework 1.0.0 with a constructor that assigns static::$instance = $this.Upgrade to 1.0.1+, where that is the supported pattern.

Assets, components & templates

SymptomCauseFix
_doing_it_wrong: Asset file "app.js" is missing. The asset will not be registered.No file at <base_dir>/<assets_dir>/app.js. The asset path is relative to the assets dir and carries no extension in the call.Build first; check the AssetLoader constructor's $base_dir / $assets_dir.
_doing_it_wrong: Asset manifest "…" is invalid; the file modification time will be used as the version.app.asset.php exists but doesn't return an array.Regenerate the build. Harmless otherwise — registration continues with a filemtime() version.
_doing_it_wrong: Block manifest file is missing. Blocks will not be registered.register_block_manifest() got a manifest path that doesn't resolve under the base dir.Pass the path relative to the base dir, e.g. build/blocks-manifest.php.
Script registers but never loadsRegistration is not enqueueing.Call wp_enqueue_script() with the handle. ComponentLoader does this for component assets; AssetLoader never does.
Handles collide with another packageHANDLE_PREFIX left at its wp-framework- default.Override the constant in the consumer's AssetLoader subclass.
RuntimeException: Acme\Components requires an AssetLoader: inject one via the constructor or override get_asset_loader().The ComponentLoader subclass was constructed with no asset loader — typical when the framework Loader instantiates it with no arguments.Build one in the subclass constructor, or override get_asset_loader() to resolve a shared instance lazily.
_doing_it_wrong: Component "Foo" could not be resolved.No Foo/Foo.php under any layer of the hierarchy, or the name was rejected: names must match ^[A-Za-z0-9_-]+$ and be ≤128 characters. A slash or .. is refused outright — that check is a security boundary, not a convenience.Fix the path or the name.
A theme override isn't picked upThe render passed allow_override => false, or the override sits in a layer the loader doesn't search (a plugin-owned loader searches child → parent → package; a theme's own loader collapses the redundant layers).Drop the option, or place the file in a searched layer.
Overrides stop resolving after a theme or blog switchThe hierarchy and component metadata are memoised per request, and both depend on the active theme.Call clear_cache() after switch_theme() / switch_to_blog() on a long-lived (Shareable) loader.
render() outputs nothing, no noticeA missing template is a deliberate silent no-op (locate() returns false).Check with locate( $slug, $name ).
Wrong template winsResolution puts the name in the outer loop, so a {slug}-{name}.php in the package beats a generic {slug}.php in the theme — matching core's locate_template() precedence.Override the specific variant, not the generic one.

Utilities

SymptomCauseFix
InvalidArgumentException: Encryptor only supports GCM ciphers …A non-GCM cipher was passed. The stored IV ‖ tag ‖ ciphertext layout is GCM-specific.Use aes-256-gcm (the default) or another -gcm cipher.
RuntimeException: No encryption key provided. …Constructed with an empty key and key() not overridden.Pass a key, or override the protected key() seam.
_doing_it_wrong: OpenSSL extension is not loaded. and encrypt()/decrypt() return falseThe OpenSSL PHP extension is missing on the host.Install/enable it. Encryptor is the only part of the package that needs it.
decrypt() returns falseTampered or truncated ciphertext (GCM authentication failed), non-base64 input, or the wrong key. false is a return value, not an exception — always check it.Verify the key domain; treat a failure as untrusted data.
Same value encrypts to a different blob every timeCorrect: a fresh random IV per call.Never compare ciphertexts for equality; decrypt and compare plaintext.
A feature flag is always offis_enabled() is fail-closed and returns false for an unregistered flag silently — usually a typo, or register() running after the check.Register before checking; use the exact registered slug.
_doing_it_wrong: Feature flag "x" collides with already-registered "y" …Two slugs normalize to the same storage key. The first registration is kept.Rename one.
_doing_it_wrong: Feature flag "x" is not registered; enable() ignored.enable() / disable() called for an unregistered flag.Register it first.
A toggle on the settings page won't changeThe flag is locked by a PHP constant, which always wins over the stored value. The checkbox renders disabled and the stored value is preserved.Remove the constant from wp-config.php. Note 'false' (the string) is treated as false.
SWR still stampedes across workersremember_swr() locking needs a persistent object cache (Redis, Memcached). With WordPress's default request-local cache the lock isn't shared between PHP workers.Deploy a persistent backend; without one the API still works as get-or-set.
A deleted cache key comes back staledelete() removes the primary key only — {key}_stale and {key}_lock survive.flush_group() to invalidate an SWR entry completely.
Nothing appears in the logLogger writes only when logging is enabled, which tracks WP_DEBUG by default.Enable WP_DEBUG, or override the protected is_enabled() seam.
Two modules overwrite each other's transientsUnprefixed set_transient() calls somewhere. Transients exists precisely to namespace them.Construct one Transients per module with its own prefix.
_doing_it_wrong: Timer "x" has already been started / was never started / has already been stoppedTimer misuse. Reads are silent: get() returns null for an unknown label.Share the same Timer instance across scopes (register it Shareable); a new instance has no timers.

Environment

SymptomCauseFix
Class "rtCamp\WPFramework\…" not foundThe consumer's vendor/autoload.php was never required, or the package resolved from a stale vendor/.Require the autoloader in the plugin/theme entry point; composer update rtcamp/wp-framework.
Composer can't find the packageIt is not on public Packagist.Add the VCS repositories entry — see getting-started.md.
A missing abstract method only surfaces at runtimeThe subclass doesn't implement everything the abstract declares.Run PHPStan in the consuming package; it catches contract breaks before a request does.

Still stuck? The classes are small and heavily commented — Loader::load() in inc/Contracts/Traits/Loader.php explains the whole boot in one screen.