1. Overview

The Play framework’s easy-to-use APIs simplify the management of our application’s configuration. Learning how to use these APIs is an important first step to effectively using the framework.

In this tutorial, we’ll learn how to access a Play application’s configuration data with Scala.

2. Play Setup

For our examples, we’ll use the latest version of Play, 2.8.2. However, much of what is presented here will work with versions of Play as far back as 2.4.

To create a new Play application, the easiest way is to use sbt and the giter8 Play template, play-scala-seed.g8:

sbt new playframework/play-scala-seed.g8

The template will prompt us for the name of our project and the top-level domain name for our organization. The latter will be used as a default for our base package namespace. More importantly, it will create a skeleton Play application.

The Play configuration file is <our-application-name>/conf/application.conf.

3. Configuration File Format

We can write Play configuration files using a syntax similar to Java properties files:

player.name = "Player 1"
player.email = "[email protected]"
player.age = 18
player.twitterHandle = null
player.signUpDate = "2020-09-22T10:10:06"

or a structure reminiscent of JSON:

player {
  name = "Player 1"
  email = "[email protected]"
  age = 18
  twitterHandle = null
  signUpDate = "2020-09-22T10:10:06"
}

Both of those formats specify the same values.

3.1. Overriding Configuration Values

It’s not an error to specify the same key/value more than once in our configuration. The basic rule of thumb is that the most recent definition will take precedence. We can take advantage of this using the syntax to set a configuration variable from an environment variable:

myApp.apiKey= null
myApp.apiKey= ${?MYAPP_APIKEY}

Here, we’re specifying that the default value for myApp.apiKey is null but that it can be set from an environment variable, MYAPP_APIKEY. This environment variable could then be set by the hosting platform or, if our app is hosted in a Docker image, passed to it from the command line.

4. Main Scala APIs

As of Play version 2.4, the recommended way to access the Play configuration is by injecting an instance of play.api.Configuration into our component:

class MyService @Inject() (configuration: Configuration) {
  val name = configuration.get[String]("myApp.user.name")
  val email = configuration.get[String]("myApp.user.email")
}

The Configuration class provides a number of ways to access our application’s configuration, but the API we’ll almost always use is:

Configuration.get[T](path: String)(implicit loader: ConfigLoader[T]): T

Note that we can also specify an Option-wrapped type such as configuration.get[Option[String]](“myApp.user”), which will return an Option value:

class MyService @Inject() (configuration: Configuration) {
  val name = configuration.get[Option[String]]("myApp.user.name").getOrElse("Player1")
  val email = configuration.get[Option[String]]("myApp.user.email").getOrElse("[email protected]")
}

We can use Option-wrapped variables in conjunction with the Option.getOrElse() method in a couple of ways:

  1. Default values – This is considered to be a bit of an anti-pattern since it can lead to unexpected behavior in the presence of multiple layers of overrides.
  2. Fine-grained error handling for missing values – We can throw an application-specific exception if the specified configuration variable is not defined.

The implicit ConfigLoader is a critical piece of the puzzle here. It is responsible for converting the values in your Play application’s configuration into specific Scala types. Play provides a number of ConfigLoaders that cover most cases. These range from support for String, Int, and Boolean, to more complex types such as Seq and Duration.

5. Custom Data Types

Using the ConfigLoader, we can easily support mapping our configuration data to custom data types. Let’s see how we would do that for the player configuration we showed previously.

First, we write a Scala case class that has the fields we’re interested in retrieving. As expected, we can specify optional fields by wrapping them with the Option trait:

case class PlayerInfo(
    name: String,
    email: String,
    age: Int,
    signUpDate: Date,
    twitterHandle: Option[String] = None
)

Next, we write a companion object that defines an implicit ConfigLoader parameterized by our new type:

object PlayerInfo {
  implicit val playerInfoConfigLoader: ConfigLoader[PlayerInfo] =
    (rootConfig: Config, path: String) => {
      val config = rootConfig.getConfig(path)
      new PlayerInfo(
        config.getString("name"),
        config.getString("email"),
        config.getInt("age"),
        javax.xml.bind.DatatypeConverter
          .parseDateTime(config.getString("signUpDate"))
          .getTime,
        Try(config.getString("twitterHandle")).toOption
      )
    }
}

Now we can load instances of the PlayerInfo class directly:

val playerInfo: PlayerInfo = configuration.get[PlayerInfo]("player")

This might seem like overkill, and if we’re only using this class in one place, it probably is. But if our intention is to write re-usable code, it’s nice to know that we can easily encapsulate the structure of our configuration data.

6. Conclusion

In this article, we introduced and discussed accessing our Play application’s configuration data. As we can see, Play makes this very easy to do, whether we are accessing that data as existing data types or as an application-specific data type. As always, the full source code can be found over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments