Skip to content

ServiceProvider interface — opt-in modern plugin contract #1983

Closed

Description

@bpamiri

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

Detection

During $pluginsProcess():

  1. Check for ServiceProvider.cfc in plugin directory
  2. If found, instantiate and verify it implements the interface
  3. Collect all providers, call register() in load order
  4. 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

Activity

  1. bpamiri commented on Mar 16, 2026

    @bpamiri
    CollaboratorAuthor

    Analysis

    Synopsis

    Introduce a ServiceProviderInterface with register(container) and boot(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

    • Strong design: The dual-mode approach (ServiceProvider.cfc alongside legacy MyPlugin.cfc) is the right incremental strategy — it lets plugin authors migrate at their own pace without a flag day.
    • DI container is ready: Injector.cfc already supports map().to().asSingleton(), asRequestScoped(), auto-wiring, and is accessible via application.wheelsdi and the service() / injector() helpers in Global.cfc (lines 997-1027). The register(container) call can pass this directly.
    • Boot context needs definition: The boot(app) parameter is underspecified. The issue says "access to routes, middleware, events, and resolved services" but doesn't define what app actually is. This needs to be a concrete struct or facade, not just a loosely typed any. Recommendation: define an AppContext struct or lightweight CFC that wraps application.wheels.mapper, the middleware pipeline, the DI container, and config — this prevents plugins from reaching directly into application scope.
    • Ordering concern: The issue says "call register() in load order, then boot() 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 explicit priority or order field in plugin.json to control it.

    Implementation Plan

    1. Create vendor/wheels/ServiceProviderInterface.cfc — interface with register(required any container) and boot(required any app) methods, as specified in the issue.

    2. Modify vendor/wheels/Plugins.cfc:

      • Add variables.$class.serviceProviders = [] in $init() (line 12).
      • In $pluginsProcess() (lines 146-178), after instantiating each plugin at line 156, check for ServiceProvider.cfc in the plugin folder using FileExists(local.pluginValue.folderPath & "/ServiceProvider.cfc").
      • If found, instantiate it and store in variables.$class.serviceProviders.
      • Add new method $processServiceProviders() that loops through collected providers and calls register(application.wheelsdi).
      • Add $bootServiceProviders(app) that calls boot(app) on each.
      • Add getter getServiceProviders().
    3. Modify vendor/wheels/events/onapplicationstart.cfc:

      • After $loadPlugins() at line 237, call register() on all service providers (before mixin injection at line 241).
      • After routes and dispatcher are created (after line 256), call boot() on all service providers — this is where they get access to the mapper, middleware pipeline, and fully initialized DI container.
    4. Define the app context passed to boot():

      • At minimum: {container: application.wheelsdi, mapper: application.$wheels.mapper, config: application.$wheels, environment: application.$wheels.environment}.
      • Consider a thin AppContext.cfc facade 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


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

  2. added this to the v3.2 milestone on Mar 17, 2026
  3. bpamiri commented on Mar 26, 2026

    @bpamiri
    CollaboratorAuthor

    Implemented: ServiceProviderInterface.cfc with register(container) / boot(app) lifecycle. Wired into both Plugins.cfc and the new PackageLoader. Tested in ServiceProviderInterfaceSpec.cfc.

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