Controllers and the context

A controller method is a plain sub receiving one argument: the Punk::Context. No instance, no dispatch overhead - the coderef was resolved once at to_app and is called directly per request.

package MyApp::Controller::Web::Book;
use parent 'Punk::Controller';

sub view {
    my ($c) = @_;
    my $book = $c->model('Book')->get(id => $c->param('id'));
    return $c->render('book/view', { book => $book });
}

1;

The context is a blessed array with a fixed slot layout, implemented in C; everything below is an XSUB reading those slots directly.

Parameters

$c->param($name) resolves through layers, most specific first: validated OpenAPI parameters (path, then query), then route captures, then the request's query string, then the form body.

$c->params takes the same layers several names at a time. With no names it is everything merged, stacked in that precedence. With names it returns a list of values in the order asked for - undef for a name no layer has - or, in scalar context, a hashref of just the names that were there, so a set of optional filters is one call:

my ($page, $size) = $c->params(qw(page size));
my %filter = %{ $c->params(qw(state queue task)) };

A repeated parameter (?tag=a&tag=b) is an arrayref.

The request

$c->req is the lazy Punk::Request - nothing is parsed until asked for, and everything parsed is cached:

$c->req->method;              # GET
$c->req->path;                # /books/42
$c->req->header('User-Agent');
$c->req->headers;             # every header, lowercase-dashed keys
$c->req->query;               # parsed query hashref
$c->req->form;                # parsed body hashref
$c->req->json;                # the body, JSON-decoded
$c->req->body;                # the raw bytes
$c->req->cookie('sid');
$c->req->upload('avatar');    # a Punk::Upload from multipart

Responding

$c->json({ ok => 1 });            # application/json
$c->text('plain');                # text/plain
$c->html('<p>markup</p>');        # text/html
$c->render('book/view', \%data);  # through the view engine
$c->redirect('/login');           # 302, or pass a status
$c->not_found;                    # the 404 response

Responses can be shaped before they are sent:

$c->status(201);
$c->header('X-Request-Id' => $id);
$c->cookie(theme => 'dark', expires => '30d');

A handler may also return plain data - it is JSON-encoded with the pending status and headers folded in - or a future that resolves to any of the above; see Async.

The stash

$c->stash is a per-request hashref for passing values between guards, hooks and the controller:

# in a guard
$c->stash->{user} = $user;

# in the handler
my $user = $c->stash->{user};

Outbound HTTP

$c->ua is a Fetch agent - HTTP/2, future-based, one per worker so its keep-alive pool survives between requests. Named agents carry their own options:

ua partner => { timeout => 2 };          # in the app

my $res = $c->await($c->ua('partner')->get('https://api.example.com/'));

Helpers and models

helper installs new context methods (usually from plugins), and $c->model($name) returns the registered model instance - see Models.