NAME

Kelp::Manual::Cookbook - Recipes for Kelp dishes

DESCRIPTION

This document lists solutions to common problems you may encounter while developing your own Kelp web application. Since Kelp leaves a lot for you to figure out yourself (also known as not getting in your way) many of these will be just a proposed solutions, not an official way of solving a problem.

RECIPES

Setting up a common layout for all templates

Kelp does not implement template layouts by itself, so it's up to templating engine or contributed module to deliver that behavior. For example, Template::Toolkit allows for WRAPPER directive, which can be used like this (with Kelp::Module::Template::Toolkit):

# in config
modules => [qw(Template::Toolkit)],
modules_init => {
    'Template::Toolkit' => {
        WRAPPER => 'layouts/main.tt',
    },
},

Connecting to DBI

There are multiple ways to do it, like the one below:

# Private attribute holding DBI handle
# anonymous sub is a default value builder
attr _dbh => sub {
    shift->_dbi_connect;
};

# Private sub to connect to DBI
sub _dbi_connect {
    my $self = shift;

    my @config = @{ $self->config('dbi') };
    return DBI->connect(@config);
}

# Public method to use when you need dbh
sub dbh {
    my $self = shift;

    # ping is likely not required, but just in case...
    if (!$self->_dbh->ping) {
        # reload the dbh, since ping failed
        $self->_dbh($self->_dbi_connect);
    }

    $self->_dbh;
}

# Use $self->dbh from here on ...

sub some_route {
    my $self = shift;

    $self->dbh->selectrow_array(q[
        SELECT * FROM users
        WHERE clue > 0
    ]);
}

A slightly shorter version with state variables and no ping:

# Public method to use when you need dbh
sub dbh {
    my ($self, $reconnect) = @_;

    state $handle;
    if (!defined $handle || $reconnect) {
        my @config = @{ $self->config('dbi') };
        $handle = DBI->connect(@config);
    }

    return $handle;
}

# Use $self->dbh from here on ...

sub some_route {
    my $self = shift;

    $self->dbh->selectrow_array(q[
        SELECT * FROM users
        WHERE clue > 0
    ]);
}

Same methods can be used for accessing the schema of DBIx::Class.

Custom 404 and 500 error pages

Error templates

The easiest way to set up custom error pages is to create templates in views/error/ with the code of the error. For example: views/error/404.tt and views/error/500.tt. You can render those manually using $self->res->render_404 and $self->res->render_500. To render another error code, you can use $self->res->render_error.

Within the route

You can set the response headers and content within the route:

sub some_route {
    my $self = shift;
    $self->res->set_code(404)->template('my_404_template');
}

By overriding the Kelp::Response class

To make custom 500, 404 and other error pages, you will have to subclass the Kelp::Response module and override the render_404 and render_500 subroutines. Let's say your app's name is Foo and its class is in lib/Foo.pm. Now create a file lib/Foo/Response.pm:

package Foo::Response;
use Kelp::Base 'Kelp::Response';

sub render_404 {
    my $self = shift;
    $self->template('my_custom_404');
}

sub render_500 {
    my $self = shift;
    $self->template('my_custom_500');
}

Then, in lib/Foo.pm, you have to tell Kelp to use your custom response class like this:

sub response {
    my $self = shift;
    return Foo::Response->new( app => $self );
}

Don't forget you need to create views/my_custom_404.tt and views/my_custom_500.tt. You can add other error rendering subroutines too, for example:

sub render_401 {
    # Render your custom 401 error here
}

Altering the behavior of a Kelp class method

The easiest solution would be to use KelpX::Hooks module available on CPAN:

use KelpX::Hooks;
use parent "Kelp";

# Change how template rendering function is called
hook "template" => sub {
    my ($orig, $self, @args) = @_;

    # $args[0] is template name
    # $args[1] is a list of template variables
    $args[1] = {
        (defined $args[1] ? %{$args[1]} : ()),
        "my_var" => $self->do_something,
    };

    # call the original $self->template again
    # with modified arguments
    return $self->$orig(@args);
};

Handling websocket connections

Since Kelp is a Plack-based project, its support for websockets is very limited. First of all, you would need a Plack server with support for the psgi streaming, io and nonblocking, like Twiggy. Then, you could integrate Kelp application with a websocket application via Kelp::Module::Websocket::AnyEvent CPAN module (if the server implementation is compatible with AnyEvent):

sub build {
    my ($self) = @_;

    my $ws = $self->websocket;
    $ws->add(message => sub {
        my ($conn, $msg) = @_;

        $conn->send({echo => $msg});
    });

    $self->symbiosis->mount("/ws" => $ws);
}

Keep in mind that Plack websockets are a burden because of lack of preforking server implementations capable of running them. If you want to use them heavily you're better off using Mojolicious instead or integrating a Mojo::Server::Hypnotoad with a small Mojo application alongside Kelp as a websocket handler.

Deploying

Deploying a Kelp application is done the same way any other Plack application is deployed:

> plackup -E deployment -s Gazelle app.psgi

In production environments, it is usually a good idea to set up a proxy between the PSGI server and the World Wide Web. Popular choices are apache2 and nginx. To get full information about incoming requests, you'll also need to use Plack::Middleware::ReverseProxy.

# app.psgi

builder {
    enable_if { ! $_[0]->{REMOTE_ADDR} || $_[0]->{REMOTE_ADDR} =~ /127\.0\.0\.1/ }
    "Plack::Middleware::ReverseProxy";
    $app->run;
};

(REMOTE_ADDR is not set at all when using the proxy via filesocket).

Changing the default access logging

Access logs reported by Kelp through logger can be modified or disabled by writing your own customized "before_dispatch" in Kelp method (not calling the parent version).

sub before_dispatch {} # enough to disable the access logs

Using sessions

In order to have access to "session" in Kelp::Request a Plack::Middleware::Session middleware must be initialized. In your config file:

middleware => ['Session'],
middleware_init => {
    Session => {
        store => 'File'
    }
}

Note that you pretty much need to choose a store right away, as otherwise it will store data in memory, which is both volatile and does not work with multi-process servers.

Responding in the same charset as request

Kelp usually uses its own "charset" in Kelp as response encoding, but makes it easy to use the same charset in response as the one you got in request:

use utf8;

$self->add_route('/copy_charset' => sub {
    my $self = shift;

    $self->res->charset($self->req->charset);
    return 'et voilà!';
});

Note that request charset is only actually used if the Content-Type of the request is either text/* or application/*.

Custom encodings in requests and responses

It is trivial to extend Kelp::Request and Kelp::Response to make it seamlessly handle other serialization schemes, for example YAML (through Kelp::Module::YAML):

(Note that there is kelp_extensions flag in "CONFIGURATION" in Kelp::Module::YAML, which will install this logic automatically right into base Kelp packages, so this is just an example for custom encodings)

Extending Request

package YAML::Request;
use Kelp::Base 'Kelp::Request';
use Try::Tiny;

sub is_yaml {
    my $self = shift;
    return 0 unless $self->content_type;
    return $self->content_type =~ m{^text/yaml}i;
}

sub yaml_content {
    my $self = shift;
    return undef unless $self->is_yaml;

    return try {
        $self->app->get_encoder(yaml => 'internal')->decode($self->content);
    }
    catch {
        undef;
    };
}

Extending Response

package YAML::Response;
use Kelp::Base 'Kelp::Response';

sub yaml {
    my $self = shift;
    $self->set_content_type('text/yaml', $self->charset || $self->app->charset);
    return $self;
}

sub _render_ref {
    my ($self, $body) = @_;

    if ($self->content_type =~ m{^text/yaml}i) {
        return $self->app->get_encoder(yaml => 'internal')->encode($body);
    }
    else {
        return $self->SUPER::_render_ref($body);
    }
}

Using it in an app

use Kelp::Exception;

sub build {
    my $self = shift;

    $self->load_module('YAML');
    $self->request_obj('YAML::Request');
    $self->response_obj('YAML::Response');

    $self->add_route('/yaml' => 'handler');
}

sub handler {
    my $self = shift;
    my $yaml_document = $self->req->yaml_content;

    Kelp::Exception->throw(400)
        unless defined $yaml_document;

    # ... do something with $yaml_document

    $self->res->yaml;
    return $yaml_document;
}

SEE ALSO

Kelp::Manual

Kelp

Plack

SUPPORT