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::Controllers - Making your app use controllers

DESCRIPTION

This document describes the technical aspect of implementing controllers in your app. By default, Kelp has no controllers - it resolves all your routes in the context of the main app. In other words, all routes take a instance of the web application as a first parameter - even if those routes live in another class.

Controllers lets you separate some of the route handling logic to other classes and have your subs take the object of the correct class as the first argument. In Kelp, there is no special base class for controllers by default - all controllers must be subclasses of Kelp, unless you want to develop your own base controller - see "Use different method than reblessing".

Reblessing details

Reblessing will happen after request is matched to a route. Route handler has to be specified as class and method string, and class must be a subclass of class configured for "base" in Kelp::Routes. "rebless" in Kelp::Routes must also be enabled for that to occur.

The default value of base field is the application class, so your application class is by default your main controller class. All other controllers must (directly or indirectly) inherit from your application class.

These methods will be automatically run on your controller object for each request:

  • route handler method

  • before_dispatch

  • before_finalize

No other methods will be called from your controller unless you call them explicitly yourself. Application will be reblessed into a given controller only once per request. If a bridge route exists which uses the same controller as the regular route, the regular route will reuse the controller reblessed for the bridge. After the request ends, the reblessed controllers will be cleared.

Configuring controllers

Step 1: Configure the controller

It is a good practice to set up a different base, so that you separate general app code from request-handling code.

    # config.pl
    {
        modules_init => {
            Routes => {
                rebless => 1, # the app instance will be reblessed
                base => 'MyApp::Controller',
            }
        }
    }

Step 2: Create a main controller class

This step is only required if you've changed the base.

    # lib/MyApp/Controller.pm
    package MyApp::Controller;
    use Kelp::Base 'MyApp';

    # Now $self is an instance of 'MyApp::Controller';
    sub service_method {
        my $self = shift;
        ...;
    }

    1;

Step 3: Create any number of controller classes

They all must inherit from your main controller class.

    # lib/MyApp/Controller/Users.pm
    package MyApp::Controller::Users;
    use Kelp::Base 'MyApp::Controller';

    # Now $self is an instance of 'MyApp::Controller::Users'
    sub authenticate {
        my $self = shift;
        ...;
    }

    1;

Step 4: Add routes with shorter class names

You no longer have to prefix destinations with the base controller class name.

    # lib/MyApp.pm

    ...

    sub build {
        my $self = shift;

        # if 'base' was not changed, this would have to be written as:
        # => 'Controller::Users::authenticate'
        $self->add_route('/login' => 'Users::authenticate');

    }

Use different method than reblessing

While reblessing is how core Kelp deals with controllers, it is entirely possible to introduce your own base controller classes. Most of the details remain the same, like setting rebless and base configuration, but your main controller class does not have to inherit from your app class:

Step 1: Custom Context object

Create a custom context object which will call new on your controller classes instead of reblessing the main app. It is also a good idea in this case to enable persistent_controllers regardless of configuration.

    # lib/MyContext.pm
    package MyContext;

    use Kelp::Base 'Kelp::Context';

    # make sure controller objects are never cleaned
    attr persistent_controllers => !!1;

    sub build_controller {
        my ($self, $class) = @_;

        return $class->new(context => $self);
    }

Step 2: Custom base controller

A base controller class has to implement at least context, before_dispatch and before_finalize to be compatible with core Kelp. Some specific modules might assume it's a descendant of Kelp, which may require adding more methods to achieve compatibility. It may be also a good idea to implement req and res for easier access.

    # lib/MyController.pm
    package MyController;

    use Kelp::Base;

    attr -context => undef;

    sub app {
        my $self = shift;
        return $self->context->app;
    }

    sub req {
        my $self = shift;
        return $self->context->req;
    }

    sub res {
        my $self = shift;
        return $self->context->res;
    }

    sub before_dispatch {
        my $self = shift;
        $self->app->before_dispatch(@_);
    }

    sub before_finalize {
        my $self = shift;
        $self->app->before_finalize(@_);
    }

    sub some_route {
        my $self = shift;
        $self->res->text->render('hello from controller');
    }

Step 3: setting the custom context in the app

This can be done a couple different ways, but the easiest one is to change the default context_obj, which is the class name of the context:

    # lib/MyApp.pm
    package MyApp;

    use Kelp::Base 'Kelp';

    attr context_obj => 'MyContext';

    sub build {
        my $self = shift;

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

CAVEATS

There are some controller gotchas which come from a fact that they are not by default constructed like a regular object:

Main application object is shallow-cloned before rebless

By default, controllers are only temporary. Setting top-level attributes in a controller, for example "charset" in Kelp, will work until the request is fully handled. After that, the controller copy will be destroyed and the changes will not propagate back to main application. Moreover, any extra fields you set in the controller will be lost when the request handling is over.

If your main app has no changing state, a special configuration field persistent_controllers can be added to combat this. If it is set to a true value, the app will be reblessed just once per controller. This means no changes to the main app attributes will be visible in controllers, but the controller will be free to set and use all of its attributes at will. This way your controllers will be reused indefinetly and no changes in app's state will propagade to controllers.

Note that by default, Kelp main class has no top-level state which may change between requests so it should be pretty safe to enable this configuration as long as you don't instantiate your controllers before app building is finished.

Getting a controller copy in build

Controllers are never actually constructed, but instead the main app object is cloned and reblessed into the correct class. Don't expect your override of new or build to ever be called. No automatic controller initialization happens in Kelp.

If you'd like to access a controller in other context than route handling - for example in build method, allowing you to move route definitions to the controller - you may use context tracking object:

    # in MyApp.pm
    sub build {
        my $self = shift;

        # get a temporary rebless of the app and call its bulid method
        # will return MyApp::Controller::Special, if route base is MyApp::Controller
        my $controller_special = $self->context->controller('Special');
        $controller_special->build;

        # will return the main controller (MyApp::Controller)
        my $controller = $self->context->controller;
        $controller->build;

    }

Note that you will still have to use the controller name in routes even though they live in the same class:

    # in MyApp/Controller/Special.pm
    sub build {
        my $self = shift;

        # need to add special, even though this is controller special
        $self->add_route('/my_route' => 'special#handler');
    }

    sub handler { ... }

NOTE: Take extra care not to call build again if it wasn't overridden in a controller, as the controller will try to re-initialize the app, which will surely result in a loop! In addition, make sure to never call $self->SUPER::build in a controller.

Getting a main application object in a controller

This may be done by similarly using context:

    sub handler {
        my $controller = shift;

        # this will always be the main app object
        my $app = $controller->context->app;
    }

SEE ALSO

Kelp::Manual

SUPPORT