Actions that read like the request.
A controller is a subclass of MVC::Keayl::Controller, and each public method is an
action. The framework builds one instance per request, runs the callbacks, dispatches the named
action, and returns the response. Actions never share state across requests.
#actions and per-request state
Each instance carries the request, a fresh response, and the merged params for that request. When an action does not write to the response, its string return value is rendered implicitly. Only methods defined on the controller are dispatchable.
use MVC::Keayl::Controller;
class UsersController is MVC::Keayl::Controller {
method index {
# becomes the response body
'all users';
}
method show {
'user ' ~ self.params<id>;
}
method create {
self.response.status = 201;
self.response.body('created');
}
}
self.request is the incoming Request, self.response the
outgoing Response, and self.params the merged collection.
#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: params<id> and
params{5} reach the same value. Bracketed names build nested structures, the way
Rack does.
# params<user><name>, params<user><email>
user[name]=Ada&user[email]=a@b.com
# params<ids> is ['1', '2']
ids[]=1&ids[]=2
# params<user><roles> is ['admin']
user[roles][]=admin
# params<users>[0] is { name => 'A', age => '1' }
users[][name]=A&users[][age]=1
The body is parsed by its Content-Type: form-urlencoded parses like a query string,
JSON parses the object into params, and multipart parses text fields plus file fields into a
{ filename, content, type } hash.
#render
render writes the response explicitly. The content modes set a default content type
and the body; status and content-type override. A template is rendered
by name, by another action, or inline, with locals and a layout passed as options.
# application/json
self.render(json => { ok => True });
# text/plain
self.render(plain => 'hello');
# text/html
self.render(html => '<b>hi</b>');
# custom content type
self.render(body => 'id,name',
content-type => 'text/csv');
# text/plain, status 201
self.render(plain => 'created', status => 201);
# no body
self.render(status => 204);
# the show template
self.render('show');
# with locals
self.render('show', locals => { user => $user });
# a named layout
self.render('show', layout => 'admin');
# no layout
self.render('show', layout => False);
When a renderer is configured, an action that does not render explicitly implicitly renders the template named after the action. Rendering twice raises a double-render error.
#redirects, head, and files
redirect-to sends a redirect with an empty body, taking a path or URL and an
optional status (numeric or named). :back redirects to the Referer
with a fallback. head sends a status and headers with no body.
self.redirect-to('/dashboard');
self.redirect-to('/new', status => 301);
# named status, 303 See Other
self.redirect-to('/x', status => 'see-other');
self.redirect-to(:back, fallback => '/home');
self.head(204);
self.head('created', location => '/users/5');
self.send-data($csv,
type => 'text/csv',
filename => 'report.csv');
self.send-file('public/logo.png',
disposition => 'inline');
send-file advertises Accept-Ranges: bytes and honours a
Range header, replying with 206 Partial Content, a
Content-Range header, and the requested slice.
#callbacks
before-action, after-action, and around-action register
callbacks on the controller. A callback is a method name or a block. Around callbacks
receive a continuation they invoke to run the rest of the chain. only /
except scope a callback to specific actions, and if / unless
gate it.
Attach a callback inline 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.
class UsersController is MVC::Keayl::Controller {
method authenticate
is before-action(except => <index show>) {
...
}
method timer($next)
is around-action { ...; $next(); ... }
method audit is after-action { ... }
method show { ... }
}
Registering on the class is equivalent, and reads better when a callback is gated or shared across a hierarchy.
UsersController.before-action('authenticate',
except => <index show>);
UsersController.around-action('timer');
UsersController.before-action('require-admin',
if => 'is-admin');
A request runs the before callbacks in order, then the around callbacks wrapping the action, then
the after callbacks in reverse. A before or around callback that renders or redirects halts the
chain. A subclass inherits its parents' callbacks and can drop one with
skip-before-action.
#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, with
nested shapes spelled out. expect combines the two into one strict call.
my $attrs = self.params
.require('user')
.permit('name', 'email');
self.params.require('user').permit(
'name', 'email',
# an array of scalars
roles => [],
# a nested hash with these keys
address => <street city>,
# an array of hashes with these keys
tags => <id name>,
);
my $user = self.params.expect(
user => <name email>);
# an array of scalars
self.params.expect(ids => []);
# a required scalar
self.params.expect('id');
expect raise X::MVC::Keayl::ParameterMissing, which the
default rescue turns into a 400 before it reaches the model.
#rescuing exceptions
rescue-from maps an exception type to a handler that turns it into a response.
The is rescue-from trait attaches the mapping to the handler method and takes the
exception type, or several. Lookup is inheritance-aware: the most specific registered handler
wins, and a subclass inherits and can override its parents' mappings.
class ArticlesController is MVC::Keayl::Controller {
method not-found($error)
is rescue-from(X::MVC::Keayl::NotFound) {
self.head(404);
}
}
The base controller ships default mappings: NotFound becomes a 404, and
ParameterMissing and UnpermittedParameters become a 400. An exception
with no matching handler propagates.
#flash
The flash carries a short message across a redirect. It is stored in the session and exposed as
flash. A value written to it survives exactly one request, then is dropped, so a
message shown after a redirect does not linger. When it has entries it is exposed to views as the
flash local.
method create {
self.flash<notice> = 'Saved';
self.redirect-to('/posts');
}
-# app/views/posts/index.html.haml
- with $flash<notice>
%p.notice= $flash<notice>
flash.now makes a message available in the current request only, for a page rendered
without a redirect. keep carries the flash one more request and discard
drops an entry early. add-flash-types registers named types that read and write
through a method, with an is add-flash-types trait form for the class header.
class ApplicationController is MVC::Keayl::Controller
is add-flash-types('success', 'error') { }
# this request only
self.flash.now<alert> = 'Could not save';
# writes flash<success>
self.flash.success('Saved');
#the ApplicationController pattern
Put shared callbacks, rescue handlers, and helpers on a base controller, and subclasses inherit
them. helper-method exposes a controller method to the template, and
assign records a value for it.
class ApplicationController is MVC::Keayl::Controller {
method authenticate is before-action { ... }
method current-user is helper-method { ... }
method site-name
is helper-method { 'keayl.dev' }
}
class UsersController is ApplicationController {
method show {
self.assign('title', 'Profile');
}
}
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.