カテゴリ内の前後へのリンクについての考察

エントリーのページに付随する「次のエントリーへ」のリンクは、その次のエントリーが同じカテゴリー内にあるエントリーであればいいのだけれども、そうでない場合は、その遷移には少々違和感を覚えるものだ。

Six Apart のプラグイン・ディレクトリで、 PreviousNextInCategory [参考1] というプラグインを見つけたのでそう思ったのか、そう思ったからこのプラグインを見つけたのか、どちらが先だったか、ともかくも、つまりは、エントリーの次のエントリーは、同じカテゴリー内での「次のエントリー」であるべきだと思ったので、これを試してみようかと思った。

その前に、ちまたでの評判はどうかな? と調べてみると、このプラグインは少し問題を抱えているようだった。

そのエントリーが複数のカテゴリーに属している場合。

これについてはパッチしている例がたくさん見られたので、それを真似すればよいかと思われた。ただその結果、その成果として得られるように、複数のカテゴリーぶんだけのナビゲーション・リンクを表示させるのは、あまりスマートなものではないなと思えた。でもそれは致し方のない事か。カテゴリーという概念が、そもそも使いづらいのだろう。これは Movable Type (MT) がどうこういう問題でもない。

それから、問題ではないけれども、そのエントリーをビルドした際に、カテゴリー内の前後のエントリーは再構築されないので、エントリーを投稿(作成、更新、削除)するたびに、関係するエントリーを再構築しないといけない。そうしないと、次へのリンクが生成(更新)されないから、あたらしいエントリーへの道筋が作られない。

再構築は時間もかかるし面倒くさいから、願わくば、再構築の必要があるエントリー(前後のエントリ−)だけを、自動で再構築するようにしてもらいたい。──そういう要望もやはりみな同じく持っているようで、じぶんも、言われてみれば確かにそうだなと気がついた。この問題に対しても、併せてパッチしているようだった。

それから、そういう再構築を自動で行わせるための機能を、パッチするではなくプラグインで実装したものがあった[参考2]。システム標準のファイルを改造するのは宜しくないということだ。そのコンセプトにおおいに頷きつつ、実際に使ってみた。

──そして残念ながら、そのプラグインはうまく動かなかった。

しかしただ「動かない。」だけでは気が済まないし、なによりそのコンセプトにはおおいに共感するものがあったので、なぜ動かないのかを追ってみる事にした。そして、その解決方法について考えてみた。

新規エントリー時の問題

そのプラグイン 'RebuildPrevNextInCategory.pl' (バージョン 1.2 )は、次のように、 MT::Entry のコールバックを利用していた。これは、エントリーがデータベースに保存または削除された後に、 post_save_entry() というルーチンを実行する。

MT::Entry->add_callback ('post_save', 10, $plugin, ¥&post_save_entry);
MT::Entry->add_callback ('post_remove', 10, $plugin, ¥&post_save_entry);

このプラグインでの問題のひとつに、まず、それはその配布元でも説明がなされているように、新規エントリーを「公開する」と共に保存した時には、期待する再構築は動かない。しかし、いったん保存した後で、もういちど保存すると動く。──確かに、そのとおりだった。

デバッグしてみると、新規エントリーの保存の時には、「そのエントリーからカテゴリーが拾えない場合は何もしない」という処理のところに、制御が入って来ていた。

my $cat = $entry->category;
if (!$cat || $entry->status != MT::Entry::RELEASE) {
    return;
}

ではなぜ、新規保存のときに呼ばれるエントリー( MT::Entry )の 'save' 時には、エントリーはカテゴリーを持っていないのだろうか? (それは $entry->category が 'undef' を返すという事)。その疑問は、 MT::App::CMS->save_entry を見ると判明した。

ひとことで言うならば、エントリーの内容と、そのカテゴリー(内部では Placement と呼ばれる)は別々に保存されている、ということだった。そして同時に、エントリー( MT::Entry オブジェクト)はそもそもカテゴリーを持っているわけではない、ということも関係しているようだった。

詳しく動作の段取りを辿ってみると、新規保存の場合、まずエントリーが保存されて、このときコールバックが呼ばれて、そしてそのあとにカテゴリーが保存される。従って新しいエントリーをデータベースに 'save' したところで、カテゴリーの情報は、そのエントリーにはまだひもづけられていない。言い換えると、 'save' のときに呼ばれるコールバック、すなわちプラグインに制御が入って来た時には、まだカテゴリーの保存が済んでいないから、プラグインの中(のコールバックルーチン)では「そのエントリーからカテゴリーが拾えない」ので、「その場合は何もしない」に制御が入ってしまい、結果、再構築は行われない。一方で、 MT::Entry がいったん保存されている場合は、カテゴリーも保存されているので、それを $entry->category は読み出す事ができるために、処理は再構築の方へ進んで行く。

以上をまとめて抽象化すると、 MT::App::CMS では、次のような段取りで、エントリーとカテゴリーの保存はなされていた:

if( 新規エントリー ){
    オブジェクト = 空の MT::Entry を作成する
}else{
    オブジェクト = エントリー ID の MT::Entry をロードする
}

オブジェクトにエントリーの内容を入れる(エントリーを更新する)

エントリーを保存(ここで、コールバックが呼ばれる)

カテゴリーのリストを、入力から読み込む

保存されていたカテゴリーをすべて破棄してから、カテゴリーのリストを保存する

ここで、もうひとつ気になる点も見つかった。エントリーの更新時には、もちろん、カテゴリーの変更を伴う場合もあるだろうけど、 MT::Entry の save のところでコールバックをするようだと、カテゴリーを変更した場合には、変更する前のカテゴリーにのみ影響が出て、変更後のカテゴリーのほうは、取り扱われないのではなかろうか??

それからまた、デバッグをしてみたらば、コールバックは連続して2回呼ばれていた。これはなぜなのだか辿りきれていないものの、これまで述べたところからも、少なくとも MT::Entry の save 時のコールバックは適切でないように思われた。

ではどうすべきか。たぶん、コールバックするのに適切な場所が、どこか別にあるのだろうと思われた。──調査を続けてみる。

早速見つけたのは、 MT::App::CMS ではアプリケーションレベル・コールバックとして、次のコールバックが用意されていた。

CMSPostEntrySave

これは、エントリーがセーブされた後に、何かの処理を行わせる事ができるようにする。それだけ聞くと、 MT::Entry の 'post_save' と同じようだ。けれども、その実、タイミングはぜんぜん異なるものだ。

MT::Entry の 'post_save' は、データベースへのセーブの後に発動するコールバックなのに対して、 CMSPostEntrySave は、エントリー編集画面上(アプリケーション・レベル)での「エントリーのセーブ」という動作の後に発動する。このコールバックが発動される場所を調べてみると、「エントリーのセーブ」の中には、エントリーと、そしてカテゴリーのデータベースへのセーブが含まれているものであった。──つまりこれがまさに、おあつらえ向きだった。

そこで、こんなふうに実装してみる:

MT->add_callback('CMSPostEntrySave',10,undef,¥&cms_post_entry_save);

sub cms_post_entry_save {
    my ($eh, $app, $entry) = @_;
    ...
}

cms_post_entry_save() に渡された $entry には、エントリー編集画面にて、新規に投稿されたエントリー、または、更新されたエントリーのオブジェクト( MT::Entry )が渡されて来る。そして、その category メソッドは、新規であろうと更新であろうと、保存された後のカテゴリーの情報を再構成するもの(オブジェクト)であるから、結果、このコールバックを用いるのがよかろう、という結論に導かれた。

そうしたらあとは、参考にしたプラグインの中身のとおり、再構築メソッドを走らせればいい。

ということで、この点については、このコールバックを使えばよさそうだ。

エントリー削除時の問題

さて次に、エントリー削除時の挙動を追ってみた。

参考にしたプラグインでは、コールバックするポイントが異なるだけで、処理はまったく同じ事を行っている。──でも、これは働かなかった。

この理由も、結局は同じような事だった。 'post_remove' はエントリーをデータベースから削除した後に、発動する。しかし、削除するメソッドの中身を見てみたらば、エントリーの内容を消す前に、先に、エントリーにひもづいている Placement (カテゴリー)をすべて削除していた。つまり、エントリーの remove に到達したときには、すでにカテゴリーを失っているため、 remove によって呼ばれたコールバックの中でカテゴリーを拾いだそうとしても、もう遅いのだった。つまりこのコールバックのポイントでは、残念ながら不適切なのだ。

そこで、これも MT::App::CMS に頼りどころがあるのではないかと、それを見てみた。しかし、削除する時には、コールバックする機会が設けられていなかった。

そうすると、プラグインですべてを賄うには難しいのだろうか。

いや、まだチャンスはあった。データベースの削除時に発動するコールバックはなにも MT::Entry だけではない。もちろん、カテゴリーである MT::Placement でもいい。ということは、先にカテゴリーが削除されてしまうのならば、削除される前にその情報をどこかに保存しておいて、エントリーが削除されるところで改めてその情報を使う事も、できるのではないだろうか、と気がついた。

...
MT::Placement->add_callback(
  'pre_remove',1,undef,¥&placement_pre_remove);
MT::Entry->add_callback(
  'pre_remove',1,undef,¥&entry_pre_remove);

our %entry_had_categories = ();  # 保存場所

sub placement_pre_remove {
  my ($cb, $obj) = @_;
    
  # エントリー ID に、カテゴリー ID を保存しておく
  $entry_had_categories{$obj->entry_id}->{$obj->category_id} = 1;
  ...    
}

sub entry_pre_remove {
  my ($cb, $obj) = @_;

  for my $catid ( keys %{$entry_had_categories{$obj->id}} ){
      # カテゴリー ID $catid を使って、何かをする
      ...
     }
  ...
}

こんなふうに試してみたらば、 entry_pre_remove() の中でも、首尾よくカテゴリー ID のリストを得る事ができた。

そうしたらば、あとはそのカテゴリーの前後の記事を拾いだして、再構築をかけるようにしむけてあげればよいことになる。

その他のポイント

以上大きく二点。だいたいこんなところだろうか。

繰り返しになるけれども、プラグインにしようとも、再構築を行うところのソースにパッチしようとも、同じ問題として気をつけないといけないのは、カテゴリーに変更があった際には、変更前後のそれらについても、再構築するようにしないと、前後のエントリーのページにリンクが残ったままになってしまうだろうから、これも考慮しておかないといけない(できるかな?)。

そして参考にしたプラグインについてついでをいうならば、そのプラグインではプライマリ・カテゴリーだけが対象となっていたけれども、エントリーが複数のカテゴリーを持っていた場合は、そのすべてについて、前後のエントリーを再構築するようにすることも必要だろう。

それから以上の修正を加えたところで、それを使うにあたっては PreviousNextInCategory を使う事が前提となる。でもそれは限定的な依存関係だろうから、わざわざ別々にしておく事もない。その前提とするプラグインが提供するタグも、マージしてしまったほうがスッキリするだろう。もちろん、その際には、皆がパッチしている内容を取り込んで。

──というふうな見通しもついたなので、では諸々まとめたものを作ってみようか、と思ったけれども、ここまで気を使うくらいだったらば、再構築を行うところに、パッチを充てる方がむしろ手間がなくていいかもしれないと思えてきた。ソースに手を加えないというコンセプトは捨てがたいけれども。

でもそれよりか何よりか、さっさと(エントリー・アーカイブについて)ダイナミック・パブリッシングに移行した方がよいのかもしれない。そもそも再構築をすることがなくなれば、それでいいのだから。

参考:

TB:

  • Movable Type

トラックバック

このエントリーのトラックバックURL:
http://hwat.sakura.ne.jp/mt/ftb.cgi/272

コメント (4)

sugi:

こんにちは。RebuildPrevNextInCategoryを作ったものです。本来こちらで調べるべきところ、ここまで調べていただいて、感謝しています。
hiroakiさんの方ですべてとりこんだプラグインを作るかもとありますが、具体的に進められていますか?もし、計画がないかあるいはかなり先になるということでしたら、こちらもいい加減なものをそのまま公開しておくのもどうかと思うので、調べていただいた内容をとりこんで修正したいと思います。逆にhiroakiさんのほうで作成するということでしたら、そちらに便乗させていただき、こちらの公開はとりやめようと思います。
いかがでしょうか?

hiroaki:

sugi さん、コメントありがとございます。こっそりしたTBで失礼しました。カテゴリーってどうも扱いづらくて、結局いまでもどうしようかなあと悩んでいます。

プラグイン、作るかもだなんて思わせぶりに書いてしまってスミマセン。いまの所は作るつもりはありません。ページをダイナミックにしようかなといま模索中でもあり、そうすると不要になってしまいますからね。カテゴリーの概念自体も、どうしたら捨てられるかなあなんて妄想したりしている次第なのです。

そういうぼくの話は別としても、もし、作ってくださるのであれば、そのほうがよかろうかとも思います。何より、もともとのアイデアは sugi さんの所にありますし、同プラグインはそれなりに普及していると見受けられますし。(あとで気づいたのですが、けっこう前に作られたものだったのですね。)──その際に、ぼくの調査報告がお役に立てるようであれば、さいわいに思うのであります :-)

sugi:

なぜかこのエントリーへのトラックバックが403で失敗してしまうので、コメントでご報告します。下記ページで修正したプラグインを公開しました。今回はいろいろありがとうございました。

http://chez-sugi.net/movabletype/001360.html

hiroaki:

sugi さんこんにちは。──あれッ トラックバックできませんでしたか...(!?)
迷惑トラックバックにはいっちゃったかな? と思ってみてみたら、別な意味でぎょっとしました。気づかないうちに、大量の(本当の意味の)迷惑TBが入っていました... 。こういうもんなんですねぇ。
ちなみに調べましたところ、 MT 3.2 にはスロットル制限があって、それが迷惑トラックバックとの関わりの上で、いささかの問題を持っているようなので、それに対するパッチを充てて対処しました。お閑な時があれば、お手数ではありますが、トラックバックしてみてください。

さて、本題の方ですが、ご報告いただきましてありがとございます。ぼくはまだカテゴリーどうしようかなと悩んでいるのですが、結局 sugi さんのプラグインにお世話になるかもしれません。その際はまたよろしくおねがいします。

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)

広告

国道

hugin - Panorama Tools GUI