Repository navigation
ServiceProvider interface — opt-in modern plugin contract #1983
Copy link
Copy link
Closed
Labels
enhancementphase:3-pluginsPlugin lifecycle, DI, middleware, migration bridgePlugin lifecycle, DI, middleware, migration bridge
Milestone
Description
Summary
Define a wheels.ServiceProviderInterface with register(container) and boot(app) methods. Plugins that implement this interface get the modern lifecycle; others continue with mixin injection. This is the key 3.x bridge to the 4.0 Service Provider model (#1917).
Proposed Interface
// vendor/wheels/ServiceProviderInterface.cfc
interface {
/**
* Register services, bindings, and config with the DI container.
* Called during plugin load phase.
*/
public void function register(required any container);
/**
* Boot the plugin after all providers have registered.
* Access to routes, middleware, events, and resolved services.
*/
public void function boot(required any app);
}Dual-Mode Support
A plugin can include both a ServiceProvider.cfc implementing this interface AND legacy mixin methods during migration:
plugins/myPlugin/
├── MyPlugin.cfc ← legacy mixin plugin (still works)
├── ServiceProvider.cfc ← new-style provider (also detected)
└── plugin.json ← manifest
- If
ServiceProvider.cfcexists,register()andboot()are called - If legacy mixin methods exist, they are still injected (with deprecation warning per Fixed bug in $findRoute() that causes blow up on unmatched named route #10)
- Plugin authors migrate incrementally: move functionality from mixins to services, then delete legacy CFC
Detection
During $pluginsProcess():
- Check for
ServiceProvider.cfcin plugin directory - If found, instantiate and verify it implements the interface
- Collect all providers, call
register()in load order - After all registered, call
boot()in load order
Files
- New:
vendor/wheels/ServiceProviderInterface.cfc - Modified:
vendor/wheels/Plugins.cfc— detect and invoke service providers
Phase
Phase 3 — Migration Path to Service Providers (3.x → 4.0 bridge)
Related
- Plugin system → Service Provider model (Wheels 4.0) #1917 — Plugin → Service Provider model (4.0 epic)
- Wheels 4.0: Module system and package format #1966 — Module system and package format
- Wheels 4.0: Interface-driven design contracts Issue #1967 #1967 — Interface-driven design contracts
- Depends on: Plugin lifecycle hooks: onPluginLoad, onPluginActivate #1979 (lifecycle hooks), Plugin DI integration — register services via Injector #1980 (DI integration)
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementphase:3-pluginsPlugin lifecycle, DI, middleware, migration bridgePlugin lifecycle, DI, middleware, migration bridge
Activity
bpamiri commentedon Mar 16, 2026
Analysis
Synopsis
Introduce a
ServiceProviderInterfacewithregister(container)andboot(app)methods so plugins can opt into modern DI-backed lifecycle while legacy mixin plugins continue working unchanged. This is the 3.x bridge to the full Service Provider model in 4.0.Merit Assessment
Recommendation: Proceed with modifications
Injector.cfcalready supportsmap().to().asSingleton(),asRequestScoped(), auto-wiring, and is accessible viaapplication.wheelsdiand theservice()/injector()helpers inGlobal.cfc(lines 997-1027). Theregister(container)call can pass this directly.boot(app)parameter is underspecified. The issue says "access to routes, middleware, events, and resolved services" but doesn't define whatappactually is. This needs to be a concrete struct or facade, not just a loosely typedany. Recommendation: define anAppContextstruct or lightweight CFC that wrapsapplication.wheels.mapper, the middleware pipeline, the DI container, and config — this prevents plugins from reaching directly intoapplicationscope.register()in load order, thenboot()in load order" but the current$pluginsProcess()sorts plugins alphabetically (line 148). This is fine for mixin injection but may be wrong for service providers where registration order matters. Consider adding an explicitpriorityororderfield inplugin.jsonto control it.Implementation Plan
Create
vendor/wheels/ServiceProviderInterface.cfc— interface withregister(required any container)andboot(required any app)methods, as specified in the issue.Modify
vendor/wheels/Plugins.cfc:variables.$class.serviceProviders = []in$init()(line 12).$pluginsProcess()(lines 146-178), after instantiating each plugin at line 156, check forServiceProvider.cfcin the plugin folder usingFileExists(local.pluginValue.folderPath & "/ServiceProvider.cfc").variables.$class.serviceProviders.$processServiceProviders()that loops through collected providers and callsregister(application.wheelsdi).$bootServiceProviders(app)that callsboot(app)on each.getServiceProviders().Modify
vendor/wheels/events/onapplicationstart.cfc:$loadPlugins()at line 237, callregister()on all service providers (before mixin injection at line 241).boot()on all service providers — this is where they get access to the mapper, middleware pipeline, and fully initialized DI container.Define the
appcontext passed toboot():{container: application.wheelsdi, mapper: application.$wheels.mapper, config: application.$wheels, environment: application.$wheels.environment}.AppContext.cfcfacade for type safety and documentation.Estimated complexity: Medium. Core detection logic is ~40 lines in Plugins.cfc, bootstrap wiring is ~15 lines in onapplicationstart.cfc, plus the interface CFC.
Dependencies & Risks
"serviceProvider": truemanifest flag to avoid filesystem probing. Consider implementing Add plugin.json manifest support alongside box.json #1976 first or in parallel.register()/boot()pattern supersedes the simpleronPluginLoad()/onPluginActivate()hooks. If both are implemented, the overlap needs clear documentation — service providers should NOT also get lifecycle hook calls to avoid double-initialization.register(container)method IS the DI integration for service providers. If Plugin DI integration — register services via Injector #1980'sservices()method is also implemented, plugins would have three ways to register services (mixin,services(),register()). Recommend Plugin DI integration — register services via Injector #1980 targets only legacy-style plugins, and service providers useregister()exclusively.application.wheelsdiInjector is initialized via bindings CFC before plugins load, so the container will exist. However, plugins callinggetInstance()duringregister()could fail if the dependency hasn't been registered yet — document thatregister()should only callmap()/bind(), notgetInstance().Automated analysis by @claude — review and adjust as needed.
bpamiri commentedon Mar 26, 2026
Implemented:
ServiceProviderInterface.cfcwithregister(container)/boot(app)lifecycle. Wired into both Plugins.cfc and the new PackageLoader. Tested in ServiceProviderInterfaceSpec.cfc.