Controller & Actions
- Introduction
- Creating a new Controller
- Reading Query and Body Parameters
- Lifecycle
- Accessing the Current Action
- Accessing the Current Request
- Rendering Responses
- Redirects
- Action Execution
- Controller Context
- File Uploads
Introduction
In IHP an action is a place for request handling logic. A controller is just a group of related actions.
You can think about actions as messages sent to your application, e.g. a ShowPostAction { postId :: Id Post }
is the message “Show me the post with id $postId”.
Each action needs to be defined as a data structure inside Web/Types.hs
. Therefore you can see an overview of all the messages which can be sent to your application just by looking at Web/Types.hs
.
Creating a new Controller
We recommend using the code generators for adding a new controller. Using the web GUI, you can open http://localhost:8001/NewController. Using the CLI, run new-controller CONTROLLER_NAME
.
The following section will guide you through the manual process of creating a new controller.
Let’s say we want to create a new controller with a single action ShowPostAction
. First we need to define our types in Web/Types.hs
:
data PostsController
= ShowPostAction { postId :: !(Id Post) }
deriving (Eq, Show, Data)
This defines a type PostsController
with a data constructor ShowPostAction { postId :: !(Id Post) }
. The argument postId
will later be filled with the postId
parameter of the request URL. This is done automatically by the IHP router. IHP also requires the controller to have Eq
, Show
and Data
instances. Therefore we derive them here.
After we have defined the “interface” for our controller, we need to implement the actual request handling logic. IHP expects to find this inside the action
function of the Controller
instance. We can define this instance in Web/Controller/Posts.hs
:
module Web.Controller.Posts where
import Web.Controller.Prelude
instance Controller PostsController where
action ShowPostAction { postId } = renderPlain "Hello World"
This implementation for ShowPostAction
responds with a simple plain text message. The action
implementation is usually a big pattern match over all possible actions of a controller.
Reading Query and Body Parameters
Inside the action, you can access request parameters using the param
function. A parameter can either be a URL parameter like ?paramName=paramValue
(this is also called a query parameter), or given as a form field like <form><input name="paramName" value="paramValue"/></form>
(in that case we’re talking about a body parameter). The param
function will work with query and body parameters, so you don’t have to worry about that (in case a query and body parameter is set with the same name, the body parameter will take priority).
Given a request like GET /UsersAction?maxItems=50
, you can access the maxItems
like this:
action UsersAction = do
let maxItems = param @Int "maxItems"
An alternative request to that action can use a form for passing the maxItems
:
<form action={UsersAction} method="POST">
<input type="text" name="maxItems" value="50" />
<button type="submit">Send</button>
</form>
The value is automatically transformed to an Int
. This parsing works out of the box for Ids, UUID, Bools, Timestamps, etc. Here are some more examples:
action ExampleAction = do
let userName = param @Text "userName"
let timestamp = param @UTCTime "createdAt"
let userId = param @(Id User) "userId"
Missing parameters
In case there is a problem parsing the request parameter, an error will be triggered.
When the parameter is optional, use paramOrDefault
:
action UsersAction = do
let maxItems = paramOrDefault @Int 50 "maxItems"
When this action is called without the maxItems
parameter being set (or when invalid), it will fall back to the default value 50
.
There is also paramOrNothing
which will return Nothing
when the parameter is missing and Just theValue
otherwise.
Multiple Params With Same Name (Checkboxes)
When working with checkboxes sometimes there are multiple values for a given parameter name. Given a form like this:
<h1>Pancake</h1>
<input name="ingredients" type="checkbox" value="milk" /> Milk
<input name="ingredients" type="checkbox" value="egg" /> Egg
When both checkboxes for Milk and Egg are checked, the usual way of calling param @Text "ingredients"
will only return the first ingredient "Milk"
. To access all the checked ingredients
use paramList
:
action BuildFood = do
let ingredients = paramList @Text "ingredients"
When this action is called with both checkboxes checked ingredients
will be set to ["milk", "egg"]
. When no checkbox is checked it will return an empty list.
Similar to param
this works out of the box for Ids, UUID, Bools, Timestamps, etc.
Passing Data from the Action to the View
A common task is to pass data from the action to the view for rendering. You can do this by extending the view data structure by the required data.
Given an action like this:
action ExampleAction = do
render ExampleView { .. }
And an ExampleView
like this:
data ExampleView = ExampleView { }
instance View ExampleView where
html ExampleView { .. } = [hsx|Hello World!|]
Now we want to pass the user’s first name to the view, to make the hello world a bit more personal. We can just do it by adding a firstname :: Text
field to the ExampleView
data structure and then adding a {firstname}
to the HSX:
data ExampleView = ExampleView { firstname :: Text }
instance View ExampleView where
html ExampleView { .. } = [hsx|Hello World, {firstname}!|]
By now, the compiler will already tell us that we have not defined the firstname
field inside the action. So we also need to update the action:
action ExampleAction = do
let firstname = "Tester"
render ExampleView { .. } -- Remember: ExampleView { .. } is just short for ExampleView { firstname = firstname }
This will pass the first name "Tester"
to our view.
We can also make it act more dynamically and allow the user to specify the first name via a query parameter:
action ExampleAction = do
let firstname = paramOrDefault @Text "Unnamed" "firstname"
render ExampleView { .. }
This will render Hello World, Unnamed!
when the ExampleAction
is called without a firstname
parameter.
Accessing the FrameworkConfig inside Controllers and Views.
The config defined in Config/Config.hs
is available through the implicit parameter ?context
that is available in controllers:
action MyAction = do
let config = ?context.frameworkConfig
There are helpers that use this implicit parameter, e.g. isDevelopment
/isProduction
:
action MyAction = do
when isDevelopment (putStrLn "Running in dev mode")
Advanced: Working with Custom Types
Rarely you might want to work with a custom scalar value which is not yet supported with param
. Define a custom ParamReader
instance to be able to use the param
functions with your custom value type. For that, take a look at the existing instances of ParamReader
.
Populating Records from From Data with fill
When working with records, use fill
instead of param
. fill
automatically deals with validation failure when e.g. a field value needs to be an integer, but the submitted value is not numeric.
Here is an example of using fill
:
action CreatePostAction = do
let post = newRecord @Post
post
|> fill @'["title", "body"]
|> ifValid \case
Left post -> render NewView { .. }
Right post -> do
post <- post |> createRecord
setSuccessMessage "Post created"
redirectTo PostsAction
In the above example we are creating a new post record and then we are filling it with the values from the request. If the request contains invalid data, we are rendering the NewView
again, so the user can fix the errors. If the request contains valid data, we are creating the post record and redirecting to the PostsAction
.
Lifecycle
The Controller instance provides a beforeAction
function, which is called before the action
function is called. Common request handling logic like authentication is often placed inside beforeAction
to protect all actions of the controller against unprotected access.
Here is an example to illustrate the lifecycle:
instance Controller PostsController where
beforeAction = putStrLn "A"
action ShowPotsAction { postId } = putStrLn "B"
Calling the ShowPostAction
will cause the following output to be logged to the server console:
A B
Here is an example to illustrate authentication:
instance Controller PostsController where
beforeAction = ensureIsUser
action ShowPostAction { postId } = ...
Accessing the Current Action
Inside the beforeAction
and action
you can access the current action using the special ?theAction
variable. That is useful when writing controller helpers, because the variable is passed implicitly.
Accessing the Current Request
IHP uses the Haskell WAI standard for dealing with HTTP requests and responses. You can get access to the Wai Request data structure by using request
.
Take a look at the Wai documentation to see what you can do with the Wai Request
:
action ExampleAction = do
let requestBody = request |> getRequestBodyChunk
IHP provides a few shortcuts for commonly used request data:
action ExampleAction = do
-- Use `getRequestPath` for accessing the current request path (e.g. /Users)
putStrLn ("Current request url: " <> tshow getRequestPath)
-- Use `getRequestPathAndQuery` for accessing the path with all parameters (e.g. /ShowUser?userId=...)
putStrLn ("Current request url: " <> tshow getRequestPathAndQuery)
-- Access the request body
body <- getRequestBody
-- Get a request
let theRequest = request
Request Headers
Use getHeader
to access a request header:
action ExampleAction = do
let userAgent = getHeader "User-Agent"
In this example, when the User-Agent
header is not provided by the request
the userAgent
variable will be set to Nothing
. Otherwise, it will be set
to Just "the user agent value"
.
The lookup for the header in the request is case insensitive.
Use setHeader
to set a request header:
action ExampleAction = do
setHeader ("Max-Age", "10")
Rendering Responses
Rendering Views
Inside a controller, you have several ways of sending a response. The most common way is to use the render
function with a View
data structure, like this:
render ShowPostView { .. }
The render
function automatically picks the right response format based on the Accept
header of the browser. It will try to send an HTML response when HTML is requested, and will also try to send a JSON response when a JSON response is expected. A 406 Not Acceptable
will be send when the render
function cannot fulfill the requested Accept
formats.
Rendering Plain Text
Call renderPlain
to send a simple plain text response:
action ExampleAction = do
renderPlain "My output text"
Rendering HTML
Usually, you want to render your HTML using a view. See Rendering Views
for details.
Sometimes you want to render HTML without using views, e.g. doing it inline in the action. Call respondHtml
for that:
action ExampleAction = do
respondHtml [hsx|<div>Hello World</div>|]
You will need to import hsx
into your controller: import IHP.ViewPrelude (hsx)
.
Rendering a Static File
Use renderFile
to respond with a static file:
action ExampleAction = do
-- Allow the code to work both on Static and S3 storage types.
let storagePrefix = case storage of
StaticDirStorage -> "static/"
_ -> ""
renderFile (storagePrefix <> "terms.pdf") "application/pdf"
Rendering a Not Found Message
Use renderNotFound
to render a generic not found message, e.g. when an entity cannot be found:
action ExampleAction = do
renderNotFound
Redirects
Redirect to an Action
Use redirectTo
to redirect to an action:
action ExampleAction = do
redirectTo ShowPostAction { postId = ... }
When you need to pass a custom query parameter, you cannot use the redirectTo
function. See Redirect to a Path
for that.
The redirect will use HTTP status code 302
. The baseUrl
in Config/Config.hs
will be used. In development mode, the baseUrl
might not be specified in Config/Config.hs
. Then it will be set to localhost by default.
Redirect to a Path
Use redirectToPath
when you want to redirect to a path on the same domain:
action ExampleAction = do
redirectToPath "/blog/wp-login.php"
This can also be used to specify query parameter for actions:
action ExampleAction = do
redirectToPath ((pathTo ShowPostAction { .. }) <> "&details=on")
Redirect to a URL
Use redirectToUrl
to redirect to some external URL:
action ExampleAction = do
redirectToUrl "https://example.com/hello-world.html"
Action Execution
When calling a function to send the response, IHP will stop executing the action. Internally this is implemented by throwing and catching a ResponseException
. Any code after e.g. a render SomeView { .. }
call will not be called. This also applies to all redirect helpers.
Here is an example of this behavior:
action ExampleAction = do
redirectTo SomeOtherAction
putStrLn "This line here is not reachable"
The putStrLn
will never be called because the redirectTo
already stops execution.
When you have created a Response
manually, you can use respondAndExit
to send your response and stop action execution.
Controller Context
TODO
Request Context
Actions have access to the Request Context via the controller context:
let requestContext = ?context.requestContext
The Request Context provides access to the Wai request as well as information like the request query and post parameters and the uploaded files. It’s usually used by other functions to provide high-level functionality. E.g. the getHeader
function uses the Request Context to access the request headers.