|
| 1 | +# Dependency Injection |
| 2 | + |
| 3 | +Wheels includes a built-in dependency injection (DI) container that manages service objects and their lifecycles. Register services once in `config/services.cfm`, then resolve them anywhere in your application with `service()` or declarative `inject()`. |
| 4 | + |
| 5 | +## Registering Services |
| 6 | + |
| 7 | +Create `config/services.cfm` to register your services at application startup: |
| 8 | + |
| 9 | +```cfm |
| 10 | +<cfscript> |
| 11 | +// Get a reference to the DI container |
| 12 | +var di = injector(); |
| 13 | +
|
| 14 | +// Register a transient (new instance each time) |
| 15 | +di.map("emailService").to("app.lib.EmailService"); |
| 16 | +
|
| 17 | +// Register a singleton (one instance for the app lifetime) |
| 18 | +di.map("cacheService").to("app.lib.CacheService").asSingleton(); |
| 19 | +
|
| 20 | +// Register a request-scoped service (one instance per HTTP request) |
| 21 | +di.map("currentUser").to("app.lib.CurrentUserService").asRequestScoped(); |
| 22 | +
|
| 23 | +// Interface binding — semantic alias for map() |
| 24 | +di.bind("INotifier").to("app.lib.SlackNotifier").asSingleton(); |
| 25 | +</cfscript> |
| 26 | +``` |
| 27 | + |
| 28 | +## Lifecycle Scopes |
| 29 | + |
| 30 | +| Scope | Method | Behavior | |
| 31 | +|-------|--------|----------| |
| 32 | +| **Transient** | *(default)* | New instance every time `getInstance()` or `service()` is called | |
| 33 | +| **Singleton** | `.asSingleton()` | One shared instance for the entire application lifetime | |
| 34 | +| **Request** | `.asRequestScoped()` | One instance per HTTP request, automatically cleaned up | |
| 35 | + |
| 36 | +### When to use each scope |
| 37 | + |
| 38 | +- **Transient**: Stateless utilities, formatters, validators |
| 39 | +- **Singleton**: Database connection pools, configuration, caches |
| 40 | +- **Request**: Current user context, request-specific state, audit loggers |
| 41 | + |
| 42 | +## Using service() in Controllers and Views |
| 43 | + |
| 44 | +The `service()` global helper resolves a registered service by name: |
| 45 | + |
| 46 | +```cfm |
| 47 | +// In a controller action |
| 48 | +function show() { |
| 49 | + var emailService = service("emailService"); |
| 50 | + emailService.send(to=user.email, subject="Welcome!"); |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +```cfm |
| 55 | +<!--- In a view ---> |
| 56 | +<cfset var config = service("appConfig")> |
| 57 | +<p>Version: #config.getVersion()#</p> |
| 58 | +``` |
| 59 | + |
| 60 | +## Declarative inject() in Controllers |
| 61 | + |
| 62 | +For services used across multiple actions, declare them in your controller's `config()`: |
| 63 | + |
| 64 | +```cfm |
| 65 | +component extends="Controller" { |
| 66 | + function config() { |
| 67 | + inject("emailService"); |
| 68 | + // Or inject multiple at once: |
| 69 | + inject("emailService, cacheService, currentUser"); |
| 70 | +
|
| 71 | + // Other config... |
| 72 | + filters(through="authenticate"); |
| 73 | + } |
| 74 | +
|
| 75 | + function create() { |
| 76 | + // Services are available as this.serviceName |
| 77 | + this.emailService.sendWelcome(user); |
| 78 | + this.cacheService.invalidate("users"); |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Injected services are resolved when the controller instance is created (per-request), so request-scoped services get fresh instances automatically. |
| 84 | + |
| 85 | +## Interface Binding with bind() |
| 86 | + |
| 87 | +Use `bind()` instead of `map()` when you want to emphasize that the name represents an abstraction: |
| 88 | + |
| 89 | +```cfm |
| 90 | +// In config/services.cfm |
| 91 | +var di = injector(); |
| 92 | +
|
| 93 | +// Bind an interface name to a concrete implementation |
| 94 | +di.bind("IPaymentGateway").to("app.lib.StripeGateway").asSingleton(); |
| 95 | +
|
| 96 | +// In production, swap the implementation without changing consumers |
| 97 | +// di.bind("IPaymentGateway").to("app.lib.PayPalGateway").asSingleton(); |
| 98 | +``` |
| 99 | + |
| 100 | +`bind()` is functionally identical to `map()` — CFML has no formal interfaces, so this is semantic sugar for code clarity. |
| 101 | + |
| 102 | +## Auto-Wiring |
| 103 | + |
| 104 | +When a service's `init()` method has parameters whose names match registered service names, the container automatically resolves and injects them: |
| 105 | + |
| 106 | +```cfm |
| 107 | +// app/lib/OrderService.cfc |
| 108 | +component { |
| 109 | + public OrderService function init( |
| 110 | + required any emailService, |
| 111 | + required any cacheService |
| 112 | + ) { |
| 113 | + variables.emailService = arguments.emailService; |
| 114 | + variables.cacheService = arguments.cacheService; |
| 115 | + return this; |
| 116 | + } |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +```cfm |
| 121 | +// config/services.cfm |
| 122 | +var di = injector(); |
| 123 | +di.map("emailService").to("app.lib.EmailService").asSingleton(); |
| 124 | +di.map("cacheService").to("app.lib.CacheService").asSingleton(); |
| 125 | +di.map("orderService").to("app.lib.OrderService").asSingleton(); |
| 126 | +
|
| 127 | +// orderService.init() automatically receives emailService and cacheService |
| 128 | +``` |
| 129 | + |
| 130 | +Auto-wiring is opt-in: it only activates when no explicit `initArguments` are passed to `getInstance()`. Parameters that don't match any registered mapping are skipped. |
| 131 | + |
| 132 | +### Circular Dependency Protection |
| 133 | + |
| 134 | +The container detects circular dependencies and throws `Wheels.DI.CircularDependency` with a clear message showing the resolution chain, rather than causing a stack overflow. |
| 135 | + |
| 136 | +## Environment-Specific Services |
| 137 | + |
| 138 | +Just like `config/settings.cfm`, you can create environment-specific service overrides: |
| 139 | + |
| 140 | +``` |
| 141 | +config/ |
| 142 | + services.cfm # Base registrations |
| 143 | + development/ |
| 144 | + services.cfm # Override for development |
| 145 | + testing/ |
| 146 | + services.cfm # Override for testing |
| 147 | + production/ |
| 148 | + services.cfm # Override for production |
| 149 | +``` |
| 150 | + |
| 151 | +Environment-specific services are loaded after the base file, so they can override registrations: |
| 152 | + |
| 153 | +```cfm |
| 154 | +// config/testing/services.cfm |
| 155 | +var di = injector(); |
| 156 | +di.bind("IPaymentGateway").to("app.lib.MockPaymentGateway").asSingleton(); |
| 157 | +di.map("emailService").to("app.lib.MockEmailService").asSingleton(); |
| 158 | +``` |
| 159 | + |
| 160 | +## Introspection API |
| 161 | + |
| 162 | +The container provides methods to inspect its state: |
| 163 | + |
| 164 | +```cfm |
| 165 | +var di = injector(); |
| 166 | +di.getMappings(); // struct of all name → componentPath mappings |
| 167 | +di.containsInstance("emailService"); // true if registered |
| 168 | +di.isSingleton("emailService"); // true if marked as singleton |
| 169 | +di.isRequestScoped("currentUser"); // true if marked as request-scoped |
| 170 | +``` |
| 171 | + |
| 172 | +## Examples |
| 173 | + |
| 174 | +### Full Application Setup |
| 175 | + |
| 176 | +```cfm |
| 177 | +// config/services.cfm |
| 178 | +<cfscript> |
| 179 | +var di = injector(); |
| 180 | +
|
| 181 | +// Infrastructure |
| 182 | +di.map("mailer").to("app.lib.PostmarkMailer").asSingleton(); |
| 183 | +di.map("cache").to("app.lib.RedisCache").asSingleton(); |
| 184 | +di.map("logger").to("app.lib.AppLogger").asSingleton(); |
| 185 | +
|
| 186 | +// Request-scoped |
| 187 | +di.map("currentUser").to("app.lib.CurrentUserResolver").asRequestScoped(); |
| 188 | +di.map("auditLog").to("app.lib.AuditLogger").asRequestScoped(); |
| 189 | +
|
| 190 | +// Business logic (auto-wired) |
| 191 | +di.map("orderService").to("app.lib.OrderService"); |
| 192 | +di.map("invoiceService").to("app.lib.InvoiceService"); |
| 193 | +</cfscript> |
| 194 | +``` |
| 195 | + |
| 196 | +```cfm |
| 197 | +// app/controllers/Orders.cfc |
| 198 | +component extends="Controller" { |
| 199 | + function config() { |
| 200 | + inject("orderService, currentUser"); |
| 201 | + filters(through="authenticate"); |
| 202 | + } |
| 203 | +
|
| 204 | + function create() { |
| 205 | + var order = model("Order").create(params.order); |
| 206 | + this.orderService.processNewOrder(order, this.currentUser); |
| 207 | + redirectTo(route="order", key=order.id); |
| 208 | + } |
| 209 | +} |
| 210 | +``` |
0 commit comments