Keep controllers clean with custom action builders

Shane Auckland
shanethehat
Published in
4 min readJan 12, 2016

--

When writing controllers, there are often elements that are needed over and over again. Maybe you always seem to be getting the same objects from a repository, or you want to do authentication. Repeating simple operations like these leave controller actions bloated, and hard to read. Play Framework offers a simple and powerful way to abstract these repeated elements.

Anatomy of an action

In Play, as with most web frameworks, an action is a method of a controller that turns a request into a response. More specifically though, the Play framework contains an ActionBuilder trait which, among other things, gives a method signature for invokeBlock:

def invokeBlock[A](request: R[A], block: P[A] => Future[Result]): Future[Result]

This trait is used to create the Action object, with a trivial implementation of invokeBlock:

object Action extends ActionBuilder[Request] {
private val logger = Logger(Action.getClass)
def invokeBlock[A](request: Request[A], block: (Request[A]) =>
Future[Result]) = block(request)
}

This means that in the standard implementation of an action method, the block you provide is executed unconditionally.

def index() = Action { implicit request =>
Ok("Hello")
}

Extending the action

It is possible to replace this rather boring default Action object with your own. By implementing the ActionBuilder trait and only conditionally executing the provided block, you can quickly implement a common action requirement. In this example I'll use authentication checking.

Let's assume we have a user service that will give us a user when we give a valid token:

class UserService {
def findValidUser(token: String): Option[User] = ???
}

And that this service is being injected into our controller:

class Application(userService: UserService) {
def doSomething() = Action { implicit request =>
userService.findValidUser(request.headers.get("UserToken")) match {
case Some(user) => {
val foo = doSomethingWithoutAUser()
Ok("the result " + foo)
}
case None => Unauthorized("Invalid user token")
}
}
def doSomethingElse() = Action { implicit request =>
userService.findValidUser(request.headers.get("UserToken")) match {
case Some(user) => {
val bar = doSomethingElseWithTheUser(user)
Ok("the other result " + bar)
}
case None => Unauthorized("Invalid user token")
}
}
}

So we can see that this controller has only 2 actions, but a lot of duplication. In both actions it is attempting to get the user, and if the user service returns a None, it is issuing a 401 error.

The first thing we can do to make this neater is to move that logic into a custom Action object:

object AuthAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
userService.findValidUser(request.headers.get("UserToken")) match {
case Some(user) => block(request)
case None => Future(Results.Unauthorized("Invalid user token"))
}
}
}

Note that as the AuthAction object uses the userService, it needs to be placed inside the controller definition.

The existing actions can now be rewritten to use this new AuthAction object, which removes the duplication. Let's do the first action method first:

def doSomething() = AuthAction { implicit request =>
val foo = doSomethingWithoutAUser()
Ok("the result " + foo)
}

Much better! This is now a clean action that is very easy to read and understand. Let’s try for the second action:

def doSomethingElse() = AuthAction { implicit request =>
userService.findValidUser(request.headers.get("UserToken")) match {
case Some(user) => {
val bar = doSomethingElseWithTheUser(user)
Ok("the other result " + bar)
}
}
}

Hmm... that's not a big improvement. The problem here is that the user object is needed in the action block, and it is not being passed in from the AuthAction object.

Extending the request

The solution to getting objects from the AuthAction object into the action block is to extend the request itself. Since the request is already being passed into the block, this approach involves the least refactoring, especially in situations where there are many controller actions.

Play Framework provides a class called WrappedRequest which extends Request and accepts a Request, effectively allowing for decoration of a request. Defining a new Request that supports a User object is made simple:

class UserRequest[A](val user: User, request: Request[A]) extends WrappedRequest(request)

A few minor changes to the AuthAction object ensure that the UserRequest is passed into the block:

object AuthAction extends ActionBuilder[UserRequest] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
userService.findValidUser(request.headers.get("UserToken")) match {
case Some(user) => block(new UserRequest[A](user, request))
case None => Future(Results.Unauthorized("Invalid user token"))
}
}
}

All that's changed there is that the type given to the ActionBuilder trait specifies the new Request type, and that the block is called with an instance of UserRequest, which is decorating the original request.

Finally, we can tidy up the second controller action by using the user property of the new UserRequest:

def doSomethingElse() = AuthAction { implicit request =>
val bar = doSomethingElseWithTheUser(request.user)
Ok("the other result " + bar)
}

Conclusion

By using custom ActionBuilders and decorating requests it is possible to greatly reduce duplication and keep controller actions slim. This allows for more readable code that is much easier to maintain.

--

--