Skip to content

fix(plugin): replace PluginProxy with generated interceptor classes#15

Merged
markshust merged 2 commits intodevelopfrom
feature/plugin-interceptor-proxy-fix
Apr 6, 2026
Merged

fix(plugin): replace PluginProxy with generated interceptor classes#15
markshust merged 2 commits intodevelopfrom
feature/plugin-interceptor-proxy-fix

Conversation

@markshust
Copy link
Copy Markdown
Collaborator

Summary

  • Fixes TypeError when a plugin targets an interface (e.g., HasherInterface) and the proxy is injected into a constructor type-hinting that interface — the core bug reported by a user
  • Fixes silent plugin miss when interfaces are resolved via PreferenceRegistry instead of closure bindings — plugins registered against the interface were never found
  • Replaces PluginProxy (generic __call() wrapper that didn't satisfy PHP's type system) with eval-generated interceptor classes that properly implement target interfaces or extend target concrete classes
  • Adds loud errors for readonly class targets and ambiguous multi-interface plugin registration
  • Removes stale demo/ directory and its references from linter config

Architecture

Dual-strategy interceptor generation (no files on disk):

Scenario Strategy
Plugin targets an interface Generate class that implements the interface, delegates to wrapped instance
Plugin targets non-readonly concrete class Generate class that extends the class, overrides plugged methods
Plugin targets readonly concrete class Throw PluginException::cannotInterceptReadonly() with helpful suggestion
Multiple interfaces with plugins on same class Throw PluginException::ambiguousInterfacePlugins()

New files:

  • PluginInterceptedInterface — marker interface with getPluginTarget() (replaces instanceof PluginProxy checks)
  • PluginInterception trait — core before→target→after chain logic (deliberate exception to "no traits" rule; documented rationale)
  • InterceptorClassGenerator — generates interceptor classes via eval(), cached per target

Key changes:

  • PluginInterceptor::createProxy(originalId, resolvedId, target) — new signature passes both the requested interface and resolved concrete class
  • Container::resolve() — passes $originalId at both createProxy call sites
  • PluginRegistry::getEffectiveTargetClass() — interface-aware plugin lookup
  • Router.php — updated to use instanceof PluginInterceptedInterface

Test plan

  • 5061 tests passing (0 failures), including 50+ new tests for the interceptor system
  • Unit tests for PluginInterception trait (16 tests), InterceptorClassGenerator (20 tests), PluginInterceptor (23 tests), PluginRegistry additions (8 tests), PluginException new methods (6 tests)
  • Feature integration tests covering the full stack end-to-end (12 tests)
  • Meta-test verifying PluginProxy has been fully removed from the codebase
  • Verified in markotalk-local with the exact bug scenario from the report — controller type-hinting HasherInterface with a #[Plugin(target: HasherInterface::class)] before plugin: no TypeError, plugin fires correctly

🤖 Generated with Claude Code

Fixes TypeError when a plugin targets an interface and the proxy is
injected into a constructor that type-hints that interface. Also fixes
plugins being silently skipped when an interface is resolved via
preferences rather than closure bindings.

Key changes:
- Add PluginInterceptedInterface marker interface with getPluginTarget()
- Add PluginInterception trait with full before→target→after chain logic
- Add InterceptorClassGenerator for eval-based class generation (no files
  on disk): generates interface wrappers (implements target interface) or
  concrete subclasses (extends target class, only for non-readonly targets)
- Rewrite PluginInterceptor::createProxy() with dual strategy, now takes
  originalId and resolvedId to find plugins registered against interfaces
  even when resolved via preferences
- Fix Container::resolve() to pass originalId to createProxy at both
  call sites (closure bindings and auto-wired classes)
- Add PluginRegistry::getEffectiveTargetClass() for interface-aware lookup
- Add PluginException::cannotInterceptReadonly() and ambiguousInterfacePlugins()
- Remove PluginProxy — replaced by generated interceptors
- Update Router.php to use instanceof PluginInterceptedInterface
- Remove untracked demo/ directory; remove demo/ paths from php-cs-fixer config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the bug Something isn't working label Apr 6, 2026
Document the recommended approach of targeting interfaces instead of
concrete classes, explain how the interceptor generation works at a
high level, and document the two new error cases (readonly targets,
ambiguous multi-interface plugins).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@markshust markshust merged commit 55d3112 into develop Apr 6, 2026
@markshust markshust deleted the feature/plugin-interceptor-proxy-fix branch April 6, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant