#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');
Unpermitted keys are dropped. A malformed payload, a scalar where a hash is expected, makes 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.