Skip to content

Commit

Permalink
Add Kelp::Manual::Controllers and allow easy controller building
Browse files Browse the repository at this point in the history
  • Loading branch information
bbrtj committed Jun 23, 2024
1 parent bdb4de2 commit ce7eafb
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 131 deletions.
5 changes: 3 additions & 2 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
- Added charset, charset_encode methods to Kelp::Response
- Added is_production, get_encoder methods and request_charset, encoder_modules, context attributes to Kelp
- Added Kelp::Module::Encoder, a base class for encoders (like JSON, to be used by the new get_encoder method)
- Added adapt_psgi, effective_charset, charset_encode, charset_decode, load_and_instantiate functions to Kelp::Util
- Added adapt_psgi, effective_charset, charset_encode, charset_decode, load_package functions to Kelp::Util
- Added a third, optional argument to camelize in Kelp::Util

[General]
- Fixed Route destination getting executed if a response was already rendered by a previous one
Expand All @@ -16,7 +17,7 @@
* The destination will still be run if the render happened inside 'before_dispatch' hook
- Response 'render' method will now assume you passed a json-encoded string if content type is json and body is not a reference
* The json string will still get its charset encoded, so the encoder should be utf8-disabled
- Kelp now tracks the context it's in via 'context' attribute
- Kelp now tracks its context via 'context' attribute
* App will now rememeber if it was reblessed and will only rebless once into given controller per request
* This fixes before_finalize not working when reimplemented inside a controller
* This also improves performance if a lot of bridges are defined as methods inside a controller
Expand Down
6 changes: 2 additions & 4 deletions lib/Kelp.pm
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,7 @@ sub build
# Override to use a custom request object
sub build_request
{
return Kelp::Util::load_and_instantiate(
$_[0]->request_obj,
return Kelp::Util::load_package($_[0]->request_obj)->new(
app => $_[0],
env => $_[1],
);
Expand All @@ -179,8 +178,7 @@ sub build_request
# Override to use a custom response object
sub build_response
{
return Kelp::Util::load_and_instantiate(
$_[0]->response_obj,
return Kelp::Util::load_package($_[0]->response_obj)->new(
app => $_[0],
);
}
Expand Down
27 changes: 24 additions & 3 deletions lib/Kelp/Context.pm
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
package Kelp::Context;

use Kelp::Base;
use Kelp::Util;
use Carp;

attr -app => sub { croak 'app is required' };
attr -controllers => sub { {} };
attr -_controllers => sub { {} };
attr current => sub { shift->app };

# loads the class, reblesses and returns - can be used to get controller on
# demand with partial or unloaded class name
sub controller
{
my ($self, $controller) = @_;
my $base = $self->app->routes->base;
$controller = '+' . $base
if !defined $controller;

$controller = Kelp::Util::camelize($controller, $base, 1);
Kelp::Util::load_package($controller);

croak "Invalid controller, not subclassing $base"
unless $controller->isa($base);
return $self->app->_clone($controller);
}

# reblesses, remembers and sets the current controller - used internally
sub set_controller
{
my ($self, $controller) = @_;

my $current = $self->controllers->{$controller} //=
# the controller class should already be loaded by the router
my $current = $self->_controllers->{$controller} //=
$self->app->_clone($controller);

$self->current($current);
return $current;
}

# clears the object for the next route - used internally
sub clear
{
my $self = shift;

%{$self->controllers} = ();
%{$self->_controllers} = ();
$self->current($self->app);
}

Expand Down
92 changes: 8 additions & 84 deletions lib/Kelp/Manual.pod
Original file line number Diff line number Diff line change
Expand Up @@ -778,88 +778,6 @@ to use in the main application class.

See more examples and POD at L<Kelp::Module>.


=head2 Reblessing into controller classes

All of the examples here show routes which take a instance of the web
application as a first parameter. This is true even if those routes live in
another class.

To rebless the app instance into the controller class instance, the controller
class must extend the class specified in C<base> configuration field. The
default value of that 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.

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.

=head3 Step 1: Configure the controller

It is a good practice to set up a different C<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',
}
}
}

=head3 Step 2: Create a main controller class

This step is only required if you've changed the C<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;

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

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

}

=head1 NEXT STEPS

=head2 Debugging
Expand Down Expand Up @@ -959,9 +877,15 @@ Run the rest as usual, using C<prove>:

Take a look at the L<Kelp::Test> for details and more examples.

=head2 Common problems and solutions
=head2 Other documentation

You may want to take a look at our L<Kelp::Manual::Cookbook> for common
problems and solutions.

Details of controllers can be found in L<Kelp::Manual::Controllers>.

You may want to take a look at our L<Kelp::Manual::Cookbook>.
Specific packages contain documentation about the interface of each part of the
system.

=head1 SUPPORT

Expand Down
199 changes: 199 additions & 0 deletions lib/Kelp/Manual/Controllers.pod
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
=pod

=encoding utf8

=head1 NAME

Kelp::Manual::Controllers - Making your app use controllers

=head1 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 - all controllers must
be subclasses of L<Kelp>.

=head2 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 C<Kelp::Routes/base>. C<Kelp::Routes/rebless> must albo be
enabled for that to occur.

The default value of C<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:

=over

=item * route handler method

=item * C<before_dispatch>

=item * C<before_finalize>

=back

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.

=head2 Configuring controllers

=head3 Step 1: Configure the controller

It is a good practice to set up a different C<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',
}
}
}

=head3 Step 2: Create a main controller class

This step is only required if you've changed the C<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;

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

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

}

=head1 CAVEATS

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

=head2 Controller's constructor is never called

Controllers are also never actually constructed, but instead the main app
object is cloned and reblessed into the correct class. Don't expect your
override of C<new> or C<build> to work.

=head2 Main application object is shallow-cloned before rebless

Reblessed controllers are only temporary. Setting top-level attributes in a
controller, for example L<Kelp/charset>, 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.

=head2 Getting a controller copy in C<build>

No automatic controller initialization happens in Kelp. If you'd like to access
a controller in other context than route handling - for example in C<build>
method, allowing you to move route definitions to the controller - you may use
C<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 C<build> again if it wasn't overridden in a
controller, as the controller will try to re-initialize the app, which will
surely B<result in a loop>! In addition, B<make sure to never call> C<<
$self->SUPER::build >> in a controller.

=head2 Getting a main application object in a controller

This may be done by similarly using C<context>:

sub handler {
my $controller = shift;

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

=head1 SEE ALSO

L<Kelp::Manual>

=head1 SUPPORT

=over

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

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

=back

Loading

0 comments on commit ce7eafb

Please sign in to comment.