Trying out Wirebox in place of DI/1
I wanted to leverage some of the extra power that Wirebox gives me over DI/1 so decided to have a go at switching from DI/1 to Wirebox. This is what I learnt.
Before I start digging into this, this isn’t a blog post about ColdBox vs FW/1, I’ve used both and like both – I’d recommend you try them both and make up your own mind.
Framework 1 (or FW/1) has support for Wirebox, you’ll need the WireBoxAdapter.cfc
that is part of the FW/1 project. This is handy because FW/1 expects getBean
and containsBean
methods, which are named getInstance
and containsInstance
in Wirebox. With the adapter there is no need to update calling code.
The first step in switching DI/1 to Wirebox is to install Wirebox. You can download it, or in my case, install it via commandbox:
$ box install wirebox
There are instructions on installation in the Wirebox docs here:
https://wirebox.ortusbooks.com/getting-started/installing-wirebox
Now that wirebox is installed, we need to look at how DI/1 is configured in my FW/1 application so that we can replicate the setup in Wirebox.
Here’s the current DI/1 configuration FW/1 is using:
variables.framework = { diLocations = "/model", diConfig = { singletonPattern = "(Service|DAO|Helper|Config)$" }, ... }
The above tells FW/1 to scan the “model” directory for components (cfcs) to manage. If any of the cfcs it finds have a name ending in Service, DAO, Helper or Config then those should be treated as singletons (only create one instance per application), everything else is a transient.
To tell FW/1 to use Wirebox we need to add in the `diEngine` key (by default in FW/1 this key has a value of ‘di1’ so I didn’t need to set it previously):
variables.framework = { diEngine = "wirebox", diLocations = "/model", diConfig = { singletonPattern = "(Service|DAO|Helper|Config)$" }, ... }
The `diLocations` and `diConfig` settings in FW/1 are not passed onto Wirebox so we need to create a config that Wirebox can use. In WireBox land, this is a Binder. I’m going to give mine the imaginative name `WireboxBinder.cfc` and put it in the `config` directory.
config/WireboxBinder.cfc
component extends="wirebox.system.ioc.config.Binder"{ function configure(){ mapDirectory(packagePath="model", influence=function(binder, path){ if(ReFindNoCase("(Service|DAO|Helper|Config)$", arguments.path)){ arguments.binder.asSingleton(); } }); } }
At first glance this might look a bit complicated, but it’s actually really powerful and flexible.
The mapDirectory function will scan the packagePath I pass in – here, it’s the “model” directory.
As I want to have control over what is treated as a singleton using my own naming convention, I can take advantage of the influence
callback to match by name – replicating what the singletonPattern
does in DI/1.
I really like having this level of control in WireBox.
NOTE: In Wirebox it is common practice to use annotations to define what is a singleton rather than using an
influence
callback as I have done. I’m using theinfluence
callback here as a way to replicate DI/1’ssingletonPattern
.To use annotations you just need to add
singleton
in the component definition. More info in the docs:
https://wirebox.ortusbooks.com/configuration/component-annotations
Now that we have a Wirebox Binder, we need to add that to the FW/1 config and delete the `diLocations` and `diConfig` settings:
variables.framework = { diEngine = "wirebox", diConfig = "config.WireboxBinder",diLocations = "/model",diConfig = { singletonPattern = "(Service|DAO|Helper|Config)$" },... }
The updated diConfig
parameter means that when FW/1 instantiates the BeanFactory, it’ll use Wirebox and configure it using `config/WireboxBinder.cfc`.
So far so good. FW/1 will now use Wirebox to wire up the FW/1 controllers and inject any dependancies those controllers have without the need to change any controller code.
However, we are not out of the woods yet. In the model directory, the cfcs that Wirebox is instantiating will not have the dependancies injected. This is because DI/1 will look for all `setXYZ` public methods and inject any beans it manages that match the `XYZ` name. As such you need to be a little bit careful as you may accidentally end up injecting a dependancy you didn’t mean to as you have a setter method which happens to match a bean that DI/1 is managing.
Wirebox takes a slightly different approach, you need to add annotations to your setters to tell Wirebox that yes, you really do want a dependancy to be injected. This means that you end up with annotations in your model code for a beanfactory the business object knows nothing about, but I think that it’s a small price to pay and makes it clear to someone reading the code what is injected for you.
In code terms the change is simple; for each cfc in my model directory that I want dependancies to be injected I need to update from:
component accessors="true" { property name="UserService"; }
to:
component accessors="true" { property name="UserService" inject; }
Now Wirebox will inject my UserService as a dependancy.
OK. Controllers wired, model wired up. The legacy code dragon awakes….
In my current setUpApplication
method, I am manually instantiating instances and then adding them to the beanfactory. For example:
function setUpApplication() { var BeanFactory = getBeanFactory(); // Load and configure Hoth var HothConfig = new hoth.config.HothConfig(); HothConfig.setapplicationName(this.name); ... var Exceptiontracker = new Hoth.HothTracker(HothConfig); BeanFactory.addBean('ExceptionTracker', Exceptiontracker); // inject FW/1 so that we can create URLs in the service layer Beanfactory.addBean('FW', this); }
I could move this to the WireboxBinder (which is the ideal approach), but as I’m referencing the `this` scope of Application.cfc, at the moment I want to make the least number of changes to my codebase (as being a legacy app the integration tests are, ummm, “sparse”) so I’m going to grab a reference to the binder and doing the mapping in the setUpApplication
method in Application.cfc.
function setUpApplication() { var BeanFactory = getBeanFactory(); // Load and configure Hoth var HothConfig = new hoth.config.HothConfig(); HothConfig.setapplicationName(this.name); ... var Exceptiontracker = new Hoth.HothTracker(HothConfig); var Binder = BeanFactory.getBinder(); Binder.map('ExceptionTracker').toValue(Exceptiontracker); // inject FW/1 so that we can create URLs in the service layer Binder.map('FW').toValue(this); }
With this final change in place, everything spins up and works exactly as it did with DI/1 but we now have Wirebox so can start to leverage it in future development / refactoring.
I should thank Brad Wood and Patrick Flynn on the CFML Slack channel for their invaluable guidance, advice and patience answering my questions. Without them I’d probably still be starring at error messages on my screen!
You must be logged in to post a comment.