beginとかautoとかでdetachする

Catalystでハマった

あるControllerで、指定の条件下ではアクションを実行させず404にしたい、というときにbeginとかautoで

package MyApp::Controller::Hoge;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller' }

sub begin :Private {
    my ($self, $c) = @_;

    $c->detach('/default');
}

sub index :Path :Args(0) {
    my ($self, $c) = @_;

    $c->log->debug('hoge');
    $c->detach('/default');
    $c->log->debug('fuga');
}

__PACKAGE__->meta->make_immutable;

1;

のように書いて、

package MyApp::Controller::Root;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller' }

sub default :Path {
    my ( $self, $c ) = @_;
    $c->response->body( 'Page not found' );
    $c->response->status(404);
}

sub end : ActionClass('RenderView') {}

__PACKAGE__->meta->make_immutable;

1;

の'/default'にdetachするように書いてみる。/hoge/begin内でdetachするので/hoge/indexには到達しない、と思っていたらどうやら実行されてしまうらしい。

[debug] "GET" request for "hoge" from "127.0.0.1"
[debug] Path is "hoge"
[debug] hoge
[debug] Response Code: 404; Content-Type: text/html; charset=utf-8; Content-Length: 14
[info] Request took 0.010449s (95.703/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /hoge/begin                                                | 0.002575s |
|  -> /default                                               | 0.001170s |
| /hoge/index                                                | 0.001678s |
|  -> /default                                               | 0.000795s |
| /end                                                       | 0.000408s |
'------------------------------------------------------------+-----------'

index内でdetachしたあとの部分は実行されない。


beginとかautoとかの挙動を把握できていない。ちょっとソースを読んでみた…

package Catalyst::Controller;

...

__PACKAGE__->_dispatch_steps( [qw/_BEGIN _AUTO _ACTION/] );

...

sub _DISPATCH : Private {
    my ( $self, $c ) = @_;

    foreach my $disp ( @{ $self->_dispatch_steps } ) {
        last unless $c->forward($disp);
    }

    $c->forward('_END');
}

というところで"_BEGIN", "_AUTO", "_ACTION"が順番に実行されるらしい。$c->forward("_BEGIN"), $c->forward("_AUTO"), $c->forward("_ACTION") それぞれ返り値が真であれば順番に実行される、ということのようだ。それぞれ途中で$c->detach('/default')された場合に何が起こるのか…はCatalyst::executeを追いかければいいのかな? どうも最終的に返って来るのは'/default'の返り値のようだ。

sub default :Path {
    my ( $self, $c ) = @_;
    $c->response->body( 'Page not found' );
    $c->response->status(404);
}

と書いてある場合、"_DISPATCH"で呼ばれる$c->forward("_BEGIN")の結果は404になるっぽい。ので、ここで偽値が返るよう

sub default :Path {
    my ( $self, $c ) = @_;
    $c->response->body( 'Page not found' );
    $c->response->status(404);

    return 0;
}

と書いてやればbegin内のdetachでそれ以降のアクションが実行されないようになるようだ。

[debug] "GET" request for "hoge" from "127.0.0.1"
[debug] Path is "hoge"
[debug] Response Code: 404; Content-Type: text/html; charset=utf-8; Content-Length: 14
[info] Request took 0.008613s (116.104/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /hoge/begin                                                | 0.003071s |
|  -> /default                                               | 0.001199s |
| /end                                                       | 0.000442s |
'------------------------------------------------------------+-----------'


また、"_AUTO"では

sub _AUTO : Private {
    my ( $self, $c ) = @_;
    my @auto = $c->get_actions( 'auto', $c->namespace );
    foreach my $auto (@auto) {
        $auto->dispatch( $c );
        return 0 unless $c->state;
    }
    return 1;
}

と返り値(?)をみているので("_BEGIN"では$c->errorしかみていない)、'/default'が偽値を返さなくても

sub auto :Private {
    my ($self, $c) = @_;

    $c->forward('/default');
    return 0;
}

と書くことでその後の"_ACTION"に処理が進まないように指定できる。

[debug] "GET" request for "hoge" from "127.0.0.1"
[debug] Path is "hoge"
[debug] Response Code: 404; Content-Type: text/html; charset=utf-8; Content-Length: 14
[info] Request took 0.005840s (171.233/s)
.------------------------------------------------------------+-----------.
| Action                                                     | Time      |
+------------------------------------------------------------+-----------+
| /hoge/auto                                                 | 0.001469s |
|  -> /default                                               | 0.000943s |
| /end                                                       | 0.000376s |
'------------------------------------------------------------+-----------'

一方、Arkは

Arkの場合は、begin, autoどちらの場合もdetachするとそれ以降のアクションは実行されない。

sub begin :Private {
    my ($self, $c) = @_;

    $c->detach('/default');
}
.----------------------------------------------------------------+-----------.
| Action                                                         | Time      |
+----------------------------------------------------------------+-----------+
| /hoge/begin                                                    | 0.002738s |
|   -> /default                                                  | 0.000226s |
'----------------------------------------------------------------+-----------'
sub auto :Private {
    my ($self, $c) = @_;

    $c->detach('/default');
}
.----------------------------------------------------------------+-----------.
| Action                                                         | Time      |
+----------------------------------------------------------------+-----------+
| /hoge/auto                                                     | 0.002725s |
|   -> /default                                                  | 0.000241s |
'----------------------------------------------------------------+-----------'


Arkのなかの処理は、

package Ark::Action;

...

sub dispatch {
    my ($self, $context, @args) = @_;

    return if $context->detached;

    ...

}

sub dispatch_chain {
    my ($self, $context) = @_;

    $self->dispatch_begin($context)
        and $self->dispatch_auto($context)
        and $self->dispatch($context);

    $context->detached(0);
    $self->dispatch_end($context)
        unless $context->res->is_deferred or $context->res->is_streaming;
}

となっていて、dispatch_beginにしろdispatch_autoにしろdispatchの一番最初に$context->detachedを見て既にdetachされている状態だったら処理しない、という仕組みになっているおかげで、こういう挙動になるようだ。