Skip to content

Plugin system → Service Provider model (Wheels 4.0) #1917

Closed

Description

@bpamiri

Phase 4 — Modernization Target #14 (Wheels 4.0)

Replace the zip-based, mixin-injection plugin system with a Service Provider model.

Current Problems

  • Plugins are zip files extracted at boot
  • Methods injected via StructAppend into variable scopes — collision risk
  • No namespacing — two plugins with same method name silently overwrite
  • No lifecycle hooks for plugins
  • Dead code referencing Railo-era workarounds

Proposed Design

component implements="wheels.ServiceProviderInterface" {
    function register(container) {
        container.map("auth").to("MyAuth.AuthService").asSingleton();
    }
    function boot(app) {
        app.addMiddleware("MyAuth.AuthMiddleware");
        app.modelTrait("authenticatable", "MyAuth.Authenticatable");
    }
}

Migration Path

  1. In 3.x, introduce ServiceProvider interface alongside existing plugin system
  2. Plugins can optionally include ServiceProvider.cfc for new-style registration
  3. In 4.0, deprecate mixin injection; require Service Providers
  4. Ship wheels-legacy-plugin-adapter for running old plugins in 4.0

Dependencies

Benefits from expanded DI container (#1912) and middleware pipeline (#1906).

See design_docs/MODERNIZE-WHEELS-RIM.md §4.10 for full implementation plan.


Wheels 4.0 Sub-Issues

Activity

  1. bpamiri commented on Mar 15, 2026

    @bpamiri
    CollaboratorAuthor

    3.x Incremental Modernization Plan

    Filed 10 issues to build the foundation for Service Providers incrementally in 3.x, organized in three backward-compatible phases:

    Phase 1 — Cleanup & Foundation

    Phase 2 — DI Integration & Lifecycle

    Phase 3 — Migration Path (3.x → 4.0 bridge)

    After Phase 3 ships, this issue (#1917) becomes: "remove mixin injection as default, require Service Providers" for 4.0.

  2. bpamiri commented on Mar 16, 2026

    @bpamiri
    CollaboratorAuthor

    Analysis

    Synopsis

    Replace the zip-based, mixin-injection plugin system with a Service Provider model in Wheels 4.0. Plugins would use register(container) and boot(app) instead of having methods silently injected via StructAppend into component variable scopes.

    Merit Assessment

    Recommendation: Proceed — this is the correct architectural direction, but the 3.x bridging issues must land first.

    • Addresses real pain points: The current system in Plugins.cfc uses StructAppend(variablesScope, mixins, true) (line 313) to inject methods, meaning any two plugins defining the same method name silently overwrite each other. There is zero namespacing, zero collision detection, and zero warning. This is a real source of production bugs.
    • DI container already exists: Injector.cfc (297 lines) provides map().to().asSingleton(), asRequestScoped(), auto-wiring, circular dependency detection, and onDIcomplete() callbacks. The service() and injector() helpers in Global.cfc (lines 997-1027) give controllers/models access. The infrastructure for the Service Provider register(container) call is already in place.
    • Middleware pipeline is ready: Pipeline.cfc implements the handle(request, next) chain pattern. MiddlewareInterface.cfc defines the contract. The boot(app) method can register middleware via app.addMiddleware() without new pipeline work.
    • Migration path is realistic: The proposed 3-step migration (3.x opt-in → 3.x deprecation warnings → 4.0 required) is the industry-standard approach. The sub-issues (Wheels 4.0: Module system and package format #1966, Wheels 4.0: Interface-driven design contracts Issue #1967 #1967, Wheels 4.0: Engine adapter modules (Lucee, Adobe, BoxLang) Issue #1968 #1968, Wheels 4.0: Legacy compatibility adapter and migration path Issue #1969 #1969) cover the right dimensions.

    Implementation Plan (Epic-Level)

    The 3.x bridge work (Phase 3) must land before the 4.0 full transition. Here is the recommended sequencing:

    Already done:

    Phase 1 — Foundation (3.x, do first):

    1. Add plugin.json manifest support alongside box.json #1976 — plugin.json manifest support. Extend $pluginMetaData() (Plugins.cfc lines 184-196) to read plugin.json alongside box.json. This unlocks declarative metadata for all subsequent features.

    Phase 2 — Lifecycle & DI (3.x, after Phase 1):
    2. #1979 — Lifecycle hooks (onPluginLoad, onPluginActivate). Modify $pluginsProcess() (lines 146-178) and onapplicationstart.cfc (lines 235-246).
    3. #1980 — Plugin DI integration via services() method. Bridges legacy plugins to the container without requiring the full ServiceProvider pattern.

    Phase 3 — Service Provider Bridge (3.x, after Phase 2):
    4. #1983 — ServiceProviderInterface with register(container) / boot(app). The key bridge component.
    5. #1984 — Deprecation warnings for mixin-only plugins in dev mode. Only meaningful after #1976 and #1983.

    Phase 4 — Full 4.0 Transition:
    6. #1966 — Module system and package format (replaces zip extraction)
    7. #1967 — Interface-driven design contracts
    8. #1968 — Engine adapter modules
    9. #1969 — Legacy compatibility adapter (wheels-legacy-plugin-adapter)

    Current Codebase State

    Key files and their current responsibilities:

    File Lines Role 4.0 Impact
    vendor/wheels/Plugins.cfc 420 Zip extraction, mixin injection, dependency checking Major rewrite — extraction logic removed, mixin injection deprecated
    vendor/wheels/Injector.cfc 297 DI container (map/bind/resolve, singleton + request scoping) Stable — becomes primary plugin integration point
    vendor/wheels/middleware/Pipeline.cfc 59 Middleware chain execution Stable — plugins register middleware via boot()
    vendor/wheels/middleware/MiddlewareInterface.cfc 19 Middleware contract Stable
    vendor/wheels/events/onapplicationstart.cfc ~270 Bootstrap: loads plugins at lines 235-246, mixins at 241-246 Modified — add service provider register/boot phases
    vendor/wheels/Global.cfc:$loadPlugins() lines 2767-2786 Creates Plugins object, extracts mixins to app scope Modified — add service provider collection

    Mixable targets that currently receive injected methods: application, dispatch, controller, mapper, model, base, sqlserver, mysql, postgresql, h2, test (Plugins.cfc line 15).

    Dependencies & Risks

    • Breaking change in 4.0: Removing mixin injection will break every existing plugin. The wheels-legacy-plugin-adapter (Wheels 4.0: Legacy compatibility adapter and migration path Issue #1969 #1969) is essential — it should shim the old StructAppend behavior for plugins that haven't migrated.
    • boot(app) context definition: The epic references app.addMiddleware() and app.modelTrait() but neither API exists yet. The boot() parameter needs a concrete definition — recommend a lightweight AppContext.cfc that wraps the mapper, middleware pipeline, DI container, and a new modelTrait() registration mechanism.
    • Plugin discovery: The current system discovers plugins via zip files in a plugin directory. The 4.0 module system (Wheels 4.0: Module system and package format #1966) should support Composer/ForgeBox-style package resolution. This is a significant design decision that should be specified early.
    • Testing: The test infrastructure (vendor/wheels/tests/) has no plugin-specific test specs currently. Each phase should include tests — especially for dual-mode (legacy + ServiceProvider) plugins.
    • BoxLang compatibility: Plugins.cfc already has BoxLang-specific code paths (lines 53-85, 101-103, 138-139). Any new plugin loading logic must be tested on both Lucee and BoxLang.

    Automated analysis by @claude — review and adjust as needed.

  3. bpamiri commented on Mar 28, 2026

    @bpamiri
    CollaboratorAuthor

    Resolved by Package System (PR #1995)

    The package system that landed in PR #1995 (merged 2026-03-26) implements the full scope of this epic:

    Proposed Shipped
    ServiceProviderInterface with register()/boot() vendor/wheels/ServiceProviderInterface.cfc
    Manifest format (plugin.json) package.json with provides declarations (mixins, services, middleware)
    Directory-based packages (no more zips) packages/ → vendor/ activation model via copy or symlink
    DI container integration Container passed to register(container)
    Middleware registration provides.middleware in manifest + boot(app)
    Namespace isolation Explicit mixin targets: controller, view, model, global, none
    Deprecation warnings for legacy plugins Legacy mixin-only plugins get warnings

    Three first-party packages shipped as proof: wheels-sentry, wheels-hotwire, wheels-basecoat — all with tests and CI.

    The 3.x bridging sub-issues (#1975–#1984) were also closed as resolved.

    Remaining Wheels 4.0 work (tracked separately)

    These are orthogonal to the plugin→package transition and remain open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions