Controllers¶
A controller is a subclass of MVC::Keayl::Controller. Each public method is an
action. The framework builds one controller instance per request, dispatches the
named action on it, and returns the response.
1 2 3 4 5 6 7 8 9 10 11 | |
Per-request state¶
A controller instance carries the request, a fresh response, and the merged params for that request:
1 2 3 | |
Each instance gets its own response, so actions never share state across requests.
Params¶
self.params merges the path params (from routing), the query string, and the
request body into one MVC::Keayl::Parameters collection. Path params win over
the body, which wins over the query. Access is indifferent: a key is coerced to a
string, so params<id> and params{5} reach the same value.
Bracketed names build nested structures, the same way Rack does:
1 2 3 4 | |
The body is parsed by its Content-Type:
application/x-www-form-urlencodedparses like a query string.application/jsonparses the JSON object into params.multipart/form-dataparses text fields, and file fields into a{ filename, content, type }hash.
build-params(%path-params, $request) produces the merged Parameters for a
request, which the dispatcher passes to the controller.
Dispatch and implicit render¶
dispatch($action) runs the named action and returns the response. An action
interacts with the response directly:
1 2 3 4 | |
When an action does not write to the response, its return value is rendered implicitly. An action whose body is still empty has its string return value written to the response:
1 2 3 | |
Only actions defined on the controller are dispatchable. Dispatching an unknown action, or one of the base controller's own methods, raises.
Render¶
render writes the response explicitly. The content modes set a default content
type and the body:
1 2 3 4 | |
status sets the status, on its own or alongside content, and content-type
overrides the default:
1 2 | |
A template is rendered by name, by another action, or inline. Locals and a layout are passed as options:
1 2 3 4 5 6 | |
Template, inline, and layout rendering go through the controller's view renderer.
When a renderer is configured, an action that does not render explicitly
implicitly renders the template named after the action. An index action with no
render call renders the index template, so the explicit call is rarely needed.
Implicit render follows the request format. The default is html, but when the
request asks for another format (a path extension such as /feed.atom) and a
template exists for it, that template is rendered with the matching content type.
The format-specific template is name.<format>.<handler> (for example
index.xml.haml); the html layout is skipped for non-html formats. When no
template exists for the requested format, implicit render falls back to the html
template. Pass format to choose one explicitly:
1 | |
The handler is the file's last extension, so the default HAML handler covers the
markup formats it can emit (html, xml, atom, rss, svg). It does not produce JSON.
For JSON, render data directly with render(:json($data)) or
respond-to, or register a handler for a JSON template
language so index.json.<handler> resolves.
Rendering twice raises a double-render error.
Redirects and head¶
redirect-to sends a redirect with an empty body. It takes a path or URL, and an
optional status (numeric or named, defaulting to 302). :back redirects to the
Referer, with a fallback when there is none:
1 2 3 4 5 | |
A named-route redirect is the path a URL helper produces, passed as the target.
head sends a status and headers with no body. The status is numeric or named,
and named arguments become headers (location becomes the Location header):
1 2 3 | |
A redirect, like a render, marks the response performed, so a render or redirect after one raises a double-render error.
Sending files and data¶
send-data sends an in-memory payload, a string or a Blob, with a content type
and disposition. The default disposition is attachment:
1 2 3 | |
send-file sends a file from disk. The content type is guessed from the
extension unless given, and the filename defaults to the basename:
1 2 | |
send-file advertises Accept-Ranges: bytes and honours a Range request
header, replying with 206 Partial Content, a Content-Range header, and the
requested slice. Open-ended (bytes=100-) and suffix (bytes=-100) ranges both
work.
Callbacks¶
before-action, after-action, and around-action register callbacks on the
controller class. A callback is a method name or a block. Around callbacks
receive a continuation they invoke to run the rest of the chain. The registration
calls go after the class definition, because the class name is not yet bound to
the composed type while the body is still being built:
1 2 3 4 5 6 7 8 | |
A request runs the before callbacks in order, then the around callbacks wrapping the action, then the after callbacks in reverse order.
only and except scope a callback to specific actions, and if / unless
gate it on a method or block:
1 2 | |
Declaring callbacks inside the class¶
To keep registration in the class body, call through $?CLASS, the handle to the
class being defined:
1 2 3 4 5 6 7 8 | |
Or attach a callback to its method with the is before-action, is
around-action, and is after-action traits. The method is the callback, and the
trait takes the same only / except / if / unless options:
1 2 3 4 5 6 | |
A subclass inherits its parents' callbacks and can drop one with
skip-before-action (and skip-after-action / skip-around-action), with the
same only / except scoping:
1 | |
A before or around callback that renders or redirects halts the chain: the action and the remaining callbacks do not run.
Strong parameters¶
require and permit whitelist params before they reach the model. require
returns the value at a key, raising when it is missing or empty. permit returns
a new Parameters containing only the listed keys:
1 | |
permit accepts scalar keys, arrays (an empty-array spec), nested hashes, and
arrays of hashes:
1 2 3 4 5 6 | |
A value that is not a permitted scalar (a nested hash or array) is dropped unless
it is permitted explicitly. permit-all is the escape hatch that marks the
parameters permitted and keeps every key.
Unpermitted keys are dropped. The action taken is configurable, globally with
MVC::Keayl::Parameters.unpermitted-action('raise') or per call with
permit(..., :on-unpermitted<raise>), which raises instead of dropping.
Expecting parameters¶
expect combines require and permit into one strict call. It returns the
permitted value at a key, raising X::MVC::Keayl::ParameterMissing when the key
is missing or when the value is the wrong shape:
1 | |
The default rescue turns that exception into a 400, so a malformed payload (a
scalar where a hash is expected, or a missing key) is rejected without reaching
the model. expect follows the value's shape: a hash value permits the listed
keys, an array value permits each element as a hash, and an empty-array spec
returns an array of scalars:
1 2 3 | |
Wrapping parameters¶
wrap-parameters nests a JSON request body under a root key, so a client that
posts {"name": "Ada"} to UsersController reads it back as params<user>. The
key defaults to the controller name singularized, and only JSON requests are
wrapped:
1 | |
Pass an explicit key as the first argument, restrict the formats with :format,
and choose attributes with :include or :exclude:
1 | |
Or declare it on the class with the is wrap-parameters trait:
1 | |
Wrapping is skipped when the root key is already present in the params, so an explicitly nested body is left untouched.
Custom renderers¶
add-renderer registers a render option backed by a block, so render csv: $rows
dispatches to it. The block receives the controller, the rendered value, and the
remaining render options, sets its own content type, and returns the body:
1 2 3 4 5 6 | |
Renderers are registered globally and are available to every controller.
Rescuing exceptions¶
rescue-from maps an exception type to a handler, a method name or a block, that
turns the exception into a response:
1 2 3 4 5 | |
Or attach the mapping to the handler method with the is rescue-from trait. The
method is the handler, and the trait takes the exception type (or several):
1 2 3 | |
Lookup is inheritance-aware: when more than one registered type matches a raised exception, the most specific handler wins. A subclass inherits its parents' mappings and can override them.
The base controller ships default mappings: X::MVC::Keayl::NotFound becomes a
404, and X::MVC::Keayl::ParameterMissing and
X::MVC::Keayl::UnpermittedParameters become a 400. An exception with no matching
handler propagates.
Helpers and shared state¶
helper-method exposes a controller method to the template under its own name:
1 2 3 4 5 | |
Or mark the method with the is helper-method trait:
1 2 3 | |
assign records a value for the template:
1 2 3 4 | |
When an action renders, the template locals are built from the helper-method
values, then the assigned values, then any explicit locals (which win). The
recorded assigns are also readable through self.assigns.
The ApplicationController pattern¶
Put shared callbacks, rescue handlers, and helpers on a base controller, and subclasses inherit them:
1 2 3 4 5 6 7 8 | |
Callbacks, rescue-from mappings, and helper-method declarations all collect
across the inheritance chain, and a subclass can add to or override what it
inherits.
Every class-level declaration shown here has an is trait form so it can be
written in the class header instead of a call after the class. The ones that
attach to a method (before-action, after-action, around-action,
rescue-from, helper-method) go on the method declaration; the ones that
configure the whole controller (layout, filter-parameters, add-flash-types,
protect-from-forgery, rate-limit, wrap-parameters,
http-basic-authenticate-with) go on the class:
1 2 3 4 5 6 7 8 | |