=pod

=encoding utf8

=head1 NAME

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

=head1 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 I<not getting in your way>) many of these
will be just a proposed solutions, not an official way of solving a problem.

=head1 RECIPES

=head2 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,
L<Template::Toolkit> allows for C<WRAPPER> directive, which can be used like
this (with L<Kelp::Module::Template::Toolkit>):

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

=head2 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 L<DBIx::Class>.

=head2 Custom 404 and 500 error pages

=head3 Error templates

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

=head3 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');
    }

=head3 By overriding the Kelp::Response class

To make custom 500, 404 and other error pages, you will have to subclass the
L<Kelp::Response> module and override the I<render_404> and I<render_500> subroutines.
Let's say your app's name is Foo and its class is in I<lib/Foo.pm>. Now create a
file I<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 I<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 I<views/my_custom_404.tt> and
I<views/my_custom_500.tt>. You can add other error rendering subroutines too, for
example:

    sub render_401 {
        # Render your custom 401 error here
    }

=head2 Altering the behavior of a Kelp class method

The easiest solution would be to use L<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);
    };

=head2 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
I<streaming>, I<io> and I<nonblocking>, like L<Twiggy>. Then, you could
integrate Kelp application with a websocket application via
L<Kelp::Module::Websocket::AnyEvent> CPAN module (if the server implementation
is compatible with L<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 L<Mojolicious> instead or integrating a
L<Mojo::Server::Hypnotoad> with a small Mojo application alongside Kelp as a
websocket handler.

=head2 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 I<apache2> and
I<nginx>. To get full information about incoming requests, you'll also need to
use L<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).

=head2 Changing the default access logging

Access logs reported by Kelp through C<logger> can be modified or disabled by
writing your own customized L<Kelp/before_dispatch> method (not calling the
parent version).

    sub before_dispatch {} # enough to disable the access logs

=head2 Using sessions

In order to have access to L<Kelp::Request/session> a
L<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 C<store> right away, as otherwise it
will store data in memory, which is both volatile and does not work with
multi-process servers.

=head2 Responding in the same charset as request

Kelp usually uses its own L<Kelp/charset> 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 C<Content-Type> of the
request is either C<text/*> or C<application/*>.

=head2 Custom encodings in requests and responses

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

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

=head3 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);
        }
    }

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

=head1 SEE ALSO

L<Kelp::Manual>

L<Kelp>

L<Plack>

=head1 SUPPORT

=over

=item * GitHub: L<https://github.com/Kelp-framework/Kelp>

=item * Mailing list: L<https://groups.google.com/g/perl-kelp>

=back

