=pod
=head1 NAME
Kelp::Manual - Reference to web development with Kelp
=head1 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 L<Kelp::Less>:
# app.psgi
get '/hello/?name' => sub {
my ( $self, $name ) = @_;
"Hello " . $name // 'world';
};
run;
=head1 DESCRIPTION
If you're going to be deploying a Perl based web application, chances are that
you will be using L<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 L<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.
=head1 CREATING A NEW WEB APP
=head2 Quick development using Kelp::Less
For writing quick experimental web apps and to reduce the boiler plate, one
could use L<Kelp::Less>. In this case all of the code can be put in C<app.psgi>:
Look up the POD for L<Kelp::Less> for many examples, but to get you started off,
here is a quick one:
# app.psgi
module 'JSON';
get '/api/:user/?action' => sub {
my ( $self, $user, $action ) = @_;
my $json = {
success => \1,
user => $user,
action => $action // 'ask'
};
return $json;
};
run;
=head2 Using the C<kelp-generator> script
The easiest way to create the directory structure and a general application
skeleton is by using the C<kelp-generator> script, which comes with this package.
> kelp-generator MyApp
This will create C<lib/MyApp.pm>, C<app.psgi> and some other files (explained
below).
To create a L<Kelp::Less> app file, use:
> kelp-generator --type=less MyApp
Get help by typing:
> kelp-generator --help
=head2 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 C<kelp-generator>
utility script.
.
|--/lib
| |--MyApp.pm
| |--/MyApp
|
|--/conf
| |--config.pl
| |--test.pl
| |--development.pl
| |--deployment.pl
|
|--/views
|--/log
|--/t
|--app.psgi
=over
=item B</lib>
The C<lib> folder contains your application modules and any local modules
that you want your app to use.
=item B</conf>
The C<conf> folder is where Kelp will look for configuration files. You need one
main file, named C<config.pl>. You can also add other files that define different
running environments, if you name them I<environment>C<.pl>. Replace
I<environment> with the actual name of the environment.
To change the running environment, you can specify the app C<mode>, or you can
set the C<PLACK_ENV> environment variable.
my $app = MyApp->new( mode => 'development' );
or
> PLACK_ENV=development plackup app.psgi
=item B</views>
This is where the C<Template> module will look for template files.
=item B</log>
This is where the C<Logger> module will create C<error.log> and
any other log files that were defined in the configuration.
=item B</t>
The C<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.
=item B<app.psgi>
This is the L<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;
=back
=head2 The application classes
Your application's classes should be put in the C<lib/> folder. The main class,
in our example C<MyApp.pm>, initializes any modules and variables that your
app will use. Here is an example that uses C<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?
=over
=item
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 C<$self>
variable. Notice also that the reason we define C<dbh> as a I<lazy> attribute
is that C<config> will not yet be initialized. All modules are initialized upon
the creation of the object instance, e.g. when we call C<MyApp-E<gt>new>;
=item
Then, we override Kelp's L<Kelp/build> subroutine to create a single route
C</read/:id>, which is assigned to the subroutine C<read> in the current class.
=item
The C<read> subroutine, takes C<$self> and C<$id> (the named placeholder from the
path), and uses C<$self-E<gt>dbh> to retrieve data.
=back
I<A note about object managers:> The above example uses L<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
L<Kelp::Base>:
package MyApp;
use Kelp::Base 'Kelp';
attr dbi => sub {
...
};
1;
=head1 FRAMEWORK BASICS
=head2 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 L<Kelp::Routes>, but here are some key points. All examples
are assumed to be inside the L<Kelp/build> method and C<$r> is equal to
C<$self-E<gt>routes>:
=head3 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
=head3 Restrict HTTP methods
Make a route only catch a specific HTTP method:
$r->add( [ POST => '/update' ], "update_user" );
=head3 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 L<Kelp::Routes/PLACK APPS> for details.
=head3 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.
=head4 Explicit
$r->add( "/update/:id", "update" );
# Later
sub update {
my ( $self, $id ) = @_;
# Do something with $id
}
=head4 Optional
$r->add( "/person/?name", sub {
my ( $self, $name ) = @_;
return "I am " . $name // "nobody";
});
This will handle C</person>, C</person/> and C</person/jack>.
=head4 Wildcards
$r->add( '/*article/:id', 'Articles::view' );
This will handle C</bar/foo/baz/500> and send it to C<MyApp::Articles::view>
with parameters C<$article> equal to C<bar/foo/baz> and C<$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 B<discarded> if your
route also contains named placeholders. Name it to prevent that from happening.
=head4 Slurpy
$r->add( '/other-app/>rest' => sub {
my ( $self, $rest ) = @_;
return "other-app called with path: " . ($rest // '<none>');
} );
This is a mix of L</Wildcards> and L</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 B<discarded> if your
route also contains named placeholders. Name it to prevent that from happening.
=head3 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
=head3 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' }
=head3 Bridges
A I<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 L<Kelp::Routes/BRIDGES> for more information.
=head3 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 C<cmp>. However, you can sometimes run into trouble with their ordering.
In that case, you can use special key C<order> to sort it out. All routes have
default C<order> of C<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.
=head3 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
=head2 Reading a HTTP request
All input data comes nicely packed inside L<Kelp::Request>, which inherits
Plack::Request. It has a coulpe convenience methods and handles charset
decoding automatically.
=head3 Input data charsets
All request methods showcased below will try to decode request data with either
charset from the C<Content-Type> header (if present and supported by L<Encode>
module) or with application charset otherwise.
There are a couple methods starting with C<raw_> which return encoded data. See
L<Kelp::Request/ENCODING> for details.
=head3 C<param> and friends
The request class has a couple of C<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 C<param> methods return a single value with a C<$key> or a list of
available keys with no arguments.
=head3 C<parameters> and friends
These methods return a L<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.
=head3 C<content>, C<raw_body> and C<json_content>
These methods return the body of the request.
C<content> returns the body properly decoded.
C<json_content> tries to decode the C<content> as json and return a Perl
structure or C<undef> on error or if it isn't a json request.
C<raw_body> is same as C<content>, but it has the original request encoding.
=head3 File uploads
The request object has a C<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 L<Kelp::Less>, then you can use the C<req> reserved word:
get '/upload' => sub {
my $uploads = req->uploads;
};
=head3 Other request data
See L<Kelp::Request> and L<Plack::Request> to see how to fetch some other data
you may find useful.
=head2 Building an HTTP response
Kelp contains an elegant module, called L<Kelp::Response>, which extends
C<Plack::Response> with several useful methods. Most methods return C<$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.
=head3 Automatic content type
Your routes don't always have to set the C<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" };
}
=head3 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
C<charset> part added. To make it all work flawlessly, remember to C<use utf8;>
at the top of your files.
If you'd like to instead take charset into your own hands, you can configure
L<Kelp/charset> and L<Kelp/request_charset> to undefined values. Alternatively,
you can use C<raw_> methods in L<Kelp::Request> and
L<Kelp::Response/render_binary> and manually set content types and charsets.
=head3 Rendering text
# Render simple text
$self->res->text->render("It works!");
=head3 Rendering HTML
$self->res->html->render("<h1>It works!</h1>");
=head3 Custom content type
$self->res->set_content_type('image/png');
=head3 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;
}
}
=head3 Templates
sub hello {
my ( $self, $name ) = @_;
$self->res->template( 'hello.tt', { name => $name } );
}
The above example will render the contents of C<hello.tt>, and it will set the
content-type to C<text/html>. To set a different content-type, use
C<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 } );
}
=head3 Rendering DATA
Kelp templates can easily render from C<DATA> or other filehandle:
sub hello {
my ( $self, $name ) = @_;
return $self->template( \*DATA, { name => $name } );
}
__DATA__
Hello, [% name %]!
=head3 Headers
$self->set_header( "X-Framework", "Kelp" )->render( { success => \1 } );
=head3 Serving static files
If you want to serve static pages, you can use the L<Plack::Middleware::Static>
middleware that comes with Plack. Here is an example configuration that serves
files in your C<public> folder (under the Kelp root folder) from URLs that
begin with C</public>:
# conf/config.pl
{
middleware => [qw/Static/],
middleware_init => {
Static => {
path => qr{^/public/},
root => '.',
}
}
};
=head3 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 L<PSGI|PSGI/Delayed-Response-and-Streaming-Body> pod for more
information and examples.
=head1 CONFIGURING THE APPLICATION
=head2 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.
=head3 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.
C<Debug> in development only. All middleware loaded this way is global for your
application.
Add middleware names to the C<middleware> array in your configuration file and
the corresponding initializing arguments in the C<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 C<middleware>
array.
=head3 Middleware in routes
You can use Kelp's powerful router to find more middleware for your
application. This is done with C<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 C</checksummed> (and no other path) will have that PSGI
middleware assigned to it. You need to wrap special L<Kelp/NEXT_APP> for this
to work.
See L<Kelp::Routes/PLACK MIDDLEWARES> for details.
=head3 By subclassing L<Kelp::Middleware>
L<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.
=head3 In C<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;
};
=head2 Pluggable modules
=head3 How to load modules using the config
Kelp can be extended using custom I<modules>. There are two modules that are
B<always> loaded by each application instance. Those are C<Config> and
C<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 L<Kelp/load_module> method, or using the C<modules> key in the
configuration. The default configuration already loads these modules:
C<Template>, C<Logger> and C<JSON>. Your configuration can remove some and/or
add others. The configuration key C<modules_init> may contain hashes with
initialization arguments. See L<Kelp::Module> for configuration examples.
=head3 Encoder modules
Some encoder modules like L<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 L<Kelp/get_encoder> for details.
=head3 Inspecting loaded modules
All modules loaded can be inspected by taking a peek at L<Kelp/loaded_modules>,
which is an array of built module objects. While usually there's no need to do
that, it may come in handy when debugging.
=head3 Creating your own module
Each new module must be a subclass of the C<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?
=over
=item
First we create a class C<Kelp::Module::ExampleCache> which inherits C<Kelp::Module>.
=item
Then, we override the C<build> method (of C<Kelp::Module>), we fetch the
C<case_insensitive> value from the configuration (configured under
C<modules_init.ExampleCache.case_insensitive>).
=item
Last step is to initialize the hash which will store the keys and register two
methods into the web application via the C<register> method. They will be ready
to use in the main application class.
=back
See more examples and POD at L<Kelp::Module>.
=head1 NEXT STEPS
=head2 Debugging
Kelp's configuration and building process can be easily debugged by setting
C<KELP_DEBUG> environmental variable. These debug messages all go to C<STDOUT>.
=over
=item
If you set it to C<modules>, Kelp will print a message on every module load and every
middleware load.
=item
If you set it to C<config>, Kelp will print its full configuration.
=item
If you set it to C<routes>, Kelp router will print every route which is being added to the system.
=item
You can also set it to C<1> or C<all>, which will print all of the above plus some
messages notifying the current state application building process.
=back
=head2 Testing
Kelp provides a test class called C<Kelp::Test>. It is object oriented, and all
methods return the C<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
L<Kelp::Test/request> method, which takes a single L<HTTP::Request> parameter.
It sends the request to the web app and saves the response as an
L<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?
=over
=item
First, we create an instance of the web application class, which we have
previously built and placed in the C<lib/> folder. We set the mode of the app to
C<test>, so that file C<conf/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.
=item
Second, we create an instance of the C<Kelp::Test> class and tell it that it
will perform all tests using our C<$app> instance.
=item
At this point we are ready to send requests to the app via the
L<request|Kelp::Test/request> method. It takes only one argument, an
HTTP::Request object. It is very convenient to use the L<HTTP::Request::Common>
module here, because you can create common requests using abridged syntax,
i.e. C<GET>, C<POST>, etc. The line C<$t-E<gt>request( GET '/path' )> first
creates a HTTP::Request GET object, and then passes it to the C<request> method.
=item
After we send the request, we can test the response using any of the C<Test::>
modules, or via the methods provided by L<Kelp::Test>.
In the above example, we test if we got a code 200 back from C</path> and if the
returned content was C<It works>.
=back
Run the rest as usual, using C<prove>:
> prove -l t/test.t
Take a look at the L<Kelp::Test> for details and more examples.
=head2 Future compatibility
Versions C<2.00> and C<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 C<2.00> (with less incompatibilities) or C<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 C<2.10> forward, non-bugfix breaking changes will only be
introduced after a 6-month deprecation period. Changelog will list
them under C<[Deprecations]> section.
=head2 Other documentation
You may want to take a look at our L<Kelp::Manual::Cookbook> for common
problems and solutions.
Details of controllers can be found in L<Kelp::Manual::Controllers>.
Specific packages contain documentation about the interface of each part of the
system.
=head1 SUPPORT
=over
=item * Mailing list: L<https://groups.google.com/g/perl-kelp>
=back
=head1 AUTHOR
Stefan Geneshky - minimal <at> cpan.org
Currently maintained by Bartosz Jarzyna - bbrtj.pro <at> gmail.com
=head1 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)
=head1 LICENSE
This module and all the modules in this package are governed by the same license
as Perl itself.