The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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 ...

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

Custom 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

For one-off rendering of errors, you can alternatively set the response headers and content within the route:

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

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 logging

Default log format can be modified by configuring the Logger module. See "date_format" in Kelp::Module::Logger and "log_format" in Kelp::Module::Logger. Alternatively, Log::Dispatch can be configured with its own callback to format the message to be logged.

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;
    }

Controller fields disappearing after each request

See "Main application object is shallow-cloned before rebless" in Kelp::Manual::Controllers.

SEE ALSO

Kelp::Manual

Kelp

Plack

SUPPORT