NAME
Kelp::Manual - Reference to web development with Kelp
SYNOPSIS
First ...
# lib/MyApp.pm
package MyApp;
use parent 'Kelp';
sub build {
my $self = shift;
my $r = $self->routes;
$r->add( "/hello", sub { "Hello, world!" } );
$r->add( '/hello/:name', 'greet' );
}
sub greet {
my ( $self, $name ) = @_;
"Hello, $name!";
}
1;
Then ...
# app.psgi
use lib 'lib';
use MyApp;
my $app = MyApp->new;
$app->run;
Finally ...
> plackup app.psgi
Or, for quick prototyping use Kelp::Less:
# app.psgi
use Kelp::Less;
get '/hello/?name' => sub {
my ( $self, $name ) = @_;
"Hello " . $name // 'world';
};
run;
DESCRIPTION
If you're going to be deploying a Perl based web application, chances are that you will be using Plack. Plack has almost all necessary tools to create and maintain a healthy web app. Tons of middleware is written for it, and there are several very well tested high performance preforking servers, such as Gazelle.
Plack, however, is not a web framework, hence its creators have intentionally omitted adding certain components. This is where Kelp gets to shine. It provides a layer on top of Plack and puts everything together into a complete web framework.
CREATING A NEW WEB APP
Quick development using Kelp::Less
For writing quick experimental web apps and to reduce the boiler plate, one could use Kelp::Less. In this case all of the code can be put in app.psgi
: Look up the POD for Kelp::Less for many examples, but to get you started off, here is a quick one:
# app.psgi
use Kelp::Less;
module 'JSON';
get '/api/:user/?action' => sub {
my ( $self, $user, $action ) = @_;
my $json = {
success => \1,
user => $user,
action => $action // 'ask'
};
return $json;
};
run;
Using the kelp-generator
script
The easiest way to create the directory structure and a general application skeleton is by using the kelp-generator
script, which comes with this package.
> kelp-generator MyApp
This will create lib/MyApp.pm
, app.psgi
and some other files (explained below).
To create a Kelp::Less app file, use:
> kelp-generator --type=less MyApp
Get help by typing:
> kelp-generator --help
Directory structure
Before you begin writing the internals of your app, you need to create the directory structure either by hand, or by using the above described kelp-generator
utility script.
.
|--/lib
| |--MyApp.pm
| |--/MyApp
|
|--/conf
| |--config.pl
| |--test.pl
| |--development.pl
| |--deployment.pl
|
|--/views
|--/log
|--/t
|--app.psgi
- /lib
-
The
lib
folder contains your application modules and any local modules that you want your app to use. - /conf
-
The
conf
folder is where Kelp will look for configuration files. You need one main file, namedconfig.pl
. You can also add other files that define different running environments, if you name them environment.pl
. Replace environment with the actual name of the environment. To change the running environment, you can specify the appmode
, or you can set thePLACK_ENV
environment variable.my $app = MyApp->new( mode => 'development' );
or
> PLACK_ENV=development plackup app.psgi
- /views
-
This is where the
Template
module will look for template files. - /log
-
This is where the
Logger
module will createerror.log
and any other log files that were defined in the configuration. - /t
-
The
t
folder is traditionally used to hold test files. It is up to you to use it or not, although we strongly recommend that you write some automated test units for your web app. - app.psgi
-
This is the PSGI file, of the app, which you will deploy. In it's most basic form it should look like this:
use lib './lib'; use MyApp; my $app = MyApp->new; $app->run;
The application classes
Your application's classes should be put in the lib/
folder. The main class, in our example MyApp.pm
, initializes any modules and variables that your app will use. Here is an example that uses Moose
to create lazy attributes and initialize a database connection:
package MyApp;
use parent Kelp;
use Moose;
has dbh => (
is => 'ro',
isa => 'DBI',
lazy => 1,
default => sub {
my $self = shift;
my @config = @{ $self->config('dbi') };
return DBI->connect(@config);
}
);
sub build {
my $self = shift;
$self->routes->add("/read/:id", "read");
}
sub read {
my ( $self, $id ) = @_;
$self->dbh->selectrow_array(q[
SELECT * FROM problems
WHERE id = ?
], $id);
}
1;
What is happening here?
First, we create a lazy attribute and instruct it to connect to DBI. Notice that we have access to the current app and all of its internals via the
$self
variable. Notice also that the reason we definedbh
as a lazy attribute is thatconfig
will not yet be initialized. All modules are initialized upon the creation of the object instance, e.g. when we callMyApp->new
;Then, we override Kelp's "build" in Kelp subroutine to create a single route
/read/:id
, which is assigned to the subroutineread
in the current class.The
read
subroutine, takes$self
and$id
(the named placeholder from the path), and uses$self->dbh
to retrieve data.
A note about object managers: The above example uses Moose. It is entirely up to you to use Moose, another object manager, or no object manager at all. The above example will be just as successful if you used our own little Kelp::Base:
package MyApp;
use Kelp::Base 'Kelp';
attr dbi => sub {
...
};
1;
FRAMEWORK BASICS
Routing
Kelp uses a powerful and very flexible router. Traditionally, it is also light and consists of less than 400 lines of code (comments included). You are encouraged to read Kelp::Routes, but here are some key points. All examples are assumed to be inside the "build" in Kelp method and $r
is equal to $self->routes
:
Destinations
You can direct HTTP paths to subroutines in your classes or, you can use inline code.
$r->add( "/home", "home" ); # goes to sub home
$r->add( "/legal", "Legal::view" ); # goes to MyApp::Legal::view
$r->add( "/about", sub { "Content for about" }); # inline
Restrict HTTP methods
Make a route only catch a specific HTTP method:
$r->add( [ POST => '/update' ], "update_user" );
Nesting Plack apps
It's easy to have a Plack app nested in Kelp:
$r->add( '/app', {
to => $plack_app->to_app,
psgi => 1,
});
See "PLACK APPS" in Kelp::Routes for details.
Named captures
Using regular expressions is so Perl. Sometimes, however, it gets a little overwhelming. Use named paths if you anticipate that you or someone else will ever want to maintain your code.
Explicit
$r->add( "/update/:id", "update" );
# Later
sub update {
my ( $self, $id ) = @_;
# Do something with $id
}
Optional
$r->add( "/person/?name", sub {
my ( $self, $name ) = @_;
return "I am " . $name // "nobody";
});
This will handle /person
, /person/
and /person/jack
.
Wildcards
$r->add( '/*article/:id', 'Articles::view' );
This will handle /bar/foo/baz/500
and send it to MyApp::Articles::view
with parameters $article
equal to bar/foo/baz
and $id
equal to 500.
Wildcards can also be used without a label:
# FIXME: will match both /actions/create and /actions_and_stuff
$r->add( '/actions*' => sub { ... } );
NOTE: matched contents from an unlabelled wildcard will be discarded if your route also contains named placeholders. Name it to prevent that from happening.
Slurpy
$r->add( '/other-app/>rest' => sub {
my ( $self, $rest ) = @_;
return "other-app called with path: " . ($rest // '<none>');
} );
This is a mix of "Wildcards" and "Optional". It works like optional placeholders but will by default also match slashes.
The use case of this is to have something that hijacks all possibilities under that path, but also matches for that base path, for example the above will match all of these:
/other-app/>rest matches:
/other-app
/other-app/
/other-app/home
/other-app/article/1
/other-app/*rest matches:
/other-app/home
/other-app/article/1
/other-app/?rest matches:
/other-app
/other-app/
/other-app/home
Just like wildcards, slurpy placeholders can be used without a label:
# all user actions and their index in one route
$r->add( '/user/actions/>' => sub { ... } );
NOTE: matched contents from an unlabelled slurpy will be discarded if your route also contains named placeholders. Name it to prevent that from happening.
Placeholder restrictions
Paths' named placeholders can be restricted by providing regular expressions.
$r->add( '/user/:id', {
check => { id => '\d+' },
to => "Users::get"
});
# Matches /user/1000, but not /user/abc
Placeholder defaults
This only applies to optional placeholders, or those prefixed with a question mark. If a default value is provided for any of them, it will be used in case the placeholder value is missing.
$r->add( '/:id/?other', defaults => { other => 'info' } );
# GET /100;
# { id => 100, other => 'info' }
# GET /100/delete;
# { id => 100, other => 'delete' }
Bridges
A bridge is a route that has to return a true value in order for the next route in line to be processed.
$r->add( '/users', { to => 'Users::auth', bridge => 1 } );
$r->add( '/users/:action' => 'Users::dispatch' );
See "BRIDGES" in Kelp::Routes for more information.
Route order
Routes will be executed in order that will usually be the one you want. Bridges will execute before normal routes, and the routes will be sorted by patterns using Perl cmp
. However, you can sometimes run into trouble with their ordering.
In that case, you can use special key order
to sort it out. All routes have default order
of 0
. If you want some of them to execute earlier, reduce their order value. Late routes can be marked with positive order.
Of course, even if you specify order, bridges will still always come before regular routes.
URL building
Each path can be given a name and later a URL can be built using that name and the necessary arguments.
$r->add( "/update/:id", { name => 'update', to => 'User::update' } );
# Later
my $url = $self->route->url('update', id => 1000); # /update/1000
Reading a HTTP request
All input data comes nicely packed inside Kelp::Request, which inherits Plack::Request. It has a coulpe convenience methods and handles charset decoding automatically.
Input data charsets
All request methods showcased below will try to decode request data with either charset from the Content-Type
header (if present and supported by Encode module) or with application charset otherwise.
There are a couple methods starting with raw_
which return encoded data. See "ENCODING" in Kelp::Request for details.
param
and friends
The request class has a couple of param
methods, which allow quick and easy access to request parameters.
sub fetch_params {
my $self = shift;
my $key = 'parameter_name';
# fetch parameters from query form, body form or JSON body
my $json_or_body_or_query = $self->param($key);
my $always_query = $self->res->query_param($key);
my $always_body = $self->res->body_param($key);
my $always_json = $self->res->json_param($key);
}
These param
methods return a single value with a $key
or a list of available keys with no arguments.
parameters
and friends
These methods return a Hash::MultiValue object with parameters:
sub fetch_parameters {
my $self = shift;
# fetch parameters from query form or body form
my $body_or_query = $self->res->parameters($key);
my $always_query = $self->res->query_parameters($key);
my $always_body = $self->res->body_parameters($key);
}
They may be more useful to get a lot of parameters in one go.
content
, raw_body
and json_content
These methods return the body of the request.
content
returns the body properly decoded.
json_content
tries to decode the content
as json and return a Perl structure or undef
on error or if it isn't a json request.
raw_body
is same as content
, but it has the original request encoding.
File uploads
The request object has a uploads|Plack::Request/uploads
property. The uploads property returns a reference to a hash containing all uploads.
sub upload {
my $self = shift;
my $uploads = $self->req->uploads;
# Now $uploads is a hashref to all uploads
...
}
For Kelp::Less, then you can use the req
reserved word:
get '/upload' => sub {
my $uploads = req->uploads;
};
Other request data
See Kelp::Request and Plack::Request to see how to fetch some other data you may find useful.
Building an HTTP response
Kelp contains an elegant module, called Kelp::Response, which extends Plack::Response
with several useful methods. Most methods return $self
after they do the required job. For the sake of the examples below, let's assume that all of the code is located inside a route definition.
Automatic content type
Your routes don't always have to set the response
object. You could just return a simple scalar value or a reference to a hash, array or anything that can be converted to JSON.
# Content-type automatically set to "text/html"
sub text_route {
return "There, there ...";
}
# Content-type automatically set to "application/json"
sub json_route {
return { error => 1, message => "Fail" };
}
Automatic charset encoding
With Kelp, you don't have to worry about the encoding of the response - most of the methods will automatically encode the response into configured application's charset. Text and application content types will by default have charset
part added. To make it all work flawlessly, remember to use utf8;
at the top of your files.
If you'd like to instead take charset into your own hands, you can configure "charset" in Kelp and "request_charset" in Kelp to undefined values. Alternatively, you can use raw_
methods in Kelp::Request and "render_binary" in Kelp::Response and manually set content types and charsets.
Rendering text
# Render simple text
$self->res->text->render("It works!");
Rendering HTML
$self->res->html->render("<h1>It works!</h1>");
Custom content type
$self->res->set_content_type('image/png');
Return 404 or 500 errors
sub some_route {
my $self = shift;
if ($missing) {
return $self->res->render_404;
}
if ($broken) {
return $self->res->render_500;
}
}
Templates
sub hello {
my ( $self, $name ) = @_;
$self->res->template( 'hello.tt', { name => $name } );
}
The above example will render the contents of hello.tt
, and it will set the content-type to text/html
. To set a different content-type, use set_content_type
or any of its aliases:
sub hello_txt {
my ( $self, $name ) = @_;
$self->res->text->template( 'hello_txt.tt', { name => $name } );
}
You can also simply get template string and return it, which will work the same:
sub hello {
my ( $self, $name ) = @_;
# NOTE: it's template method from $self, not from $self->res
return $self->template( 'hello.tt', { name => $name } );
}
Rendering DATA
Kelp templates can easily render from DATA
or other filehandle:
sub hello {
my ( $self, $name ) = @_;
return $self->template( \*DATA, { name => $name } );
}
__DATA__
Hello, [% name %]!
Headers
$self->set_header( "X-Framework", "Kelp" )->render( { success => \1 } );
Serving static files
If you want to serve static pages, you can use the Plack::Middleware::Static middleware that comes with Plack. Here is an example configuration that serves files in your public
folder (under the Kelp root folder) from URLs that begin with /public
:
# conf/config.pl
{
middleware => [qw/Static/],
middleware_init => {
Static => {
path => qr{^/public/},
root => '.',
}
}
};
Delayed responses
To send a delayed response, have your route return a subroutine.
sub delayed {
my $self = shift;
return sub {
my $responder = shift;
$self->res->code(200);
$self->res->text->body("Better late than never.");
$responder->($self->res->finalize);
};
}
See the PSGI pod for more information and examples.
CONFIGURING THE APPLICATION
Adding middleware
Kelp, being Plack-centric, will let you easily add middleware. There are many ways to do this, but we recommend one of the methods described below.
Using the configuration
Adding middleware in your configuration is probably the easiest and best way for you. This way you can load different middleware for each running mode, e.g. Debug
in development only. All middleware loaded this way is global for your application.
Add middleware names to the middleware
array in your configuration file and the corresponding initializing arguments in the middleware_init
hash:
# conf/development.pl
{
middleware => [qw/Session Debug/],
middleware_init => {
Session => { store => 'File' }
}
}
The middleware will be added in the order you specify in the middleware
array.
Middleware in routes
You can use Kelp's powerful router to find more middleware for your application. This is done with psgi_middleware
field when adding a route:
use Plack::Builder;
$r->add('/checksummed' => {
to => 'get_content',
psgi_middleware => builder {
enable 'ContentMD5';
Kelp->NEXT_APP;
},
});
Now exact path /checksummed
(and no other path) will have that PSGI middleware assigned to it. You need to wrap special "NEXT_APP" in Kelp for this to work.
See "PLACK MIDDLEWARES" in Kelp::Routes for details.
By subclassing Kelp::Middleware
Kelp::Middleware is a class which handles wrapping application in middleware based on config. Subclassing it may be the most powerful way to add more middleware if default configuration is not enough.
# lib/MyApp.pm
attr middleware_obj => 'MyMiddleware';
# lib/MyMiddleware.pm
package MyMiddleware;
use Kelp::Base 'Kelp::Middleware';
sub wrap {
my $self = shift;
my $app = $self->SUPER::wrap(@_);
$app = Plack::Middleware::ContentLength->wrap($app);
return $app;
}
This lets you add middleware before or after config middleware. You can also come up with your own creative ways to use config for declaring middleware.
In app.psgi
This is the same as adding middleware to vanilla Plack.
# app.psgi
use MyApp;
use Plack::Builder;
my $app = MyApp->new();
builder {
enable "Plack::Middleware::ContentLength";
$app->run;
};
Pluggable modules
How to load modules using the config
Kelp can be extended using custom modules. There are two modules that are always loaded by each application instance. Those are Config
and Routes
. The reason behind this is that each and every application always needs a router and configuration. All other modules must be loaded either using the "load_module" in Kelp method, or using the modules
key in the configuration. The default configuration already loads these modules: Template
, Logger
and JSON
. Your configuration can remove some and/or add others. The configuration key modules_init
may contain hashes with initialization arguments. See Kelp::Module for configuration examples.
Encoder modules
Some encoder modules like Kelp::Module::JSON can register themselves as encoders for the application, letting you have encoder factories:
$self->get_encoder($type, $name);
my $json_1 = $self->get_encoder('json'); # the default encoder
my $json_2 = $self->get_encoder(json => 'another_one'); # a completely new encoder
Encoders of given type and name can be configured to have different options. See "get_encoder" in Kelp for details.
Inspecting loaded modules
All modules loaded can be inspected by taking a peek at "loaded_modules" in Kelp, which is an array of built module objects. While usually there's no need to do that, it may come in handy when debugging.
Creating your own module
Each new module must be a subclass of the Kelp::Module
class. Modules' job is to initialize and register new methods into the web application class. Here is a module which initializes an extremely simple cache:
package Kelp::Module::ExampleCache;
use Kelp::Base 'Kelp::Module';
sub build {
my ($self, %args) = @_;
my $case_sensitive = !$args{case_insensitive};
my %cache;
$self->register(cache_set => sub {
my ($app, $key, $value) = @_;
$cache{$case_sensitive ? $key : lc $key} = $value;
});
$self->register(cache_get => sub {
my ($app, $key) = @_;
return $cache{$case_sensitive ? $key : lc $key};
});
}
1;
What is happening here?
First we create a class
Kelp::Module::ExampleCache
which inheritsKelp::Module
.Then, we override the
build
method (ofKelp::Module
), we fetch thecase_insensitive
value from the configuration (configured undermodules_init.ExampleCache.case_insensitive
).Last step is to initialize the hash which will store the keys and register two methods into the web application via the
register
method. They will be ready to use in the main application class.
See more examples and POD at Kelp::Module.
NEXT STEPS
Debugging
Kelp's configuration and building process can be easily debugged by setting KELP_DEBUG
environmental variable. These debug messages all go to STDOUT
.
If you set it to
modules
, Kelp will print a message on every module load and every middleware load.If you set it to
config
, Kelp will print its full configuration.If you set it to
routes
, Kelp router will print every route which is being added to the system.You can also set it to
1
orall
, which will print all of the above plus some messages notifying the current state application building process.
Testing
Kelp provides a test class called Kelp::Test
. It is object oriented, and all methods return the Kelp::Test
object, so they can be chained together. Testing is done by sending HTTP requests to an already built application and analyzing the response. Therefore, each test usually begins with the "request" in Kelp::Test method, which takes a single HTTP::Request parameter. It sends the request to the web app and saves the response as an HTTP::Response object.
# file t/test.t
use MyApp;
use Kelp::Test;
use Test::More;
use HTTP::Request::Common;
my $app = MyApp->new( mode => 'test' );
my $t = Kelp::Test->new( app => $app );
$t->request( GET '/path' )
->code_is(200)
->content_is("It works");
$t->request( POST '/api' )
->json_cmp({auth => 1});
done_testing;
What is happening here?
First, we create an instance of the web application class, which we have previously built and placed in the
lib/
folder. We set the mode of the app totest
, so that fileconf/test.pl
overrides the main configuration. The test configuration can contain anything you see fit. Perhaps you want to disable certain modules, or maybe you want to make DBI connect to a different database.Second, we create an instance of the
Kelp::Test
class and tell it that it will perform all tests using our$app
instance.At this point we are ready to send requests to the app via the request method. It takes only one argument, an HTTP::Request object. It is very convenient to use the HTTP::Request::Common module here, because you can create common requests using abridged syntax, i.e.
GET
,POST
, etc. The line$t->request( GET '/path' )
first creates a HTTP::Request GET object, and then passes it to therequest
method.After we send the request, we can test the response using any of the
Test::
modules, or via the methods provided by Kelp::Test. In the above example, we test if we got a code 200 back from/path
and if the returned content wasIt works
.
Run the rest as usual, using prove
:
> prove -l t/test.t
Take a look at the Kelp::Test for details and more examples.
Future compatibility
Versions 2.00
and 2.10
of Kelp introduced some breaking changes, especially when it comes to how requests are decoded and how the errors are rendered. If you are affected and don't want to modify your code, you are welcome to use a fixed version 2.00
(with less incompatibilities) or 1.07
(without incompatibilities). You will be missing on a lot of improvements though - most of these changes were bugfixes or security fixes.
Kelp values backward compatibility, but at the same time it will not be hesitant to fix bugs, security issues or major inconveniences where it sees necessary. From 2.10
forward, non-bugfix breaking changes will only be introduced after a 6-month deprecation period. Changelog will list them under [Deprecations]
section.
Other documentation
You may want to take a look at our Kelp::Manual::Cookbook for common problems and solutions.
Details of controllers can be found in Kelp::Manual::Controllers.
Specific packages contain documentation about the interface of each part of the system.
SUPPORT
Mailing list: https://groups.google.com/g/perl-kelp
AUTHOR
Stefan Geneshky - minimal <at> cpan.org
Currently maintained by Bartosz Jarzyna - bbrtj.pro <at> gmail.com
CONTRIBUTORS
In no particular order:
Julio Fraire
Maurice Aubrey
David Steinbrunner
Gurunandan Bhat
Perlover
Ruslan Zakirov
Christian Froemmel (senfomat)
Ivan Baidakou (basiliscos)
roy-tate
Konstantin Yakunin (@yakunink)
Benjamin Hengst (notbenh)
Nikolay Mishin (@mishin)
Bartosz Jarzyna (bbrtj)
LICENSE
This module and all the modules in this package are governed by the same license as Perl itself.