Catalystで未定義アクションのテンプレートを描画

Railsだとコントローラにアクションが定義されていなくても、リクエストに応じたビューのテンプレートファイルが存在すればそれが描画される(ルーティングはされてる必要がある気がする)。

で、Catalystでもそうなってるのかと思ったけど、どうやらそうなってないらしい(?)ので、自分で書いてみた。

まずはコントローラの共通の基底クラスのdefaultアクションで、リクエストに応じたテンプレートを探す。

package MyApp::Controller::Base;
use Moose;

BEGIN { extends 'Catalyst::Controller' }

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

  if (my $template = $self->suggest_template($c)) {
    # テンプレートが存在すればそれをセットする
    $c->stash->{template} = $template;
    
    # 子孫クラスでの処理をendアクションまですっ飛ばす
    $c->detach;
  }
  else {
    # ファイルがなければ404
    $c->res->body('Page not found');
    $c->res->status(404);
  }
}

# テンプレートの描画はお任せ
sub end :ActionClass('RenderView') {}

#
# namespace+action+suffixに対応するビューのファイルが存在すれば
# そのrootからの相対パスを返す。存在しなければundefを返す。
# 例:GET /hoge/fuga => hoge/fuga.html
#
# suffixはstaticとして評価されないtmpl,tt,tt2,html,xhtmlのいずれかで、
# この順序でマッチしたものから優先的に返す。
#
sub suggest_template {
  my ($self, $c) = @_;

  foreach (@{$c->{static}{ignore_extensions}}) {
    my $relative_path = $c->req->{_path}.".".$_;
    my $absolute_path = $c->{root}."/".$relative_path;

    return $relative_path if -e $absolute_path;
  }

  return undef;
}

実コントローラクラスではMyApp::Controller::Baseを継承するだけでOK。

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

BEGIN { extends 'MyApp::Controller::Base' }
__PACKAGE__->config(namespace => '');

もし実コントローラクラスでdefaultアクションをオーバーライドする場合には、親クラスのdefaultを呼ぶようにする。

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

  # 親クラス(MyApp::Controller::Base)のdefaultアクションを呼ぶ
  # もしテンプレートファイルが存在すればその時点でdetachされるので以降の処理は呼ばれない
  $self->next::method($c);
  
  # 以下、実コントローラならではの処理
}

ライブラリとか使ったもっとましなやり方もあると思うんだけど見つからなかった。とりはえずはこれで動くので良し。