Categories
open all | close allTags
テスト | Migration | フォーム | アクセス制御 | CSRF | タグ | パソコン | Flash | デュアル・コア | Aptana | 名称 | 国際化 | 認証 | rake | Subversion | スキンエンジン | モデル | ドキュメント | OpenID | RESTfulSearch
動かなかった理由がよく分かりませんが
今日,もう一回イベントのところを動かしてみたら何の問題もなく動いていました。この前動かなかったのはなぜ?
というわけでeventのところはNucleusよりももうちょっと簡単に書けます。プラグインではevent~~というメソッドを用意するだけ。イベント一覧を返すメソッドがなくてもメソッド名を調べて登録します。呼び出し側もイベント名でnotifyするだけ。
プラグイン関係はあとactionが実行できるようになれば,一応一通り仕組みはできたことになります。
イベントの処理に取り掛かる
プラグイン関連の処理でイベントの部分を書いています。アクティブなプラグインからイベント情報を収集してデータベースに保存するところは完成。プラグインを呼び出すところまではできているのですが,その後がなぜか動かない。デバッガが使えないと不便です。雰囲気的には変数名が違っているといった単純なものではないかと思っているのですが。美しくない解決法
いろいろ調べるうちに,FoodynPlugin::Baseというクラス・オブジェクトが二つ存在していることが分かってきました。「なんてこった」って感じです。クラスはシングルトンじゃないんでしょうか。愚痴りたい気持ちはやまやまですが,とにかく動くようにすることが先決なので,なんとか解決法を考えました。
def self.list
@pluginlist ||= Array.new
if @pluginlist.size == 0
@pluginlist = Class.constants.select do |c|
cclass = c.constantize
if (cclass.is_a? Module) && (cclass.constants.include? 'Base')
klass = (c+'::Base').constantize
klass.ancestors.any? {|anc| anc.name == 'FoodynPlugin::Base'}
end
end
end
@pluginlist
end
def self.plugin_class plugin_name
if (pl = list.find {|p| p.downcase == plugin_name.downcase})
return (pl + '::Main').constantize
end
end
プラグイン・クラスの一覧を取るアルゴリズムですが,まずはClassレベルにおける定数を調べ,そこでModuleに属するものを選び,さらに,Module内の定数で「Base」を含んでいるものを選びます。この中から,Base付きのクラスの先祖のクラスを調べ,そこにFoodynPlugin::Baseがあるかどうかで判断しています。本来ここはクラスレベルで判定できるはずですが,上記の理由によって,わざわざ文字列に変換して比較しています。
美しくはありませんが,ともかくこれでスキン変数が動くようになりました。なんだかどっと疲れました。
わけが分からない
昨日の続きでまだうまくいきません。というかRuby(Rails?)の動きが理解できません。結局,プラグインの子クラスの一覧を取る機能は次のように実現しました。
def self.list
@@pluginlist ||= Array.new
if @@pluginlist.size == 0
@@pluginlist = Class.constants.select do |c|
cclass = c.constantize
if (cclass.is_a? Module) && (cclass.constants.include? 'Base')
FoodynPlugin::Base > (c+'::Base').constantize
end
end
end
@@pluginlist
end最初にpluginlistにArrayを入れてサイズを調べるといったまだるっこしいところをしているのは,またもやリロードのときにpluginlistが空になってしまうという問題に対応するためです。
中身はClassでの定数の配列を獲得し,それがModuleの名前かどうかを調べ,次にさらにそのモジュールでBaseという定数が定義されているかどうかを調べて,最後にFoodynPlugin::Baseと比較しています。
ところが,リロードすると最後の比較のときにtrueにならなきゃいけないところがなぜかnilに。理解できない。こまったなあ。
現象は分かったけど,どうしたものか
プラグインのスキン変数がうまく動いていなかった件,前進しましたが解決にいたらず。ちょっと困惑しています。プラグインのベースのクラスFoodynPlugin::Baseのクラスメソッドでは次のようなメソッドを用意しています。
def self.inherited subclass
@@pluginlist ||= Array.new
@@pluginlist.push subclass
end
def self.list
@@pluginlist ||= Array.new
end
def self.plugin_class plugin_name
if (pl = list.find {|p| p.pname.downcase == plugin_name.downcase})
return (pl.pname + '::Main').constantize
end
end
def self.pname
self.name =~ /^(.*)::/
return $1 # default name
end
ここで,inheritedというのは,そのクラスを親とする子クラスが定義されるときに自動的に呼ばれるメソッド。これを使って@@pluginlistというクラス変数にプラグインのベース・クラスの配列を入れています。スキンをパースするときに,エンジンが持っているparse_~~というルーチンとマッチしないと,FoodynPlugin::Base.plugin_classメソッドを使って,そのプラグインが存在しているかどうかを調べます(今思ったのですが,このメソッドは割と呼び出し回数が多いので,この中で名前を調べているのはだいぶ無駄な感じがします。名前自体を持たせる方がいいかも)。プラグインが存在すると,そのメイン部分のクラスを返します(ここもインスタンスを返すようにしたほうがいいかも)。そうするとパーサ側でこのプラグインのインスタンスに対してdo_skin_varメソッドを発行するわけです。
これで動くのですが,よく分からないのは,ページをリロードすると@@pluginlistがなくなってしまうこと。ですから1回目はスキン変数を表示しますが,リロードすると表示しません。クラスはロードしたままだけど,変数は消してしまうということでしょうか。そうだとするとinheritedに頼ることはできないので,このロジック自体考え直す必要がありそうです。ちょっといやらしい実装です。
プラグインの仕様/実装案メモ(4/23更新)
・Railsのプラグインとしてインストール可能にするSubversionのレポジトリ利用など,Railsの機能が利用できるから
・インストール後にFoodyn CMSの管理画面で利用のオンオフをする(デフォルトオフ)
プラグインはFoodynPlugin::Baseクラスの子クラスとして作りinit.rbでrequireしておく。このクラスはスタブであって,本体のコードは入らない。管理画面ではクラス一覧を獲得してそれを表示する。一覧の中でプラグイン・モデル(後述)に登録されていないものがあったら,自動的に登録する。
プラグインの本体はFoodynPlugin::Mainクラスの子クラスとして記述する。
管理画面で「オン」すると,インストールを実行する。具体的にはイベントの登録,スキーマの更新(プラグイン独自のテーブル,プラグイン・オプション)を行う。
「オフ」にするとイベントは削除。スキーマはそのまま。「アンインストール」はイベントおよびデータを削除。init.rbも削除(ここまでしないと一覧時にまた表示されてしまうから)。
「更新」はプラグインのコードを上書きしてから実行できる。イベントとスキーマ,プラグイン・モデル内のバージョン情報を書き換える。
・~~変数はNucleusと同様do_skin_varなどで実装する
Ruby的にはパーサを拡張するような実装も可能だがスキン・エンジン依存になるため,なるべくやらない。
・イベントはevent_~~などのメソッド実装で登録
管理画面上でプラグインをオンにするときに,そのプラグインのメソッドを舐めて調べる。
・PreItemを使うプラグイン用にPostUpdate系のイベントを用意
アイテム表示のたびにPreItemで走査するのは無駄なので決まりきった変換(例えばWiki系の入力補助など)はアイテム登録/更新時に変換する。変換前のデータを保持するためアイテムの本文用にフィールドを追加。
・プラグイン・オプション
プラグイン・オプションはブログ・オプションであればブログの拡張テーブル,アイテム・オプションであればアイテムの拡張テーブルに保存する。プラグイン本体のオプション用テーブルもある。
・独自管理画面
Admin用のクラスを別に持たせる。Nucleusのようにプラグインに直にリクエストが行くのではなく,どこかで受けてから渡すほうがいいだろう。
・アクション
これも一回システム側で受けてからプラグインを呼び出す方向。トラックバックのようなものはRESTのフォーマット追加でやりたいだろうと思うので,RESTのフォーマット追加やアクション追加ができるような仕組みを考えたい。
・スキーマの機能
プラグイン・オプションとプラグインの独自テーブルは,RailsのMigrationと同様の機能で更新する。
・必要なデータ・モデル
プラグイン管理用のモデルは,プラグイン名,バージョン,スキーマ・バージョン,オンオフ情報を登録
イベント管理用モデルはプラグインIDとイベントの名前を登録
プラグインオプションの説明モデルはプラグインID,オプション名,オプションの種類(ブログ,カテゴリー,…),データ・タイプ,デフォルト値,入力補助用データ
プラグインオプションのデータ・モデルは,元モデルごと(ブログ,カテゴリー,…)にテーブルを用意する。
プラグインはクラスで実装すべきかインスタンスにするべきか
プラグインのスキン変数の実装でちょっとバグっています。簡単に取れそうなのに,案外手間がかかってしまっています。その理由の一つはプラグインの実装をインスタンスでしているのに呼び出し側はクラスを呼んでしまっていたところにありました。
そもそもあるプラグインのインスタンスはシステムに一つしかない必要があります。そのため,いわゆるシングルトンのデザインパターンになります(RubyではSingletonモジュールをクラスに読み込むだけですが)。クラスはもともとシングルトンなので,プラグインをクラスで実装しても構わないということになります。
ところが,実際に書いてみると,クラスでプラグインを実装するとかなり分かりにくくなることが判明しました。具体的には,プラグインのベースクラス(プラグインを実行するかどうかにかかわらず,必ず読み込む部分)では,読み込まれたプラグインの一覧をクラス変数に確保するためにクラスメソッドを用意しています。それとプラグイン本体の部分とがごちゃごちゃになってしまうという分かりにくさがありました。
一方で,プラグインを呼び出すときは,プラグインのクラスに直接メソッドを送った方が話が早いので,ついそう書きたくなってしまうのです。結局プラグインの名前を抽出するメソッドをクラス・メソッドにもインスタンス・メソッドにも置くなど,なんだか汚いコードになってしまっています。少し設計を整理した方がいいのかもしれません。
デバッガーが~
ちょっとショック。RadRailsの現行バージョンでは,デバッガーが起動するときにエラーになります。1.0.2では直るようなのですが,まだ公開されていないみたいだし。早くしてくれ~。先は長い…
まずはこのブログをFoodynに切り替えることを目標にしているのですが,そのためにはMultipleCategoriesとTagEXからの移行部分やスキン変数も作らないといけないし,NP_Amazonの置き換えもしないといけません。タグやカテゴリーはNucleusと互換性がなくなる(データ移行はできるけど)ので,投稿も独自に持たないといけません。最初はタグやツリー型カテゴリー部分はRailsのプラグインを使うことを考えていましたが,タグとカテゴリーの変換機能などを考えるとやはり自前で作らないと結局いけないような気がして…メモ的にはロジックはできているのですが実装には少し時間がかかります。また,多分後回しにすると思うけどユーザー管理部分も手が付いていない。それになによりもまだ管理画面は皆無に近い状況です。管理画面はRESTfulな設計なので案外一つ動けばスムーズに行くのではないかとも期待しているのですが今の状況ではなんとも。
やらなきゃいけないことに対して,時間が足りなさすぎ… ちょっとしんどいなあ。
プラグイン用マイグレーション
新しいパソコンのCPUはAthlon 64 X Dual Core。安パソコンですが,デュアル・コアだけは譲れないと思って選びました。やっぱりこれまでとは全然違います。メモリも4Gバイト積んだので,Aptana Studioを起動する時間など,10倍近く早くなったような体感です。まだデバッグ機能は使っていませんが,これまでのようにデバッグモードに入るだけで何分も待つようなことはなくなるでしょう。やっぱり快適です。で,ようやく移行作業もほぼ落ち着き,コード書きに戻っていますが,このところ取り組んでいるのはプラグイン用のさまざまな機能。まだ完全ではないですが,マイグレーションのところは大分進みました。プラグインではそのプラグイン用のmigrationディレクトリに
class FpHelloworld::CreateOptions < FoodynPlugin::Migration
def self.up
add_string(:str, :desc=>'string to show', :default=>'Hello World')
add_string(:itemstr, :type=>'item', :desc=>'item string', :default=>'goodbye')
create_table :data do |t|
t.string :item_str
t.integer :item_id
end
end
def self.down
remove_string :str
remove_string :itemstr
drop_table :data
end
endといった形で書くだけ。これで適宜フィールドを作ったり,コラムを作ったりテーブルを作ったりします。いろいろなデータタイプに対応するなど,まだいくつか作業はありますが,基本的な仕組みは考えたとおりに動いています。