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 animplicit 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:
|
|
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 implementActionBuilder
.
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 requestActionFilter
: selectively intercept requestActionRefiner
: general case of the above twoActionBuilder
: 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
.
|
|
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:
|
|
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:
|
|
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.