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

    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.

SEE ALSO

Kelp::Manual

Kelp

Plack

SUPPORT