This is a study note of Play framework based on official documentation and the Essentail Play book.

Introduction

Play is a lightweight, full-stack framework that includes an intergrated HTTP server, form handling, template engine, built-in security(CSRF protection), a routing mechanism, async HTTP and HTTP client, I18n support, and data persistence. It uses Akka actor, Akka Stream and Akka HTTP under the hood to provide stateless, non-blocking, and event-driven architecture to developers. Both the template and route descritpion are type-safe.

Play is distributed in two components:

  • a runtime library used by web applications
  • an sbt plugin that helps compile, build and run a web application. It uses different code layout than the default Scala project.

Actions and Controllers

Play uses actions to process most requests. An action is a function of Request => Result that takes a request as an imput and generates a result sent to the client. The BaseController has an Action field that is the default action builder that has several forms

  • takes a code block that returns a Result.
  • takes a request argument and a code block.
  • takes an implicit request argument and a code block, useful when the controller requires an implicit request.
  • takes an additional BodyParser argument before the above three forms.

A contoller is class that has methods to generate an action value. In other words, actions are packaged inside controllers that provide some help functions for action builders, for example, the Ok method. Controllers are created when the web application boots and persist until it shut down. Actions are created for an incoming request and is destroyed when the request is processed.

Reuest and Result

Request has a set of HTTP headers and a body. It has two methods for inspecting HTTP headers: headers and cookies. Header name is case insensitive while the cookie name is case sensitive.

The request body could be too large to be buffered in memory, therefore it is modelled as an Akka stream. For small body payloads, Play provides a BodyParser abstraction. An Action uses a BodyParser[A] to retrieve a value of type A from the HTTP request. Play includes parsers for plain text, JSON, XML and forms. It selects a parser based on the request Content-Type.

The Result represents an HTTP response that includes 1) a ResponseHeader that has a status code and a map of HTTP headers, 2) a body of an HttpEntity type. The play.api.mvc.Results trait and companion object have several helpers to create common results such as Ok, NotFound, BadRequest, InternalServerError, Status, Redirect. The TODO helper produces a Not implemented dummy page.

Play converts the result body to serialized type of HTML, JSON, XML or Array[Byte] etc, sets the HTTP headers and return the result. For customized data type (not buinsess model data type), define an implicit Writeable[A] to perform the serialization.

Router

Play framework treats a request as an event that include a path and an HTTP method. A router maps a request event to an action-builder method. Routes are defined in conf/routes. A router has three parts: an HTTP method, a path, and a controller method call that returns an Action value.

If a method paramter has a non-string type, you can add an explicit type for the paramter. Play supports String, Int, Long, Double, Float, Boolean, UUID, AnyVal wrapper for supported types. For other types, defeine a PathBindable or QueryStringBindable.

For each controller used in the routes file, the router generates a reverse controller in the controllers.routes.ControllerName package with the same action methods, same signature, but returning a Call instead of an Action. The Call defines an HTTP call that has the HTTP method and the URI.

Play includes a Default controller that provides a handful of actions such as redirect, notFound, erro, todo.

Play provides a router DSL called the String Interpolating Routring DSL (SIRD) that can be used to embed a server, custom routing etc.

Body Parser

The request body can be big thus it is modelled as an Akka stream. Play provides a BodyParser abstraction to map the body stream to an object in memory. The Request and the Action traits are defined as the following:

1
2
3
4
5
6
7
trait Request[+A] extends RequestHeader {
  def body: A
}

trait Action[A] extends (Request[A] => Result) {
  def parser: BodyParser[A]
}

It shows that an Action[A] uses a BodyParser[A] to process a body of type A to generate a result. Play has built-in body paresers that can handle String, ByteString, JSON, XML and forms. The default parser produces a body of type AnyContent that has asXXX methods such as asJson to access specific types.

To specify a body parser, pass it to the apply or async method of the Action.

To create a custom body parser, implemnts the BodyParser trait. The trait takes a RequestHeader and returns an Accumulator. The Accumulator is essentially a Sink[E, Future[A]] with some convinient methods such as map, mapFuture, and recover etc. The accumulator consumes ByteString and returns Either[Result, A].

Custom Action Builder and Action Composition

The Action object is an instance of the ActionBuilder trait. To create a custom action builder, extends ActionBuilderImpl and implement the invokeBlock method.

There are three ways to to define composable, reusable actons:

  • Wrapping the Action[A] object
  • Use the Action[A] object
  • Define composeAction method when implement ActionBuilder.

To build a data transformation pipeline, use an ActionFunction implementation that processes the request type and passes processed data to the next layer. There are a few pre-defined traits that implements ActionFunction:

  • ActionTransfomer: change the request
  • ActionFilter: selectively intercept request
  • ActionRefiner: general case of the above two
  • ActionBuilder: build an action

Play also provides a global filter API that is used for global cross cutting concerns.

The Twirl

Twirl is a powerful Scala-based tempalte engine. Templates are commpiled as standard Scala functions and follow a simple naming convention. For example, views/Application/index.scala.html will generate a views.html.Application.index class that has an apply() method.

The @ character starts the begining of a dynamic statement. Don’t include whitespace between keywords of dynamic statements and parentheses. Use curley bracket to write multi-statement block. Use @@ to escape it.

Declare parameters at the top of the template file using @(p1: T1, p2: T2) or even multiple groups @(p1: T1)(p2: T2). Twirl supports constructor declaration using @this(d: Dependency) at the start of the file, before the parameter declaration.

By default, dynamic content is secaped. use @Html(...) to show raw content.

To use layout, define views/main.scala.html, define the parameter @(content: Htmll), then use it as @main {...}. Use multiple parameters for multiple parts.

Define tags in views/tags/myTag.scala.html. It is defined as a function: @(p1: T1)(body: (T1) => Html).

Compile Time DI

Play provides runtime DI out of the box. The pros is that it minimizes boilderplate code while the cons is that it reomves the compile time validation. However, manual DI management, is tedious. To have the type-safe of compile time DI and minimize the boilerplate code, play provides public constructors and factory method APIs. Additionally, play implement a thin cake pattern to wire components. Macro-based autowiring tools, implicit auto wiring and thin cake pattern help the complie-time DI management.

Because Play allows application restarts, it uses an ApplicationLoader trait to start a Play application. The tait takes a Context argument that contains all the components required by a Play application that outlive the application itself and cannot be constructed by the application itself. For example, the source mapper component allows the Play error handler to render the corresponding soruce code when an exception is thrown. To simplify the implementation of the ApplicationLoader trait, Play has a bult-in abstract class BuiltInComponentsFromContext. The minimum requirement is to provide a router field to this class. To configure logging, use LoggerConfigurator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// To configure Play to use this application loader,
// configure it in application.conf: play.application.loader=MyApplicationLoader

import play.api._
import play.api.ApplicationLoader.Context
import play.api.routing.Router
import play.filters.HttpFiltersComponents

class MyApplicationLoader extends ApplicationLoader {
  def load(context: Context) = {
    // configure logging
    LoggerConfigurator(context.environment.classLoader).foreach {
        _.configure(context.enviroment, context.initialConfiguration, Map.empty)
    }
    // provide router
    new MyComponents(context).application
  }
}

class MyComponents(context: Context)
    extends BuiltInComponentsFromContext(context)
    with HttpFiltersComponents
    with controllers.AssetsComponents {
  lazy val barRoutes             = new bar.Routes(httpErrorHandler)
  lazy val applicationController = new controllers.Application(controllerComponents)

  lazy val router = new Routes(httpErrorHandler, applicationController, barRoutes, assets)
}

The above code creates a router using Routes class. The first argument is an HttpErrorHandler that handles parameter binding errors. The other arguments are controllers, other routes and assests.

Other components can be mixed into a component cake. Other components include AhcWSComponents for async WS API clicent and CSRFComponents for csrf protection.

Public Assets

Play has a built-in AssetsComponents to serve public assets. The controllers.Assets is used to define routes for static assets. The asset path can be configured or passed as a parameter to Assets.at method. The reverse routing is used as @routes.Assets.at(...). The minified *.[-]min.* files will be handled automatically in production mode.

sbt-web can configure asset pipeline to generate fingerprints. There are two ways to obtain the real path of a fingerprinted asset. One use @routes.Assets.versioned(...) and the other uses a configuration and @assetsFinder.path(...). Though @assetsFinder make it easy to run multiple self-contained applications and to test, you have to explicitly mix in AssetFinder in every controller and template.

Build and Configuration

The Build System

In addition to adding Play plugin, enabling a default set of settings by enablePlugins(PlayScala) to define the default imports for generated templates such as i18n messages and core APIs.

Put unmanaged dependencies into lib/ folder. Play uese Apache Ivy to implement managed dependencies. Additional repositories can be added like:

1
2
3
4
5
6
7
// sonatype
resolvers += "sonatype snapshots".at("https://oss.sonatype.org/content/repositories/snapshots/")`.

// local repo
resolvers += (
  "Local Maven Repository".at(s"file:///${Path.userHome.absolutePath}/.m2/repository")
)

To define a subproject, use project macro as lazy val myLibrary = project. The name of the value is the project’s name and folder. It uses the standard sbt project layout. A Play application can depend on other Play applications – the subprojects that use PlayScala plugin. Each Play subproject should use a differnt package name. There is only one application.conf and one routes file.

Configuration

System properties override application.conf settings that override reference.conf setings found on the classpath. The system properties config.resource and config.file can be used to specify a different configuration file other than the default application.conf file. You can also read configuration using an instance of Configuration.

Play uses a secret key for signing session cookies and CSRF tokens and other encryption functions. The best practice is as the following:

1
2
play.http.secret.key="changeme"
play.http.secret.key=${?APPLICATION_SECRET}

It uses APPLICATION_SECRETE system property, if defined, as the secrete. The playGenerateSecret command in the Play console can generate a secret.

Other configurations include session cookie, JDBC connection pool, logging, WS SSL/Cache, and Akka-related modules such as Http Server and default thread pool.