Adam Warski

21 Aug 2013

Dependency Injection in Play! with MacWire

dependency injection
macwire
scala

The most recent release of MacWire (0.4) (a Scala macro to generate wiring code for class instantiation, DI container replacement) comes with new utilities which make it easier to integrate with frameworks which require by-class instance lookup. An example of such a framework is Play!, which is quite quickly gaining popularity in the web-development world.

There are two ways to find out how to do no-framework container-less dependency injection in Play! (and other web frameworks) with MacWire:

  1. read this blog
  2. try the MacWire+Play Typesafe Activator

Or you can do both ;)

Motivation

Many web frameworks require some form of generic instance lookups, e.g. when controller instances are referenced from views. In traditional DI containers like Guice, this is provided out-of-the-box, as it is the main way to get the wired objects. For example in Guice we have the Injector which translates Class objects into instances of the given class.

When using MacWire (or when doing “manual” DI), we are doing as much as possible in a type-safe way, simply referencing the objects as fields/methods in a container object, so we don’t usually need such maps. But for web framework integration, that is still needed.

General utilities

MacWire 0.4 comes with two general utilities useful for doing by-class instance lookups.

The first is a macro, valsByClass(object) which generates (at compile-time of course) a map of vals in the given object, keyed by their classes. So for example:

object Example1 {
   val aString = "Hello World!"
   val userAuthenticator = new UserAuthenticator()
   // here we are using the core MacWire feature: the wire macro
   val userFinder = wire[UserFinder] 
}

val theMap = valsByClass(Example1)

will translate the following code:

val theMap = Map[Class[_], AnyRef](
   classOf[String] -> Example1.aString,
   classOf[UserAuthenticator] -> Example1.userAuthenticator,
   classOf[UserFinder] -> Example1.userFinder
)

The second utility, InstanceLookup, extends that map to allow by-subclass/by-trait lookups. For example, say we have a Database trait and an implementation, MySQLDatabase, which ends up in the vals-by-class map under the Class[MySQLDatabase] key.

However, we would still like to get the instance when querying by Database. We can achieve that by wrapping the map with InstanceLookup:

trait Database
class MySQLDatabase extends Database

object App {
   val database = new MySQLDatabase()
}

val instanceLookup = InstanceLookup(valsByClass(App))

require(instanceLookup(classOf[Database]) == App.database)

Play integration

Having these utilities we may now proceed to integrating MacWire with Play.

The main change is that controllers now won’t be objects, but classes (usually with non-empty constructor parameter lists – the dependencies). The challenge is now how to tell Play how to get controller istances?

Let’s say we have the following controller and service class:

package controllers

import play.api.mvc._
import services.HelloWorldProvider

class MainController(helloWorldProvider: HelloWorldProvider) extends Controller {
  def index = Action {
    Ok(views.html.index(helloWorldProvider.text))
  }
}

package services

class HelloWorldProvider {
   def text = "Hello World!"
}

Here the controller (MainController) has one dependency, the HelloWorldProvider service.

Now that we have the example ready, we can proceed with the integration. There are three easy steps. Firstly we need to wire the dependencies. We can use the wire[] macro, or just go with manual DI:

trait OurApplication {
   lazy val helloWorldProvider = new HelloWorldProvider
   lazy val mainController = new MainController(helloWorldProvider)
}

Secondly, we need to specify in the routes file that we want to provide controller instances ourselves, instead of letting Play instantiate the controllers. This is done by adding the @ character before the controller name:

// File: conf/routes

GET     /                           @controllers.MainController.index()

Finally, we need to create a Global object in the main package. That’s the main integration point; here we use the by-class instance lookup maps:

import com.softwaremill.macwire.{InstanceLookup, Macwire}
import play.api.GlobalSettings

object Global extends GlobalSettings with Macwire {
  val instanceLookup = InstanceLookup(valsByClass(new OurApplication {}))

  override def getControllerInstance[A](controllerClass: Class[A]) = 
      instanceLookup.lookupSingleOrThrow(controllerClass)
}

And that’s it! From now on the controller instances will be provided from our map, using the wiring we defined in the OurApplication trait.

Be sure to checkout the Activator if you want to see a working example in action!

Adam

comments powered by Disqus

Any questions?

Can’t find the answer you’re looking for?