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.