Dependency Injection in Wheels 4.0: services.cfm, Scopes, and Auto-Wiring #3263
bpamiri
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
You've written the same
EmailServicethree times this week — oncenew'd in a controller, once in a model callback, once passed down through two function arguments to reach a place three layers deep. Wheels 4.0 ships a first-party DI container,wheelsdi, that ends that. Register a service once, give it a lifetime, resolve it anywhere.This post is a user-facing how-to — not the internals or boot-sequence story (that's the earlier From WireBox to wheelsdi post). It stays on the surface you actually touch.
Read: https://blog.wheels.dev/posts/dependency-injection-services-scopes
What it covers
Registration. A fresh Wheels app has no
config/services.cfm— the scaffold doesn't write one, andwheels.Bindingsonly registers internal framework interfaces. You author the file. Inside it,injector()returns the live container, and the fluent chain does the rest:var di = injector(); di.map("emailService").to("app.lib.EmailService").asSingleton(); di.bind("INotifier").to("app.lib.SlackNotifier").asSingleton(); di.map("currentUser").to("app.lib.CurrentUserResolver").asRequestScoped(); di.map("pdfBuilder").to("app.lib.PdfBuilder"); // transient — the defaultThree lifetimes. Transient (the default — no verb),
.asSingleton()(one per app, must be thread-safe),.asRequestScoped()(one per HTTP request). The verbs are exactlyasSingleton()/asRequestScoped()— there's no.transient()or.scope("..."); transient is what omission gives you.Resolving. The
service("name")global helper works in controllers, models, and views, and honors the registered scope. It throwsWheels.DI.ServiceNotFound(with a message pointing atservices.cfm) rather than returning null — guard optional services withinjector().containsInstance("name").Controller injection.
inject("emailService, currentUser")inconfig()declares dependencies at the class level; the framework resolves them per controller instance and attaches them asthis.<serviceName>. Class-declare / instance-resolve is precisely what makes request-scoped services correct inside controllers.Auto-wiring. An
init()parameter named after a registered mapping is resolved and injected automatically — when no explicitinitArgumentsare passed. Pass aninitArgumentsstruct and auto-wiring is skipped entirely; your struct wins. That's the override hook for test fakes. Auto-wiring also walks theextendschain and matches against live mappings on each resolve.The sharp edges (the part that saves you a debugging session)
.asSingleton().map("a").to("X").asSingleton()andmap("b").to("X").asSingleton()give two distinct X objects.service()/injector()throw, never return null —Wheels.DI.NotInitialized(too early) orWheels.DI.ServiceNotFound.Wheels.DI.CircularDependencywith the full chain; the resolving guard is request-scoped, so concurrent cold-start requests don't trip a false self-loop.mapInstance()instead ofmap()inside a plugin ServiceProvider — on Lucee/Adobe,map()on anany-typed injector can resolve to CFML's built-instruct.map()and silently no-op your registrations.Discuss
inject()or explicitservice()calls in actions — and why?Feedback on the post — what's unclear, what you'd want a follow-up to cover — welcome in this thread.
All reactions