戻るリンクの実現

戻るリンクを簡単に扱えるようにするためのプラグイン作った。

セッションを利用するのでネーミングとしてCatalyst::Plugin::Session::BackURLと名付けてみた。

package Catalyst::Plugin::Session::BackURL;
use strict;
use warnings;

our $VERSION = '0.05';

sub setup {
    my $c = shift;
    $c->config->{back_url}            ||= {};
    $c->config->{back_url}->{default} ||= '/';
    $c->config->{back_url}->{field}   ||= 'back_url';
    $c->config->{back_url}->{target}       ||= {};
    $c->config->{back_url}->{target_regex} ||= {};
    
    for my $target_key ( qw/target target_regex/ ) {
        my $flip = {};
        tie %$flip , 'Catalyst::Plugin::Session::BackURL::_TieHash' if $target_key eq 'target_regex';
        my $target = $c->config->{back_url}->{$target_key};
        for my $base_url ( keys %$target ) {
            my $back_urls = $target->{$base_url};
            $back_urls = [$back_urls] unless ref $back_urls eq 'ARRAY';
            for my $back_url ( @$back_urls ) {
                push @{$flip->{$back_url}} , $base_url;
            }
        }
        $c->config->{back_url}->{"_${target_key}_flip"} = $flip;
    }
    
    $c->NEXT::setup(@_);
}

sub finalize_session {
    my $c = shift;
    my $path = $c->req->path;
    
    for my $target_key ( qw/_target_flip _target_regex_flip/ ) {
        if ( my $target = $c->config->{back_url}->{$target_key}->{$path} ) {
            $c->save_back_url($path,$_) for @$target;
        }
    }
    
    return $c->NEXT::finalize_session(@_);
}

sub back_url {
    my $c = shift;
    my $field = $c->config->{back_url}->{field};
    my $back_url = scalar $c->req->param($field)
        || $c->session->{$field}->{$c->req->path}
        || $c->session->{$field}->{':default'}
        || shift
        || $c->config->{back_url}->{default};
    return $back_url =~ m|^[./]+| ? $back_url : '/'.$back_url;
}

sub save_back_url {
    my $c = shift;
    my $back_url = shift || $c->req->path;
    my $base_url = shift || ':default';
    $c->session->{$c->config->{back_url}->{field}}->{$base_url} = $back_url;
}

sub clear_back_url {
    my $c = shift;
    my $base_url = shift || ':default';
    return delete $c->session->{$c->config->{back_url}->{field}}->{$base_url};
}

sub all_clear_back_url {
    my $c = shift;
    $c->session->{$c->config->{back_url}->{field}} = {};
}

package Catalyst::Plugin::Session::BackURL::_TieHash;
use strict;
use warnings;
use base qw/Tie::RegexpHash/;

sub match {
    my ($self, $key) = @_;
    return unless$self->{COUNT};
    
    my $i = 0;
    my @ret;
    while ($i < $self->{COUNT}) {
        if($key =~ m/$self->{KEYS}->[ $i ]/) {
            push @ret,$i;
        }
        $i++;
    }
    
    return unless @ret;
    return [map { @{$self->{VALUES}->[ $_ ]} } @ret];
}

*FETCH = \&match;

save_back_urlで戻るリンクを保存してback_urlで保存したURLを取得する感じ。

実際の使い方として

package MyApp::Controller::Hoge;
use strict;
use warnings;
use base 'Catalyst::Controller';

sub index : Private {
    my ( $self , $c ) = @_;
    $c->save_back_url($c->req->path,'hoge/finish');
}

sub page1 : Local {
    my ( $self , $c ) = @_;
    $c->save_back_url($c->req->path,'hoge/foo');
}

sub page2 : Local {
    my ( $self , $c ) = @_;
    $c->save_back_url();
}

sub finish : Local {
    my ( $self , $c ) = @_;
    my $back_url = $c->back_url('./');
}

sub foo : Local {
    my ( $self , $c ) = @_;
    my $back_url = $c->back_url('./');
}

こんな感じ。

save_back_urlには二つの引数を渡せる。

第一引数は保存するURL。第二引数はベースとなるURL。

ベースとなるURLとはつまり第二引数のURLにアクセスしたときにback_urlを呼び出すとその時保存した第一引数のURLが取得できるってわけ。

上の例で言うとindexを経由してfinishにアクセスするとback_urlの戻り値に「hoge/」が返ってくるが、page1を経由してfinishにアクセスしてもback_urlの戻り値は「hoge/page1」にはならないといった感じ。

次にpage2のようにsave_back_urlの引数を省略した場合は現在のパスが保存される。そして第二引数も省略されているのでその場合は共通の空間にURLが保存される。

つまりpage2を経由した場合はfinishやfooでpage2のURLが返ってくることになる。

back_urlにも引数が渡せるのだがこれは保存されたURLが見つからない場合のデフォルト値として使われる。

とまぁ概要はこんな感じ。説明わかりにくいかな・・・。実際保存されてるURLをDumpとかしてみたほうがわかりやすいかも。

んでですね。

もうひとつ機能があって、わざわざsave_back_urlを呼んだりするのが面倒なんでYAMLで予め登録できるようにしてみた。

こんな感じ

back_url:
  default: /index.html
  field: back_url
  target:
    foo/finish:
      - foo/index
      - foo/page1
      - foo/bar
      - foo/baz
    bar/finish:
      - bar/index
      - foo/index
      - baz/index

こう書いておけばfoo/indexやfoo/page1などを経由してfoo/finishに行った場合にback_urlを呼び出すとその経由元のURLが返ってきます。

あと正規表現で定義することもできます。先ほどの例を正規表現で書くと、

back_url:
  default: /index.html
  field: back_url
  target_regex:
    foo/finish: 'foo/.*'
    bar/finish: '.*/index'

こんな感じになります。キー名がtargetではなくtarget_regexなので注意してください。

正規表現の判定の処理を始めTie::RegexpHashでやろうと思ったんですが最初のひとつにマッチした場合にそこで終了しちゃうんで継承して複数マッチできるようにしました。

あと素でハッシュをループしながら正規表現で判定するよりもTie::RegexpHash使って判定したほうが処理速度が2倍近く速いので驚いた。tieパワー?

ちなみにback_urlは基本的にテンプレート側で呼び出して使うことが多いと思います。

 <a href="[% c.back_url('./') %]">前のページに戻る</a>

追記:2007/04/29

finalizeで書き込みしてたのをfinalize_sessionに変更。

finalizeだとSessionより先に継承しないといけないのが微妙なんで。

finalize_sessionだとSessionより後で継承すればいいので綺麗。