Skip to content

Commit

Permalink
Add after_unrendered hook, run hooks conditionally
Browse files Browse the repository at this point in the history
  • Loading branch information
bbrtj committed Oct 7, 2024
1 parent 00c83e2 commit 489339e
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 40 deletions.
5 changes: 5 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Revision history for Kelp

{{$NEXT}}
[New Interface]
- Added after_unrendered hook to Kelp

[Changes]
- Hooks are now run conditionally on controller objects and fall back to main app object

2.17 - 2024-07-06
[Changes]
Expand Down
67 changes: 54 additions & 13 deletions lib/Kelp.pm
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,20 @@ sub res
return $self->context->res(@_);
}

sub _run_hook
{
my $self = shift;
my $method = shift;
my $c = $self->context->current;

if ($c->can($method)) {
$c->$method(@_);
}
else {
$self->$method(@_);
}
}

# Override to change what happens before the route is handled
sub before_dispatch
{
Expand All @@ -220,6 +234,24 @@ sub before_dispatch
}
}

# Override to change what happens when nothing gets rendered
sub after_unrendered
{
my ($self, $match) = @_;

# render 404 if only briges matched
if ($match->[-1]->bridge) {
$self->res->render_404;
}

# or die with error
else {
die $match->[-1]->to
. " did not render for method "
. $self->req->method;
}
}

# Override to manipulate the end response
sub before_finalize
{
Expand Down Expand Up @@ -310,18 +342,7 @@ sub psgi

# If nothing got rendered
if (!$res->rendered) {

# render 404 if only briges matched
if ($match->[-1]->bridge) {
$res->render_404;
}

# or die with error
else {
die $match->[-1]->to
. " did not render for method "
. $req->method;
}
my $c = $self->_run_hook(after_unrendered => ($match));
}

return $self->finalize;
Expand Down Expand Up @@ -363,7 +384,7 @@ sub finalize

# call it with current context, so that it will get controller's hook if
# possible
$self->context->current->before_finalize;
$self->_run_hook(before_finalize => ());

return $self->res->finalize;
}
Expand Down Expand Up @@ -790,6 +811,26 @@ default router will pass the unchanged L<Kelp::Routes::Pattern/to>. If
possible, it will be run on the controller object (allowing overriding
C<before_dispatch> on controller classes).
=head2 after_unrendered
Override this method to control what's going to happen when a route has been
found but it did not render anything. The default behavior is to render page
404 (if only bridges are found) or throw an error.
This hook will get passed an array reference to all matched routes, so you can
inspect them at will to decide what to do. It's strongly recommended to still
render 404 if the last match is a bridge (as is default).
sub after_unrendered {
my ( $self, $matches ) = @_;
if ($matches->[-1]->bridge) {
$self->res->render_404;
}
else {
# do something custom
}
}
=head2 before_finalize
Expand Down
34 changes: 16 additions & 18 deletions lib/Kelp/Manual/Controllers.pod
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ These methods will be automatically run on your controller object for each reque

=item * route handler method

=item * C<before_dispatch>

=item * C<before_finalize>
=item * hooks (if available)

=back

Expand Down Expand Up @@ -148,11 +146,10 @@ to enable C<persistent_controllers> regardless of configuration.

=head3 Step 2: Custom base controller

A base controller class has to implement at least C<context>,
C<before_dispatch> and C<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 C<req> and C<res> for easier access.
A base controller class has to implement at least C<context> 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 C<req> and C<res> for easier access.

# lib/MyController.pm
package MyController;
Expand All @@ -176,16 +173,6 @@ implement C<req> and C<res> for easier access.
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');
Expand Down Expand Up @@ -278,6 +265,17 @@ 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.

A little trick to make sure your build don't get called more than once is to
start it with a guard:

sub build {
my $self = shift;
return unless ref $self eq __PACKAGE__;
}

This way you don't have to worry about possibility of duplicated build calls
as a result of inheritance.

=head2 Getting a main application object in a controller

This may be done by similarly using C<context>:
Expand Down
6 changes: 3 additions & 3 deletions lib/Kelp/Routes.pm
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,10 @@ sub dispatch
unless $dest;

my ($to, $controller, $action) = ($route->to, @{$dest});
$app = $app->context->set_controller($controller);
my $c = $app->context->set_controller($controller);

$app->before_dispatch($to);
return $action->($app, @{$route->param});
$app->_run_hook(before_dispatch => ($to));
return $action->($c, @{$route->param});
}

1;
Expand Down
8 changes: 2 additions & 6 deletions t/lib/CustomContext/Controller.pm
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ sub res
return $_[0]->context->res;
}

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

sub before_finalize
{
my $self = shift;
Expand All @@ -35,6 +29,8 @@ sub before_finalize
sub build
{
my $self = shift;
return unless ref $self eq __PACKAGE__;

my $app = $self->app;

$app->add_route(
Expand Down
10 changes: 10 additions & 0 deletions t/lib/MyApp2/Controller/Bar.pm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use Kelp::Base 'MyApp2::Controller';

attr last_value => 1;

sub empty { }

sub naughty_secret { "I control the Bar" }

sub test_inherit { "OK" }
Expand Down Expand Up @@ -32,11 +34,19 @@ sub test_persistence
return $last;
}

sub after_unrendered
{
my ($self, $match) = @_;

$self->res->render('whoops');
}

sub build
{
my $self = shift;
my $r = $self->routes;

$r->add("/empty", "Bar::empty");
$r->add("/blessed_bar", "Bar::blessed");
$r->add("/blessed_bar2", "bar#blessed");
}
Expand Down
3 changes: 3 additions & 0 deletions t/routes_controller.t
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@ $t->request_ok(GET '/test_res_template')
->header_is('X-Controller' => 'Bar')
->content_like(qr/confession: I control the Bar/);

$t->request_ok(GET '/empty')
->content_is('whoops');

done_testing;

0 comments on commit 489339e

Please sign in to comment.