CatalystのChainedアクションとExceptionの関係

一度エントリを書き損ねると更新が止まる悪癖…

sub prepare : PathPart('') Chained('/root') CaptureArgs(1) {
    my ($self, $c, $id) = @_;
    $c->stash->{ item } = $c->model('Service::Items')->find($id)
        or MyApp::Exception->throw('item not found'); # croak with exception object
}

sub show : PathPart('') Chained('prepare') Args(0) {
    my ($self, $c) = @_;
}

Controllerにこういうのを書いて、例外が発生したら通常のフローを止めてエラー処理に入る。というのがやりたかったんだけど、これがうまくいかない。

end()やMyApp::finalize()で後処理は出来るけど、Chainedの連鎖が止められない。上記の例だとprepare()でExceptionが走っても、show()は普通に実行されてしまう。そこで $c->stash->{ item } に依存したコードを書いてしまうと、また新たなエラーが発生する。


Chainedアクションの連鎖を止めるには、$c->detachする必要がある。上記の例であれば、MyApp::Exception->throwしている部分をdetachして受け皿を別途用意してやれば希望の動作になる。

しかしそれでは、Modelの処理の正否をControllerでその都度チェックすることになる。何かあったらModelがExceptionを投げて、それをControllerのどこかでまとめてcatchするという構成にしたい。


これをChainedでなく、例えばprepare()をauto()として実装した場合、auto()でMyApp::Exception->throwするとshow()は実行されない。

これは、Catalyst.pmのexecute()がそういう実装になっているから。

    if ( my $error = $@ ) {
        if ( !ref($error) and $error eq $DETACH ) {
            die $DETACH if($c->depth > 1);
        }
        elsif ( !ref($error) and $error eq $GO ) {
            die $GO if($c->depth > 0);
        }
        else {
            unless ( ref $error ) {
                no warnings 'uninitialized';
                chomp $error;
                my $class = $last->class;
                my $name  = $last->name;
                $error = qq/Caught exception in $class->$name "$error"/;
            }
            $c->error($error);
            $c->state(0);
        }
    }
    return $c->state;

detachやgoでアクションから抜けた場合はdieしているが、それ以外のエラーでは0を返すようになっている。autoは0を返せばdispatchを飛ばしてendに行くので問題ないが、Chainedでは0を返してもチェーンが切れないので、続くアクションが実行されてしまう。

Catalystアプリの中で例外を使うアプローチとしては、

に面白い例があって、pixisがこの方法を採用しているようだ。

ただ、これもChainedアクションの中でException->throwした場合に次のアクションが実行されてしまうことには変わりない。

pixisではどうしてるのかと思ったけど、この方法を使ってる部分は2箇所しかなくて、それもChainedで実装されている部分ではなかった。

sub prepare : PathPart('') Chained('/root') CaptureArgs(1) {
    my ($self, $c, $id) = @_;
    eval {
        $c->stash->{ item } = $c->model('Service::Items')->find($id)
            or MyApp::Exception->throw('item not found');
    };
    $c->forward('catch', [ $@ ]) if $@;
}

とか書いて、catch()でエラー処理してdetachすれば解決するけど、全部のChainedアクションにいちいち書くとか面倒すぎる。

こういうのって、皆どうやって処理してるんだろうか。Exceptionはともかく、die/croakした時にChainedを切りたいってのはそんなに珍しいケースではないと思うんだけど。

現実的な対応としては、MyApp::execute()をオーバーライドして例外補足したらdetachする、みたいな対策を入れることかなぁ…



追記:試しに、MyApp.pmにこんな風に書いてみた。

sub execute {
    my $c = shift;
    my $res = $c->next::method(@_);
    die $Catalyst::DETACH if ref $@;
    return $res;
}

アドホックもいいとこだけど、end()の後にオブジェクトが投げられることはないはず。