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

A large multipart part is streamed to a temp file rather than held in memory - see Uploads and files for upload_dir, content-addressed storage and scanning.

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.

Sending files

$c->send_file is the whole download story in one call:

get '/invoice/:id' => sub {
    my ($c) = @_;
    return $c->send_file('/var/store/' . $c->param('id') . '.pdf',
        filename => 'invoice.pdf');
};

# bytes already in memory (a generated document)
return $c->send_file(\$pdf_bytes, type => 'application/pdf');

The source is a file path or a reference to a scalar of bytes, and the result is a finished response like any other. Conditional requests are answered for you - a strong ETag (from mtime and size) and Last-Modified, with 304s for If-None-Match / If-Modified-Since - as are byte Range requests (206 with Content-Range, 416 when unsatisfiable, If-Range honoured) and HEAD. Ranged file bodies stream through Punk::SendFile::Reader, so no more than 64KB of the file is in memory at once.

Options: type (otherwise inferred from the extension), filename (attachment disposition, RFC 5987-encoded when not ASCII), inline, ranges => 0, mtime / etag overrides, cache_control (see SEO and HTTP caching), and missing => 'not_found' to answer the house 404 for an unreadable path instead of croaking. The path is served as given - if any part of it came from the request, the traversal guard is yours.

Content negotiation

$c->respond_to picks a handler by the request's Accept header:

return $c->respond_to(
    json => sub { $_[0]->json({ book => $book }) },
    html => sub { $_[0]->render('book/view', { book => $book }) },
    any  => sub { $_[0]->text('book', 200) },
);

Formats are json, html, text, xml or any full media type; q-values order the choice and q=0 excludes. A client with no preference gets the format its own Content-Type names when offered, else the first registered; when nothing fits, any runs if given, otherwise the response is a 406. Every outcome carries Vary: Accept.

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.