なぜbaserCMS?

遅ればせながら始まりましたbaserCMS Advent Calender 2012の1日目を担当させて頂く@kanjihtmtです。(12/1ではないですが)

僕は最近baserCMSのコミュニティに参加したばかりですので、いきなりAdvent Calenderに参加しても「誰?」みたいに思われるでしょうから、どんな活動をしているかをまずは紹介させて下さい。

10月3日にありました「PHP Matsuri 2012」に参加させて頂き、@ryuringさんや@itm_kiyoさんと徹夜でbasercamp(CakePHP2対応版-baserCMS3の構築)を行いました。(眠たかったー) その後も引き続き空いた時間を見つけてbasercampをしております。 baserCMS3ですが年内を目処にしておりましたが、私達3人とも本業が忙しく予定どうりに進んでおりません。なるべく早くリリースが出来るように頑張りたいと思ってはおりますので、皆様は安定している2系を使いつつ温かくbaserCMS3を見守ってやって下さい。よろしくお願いします。

前置きはこれぐらいにして始めます。

私とbaserCMS

僕がbaserCMSを使い始めたときはまだ1系でしたが、日本で導入実績が多く人気のあるフレームワークを採用した本格的なCMSはありそうでなかなか無く(MVCでないと気持ち悪くなる病の僕は)baserCMSを見つけた時は「きたコレ!」と即採用し、かなりハマリながらbaserCMSでなんとかサイトを作った覚えがあります。(最初のサイトは今思うとバットノウハウ満載でした)

PHP Matsuri 2012」の発表(Ustreamにありますのでご興味があればどうぞ)でも少し説明しましたが、僕が気に入っているbaserCMSの特徴はappフォルダにbaserのソースをコピーして編集すればそちらが優先されて、コアに手を入れずカスタマイズが出来るという点になります。これはデザイナーの方などは「何がうれしい?」と思われるかもしれませんが、プログラマー的には重要です。(ですよね?)
例えば、EC-CUBEなどではカスタマイズ可能なサブクラスが用意されてますが、その手法はカスタマイズする側からすると正直面倒くさいですし、エレガント感がないように感じます。baserCMSの作りですとbaserのコアをバージョンアップする際に、独自にカスタマイズした部分ときっちりコアが別れてます(設定ファイルやキャッシュなど一部は除く)ので差分を取ったり更新(アップロード)しないフォルダやファイルを間引いたりすることなく、まんま上書きできるなどが良いところになります。(もちろんバージョンアップによって動かなくなるかどうかテストする必要はあります)

baserCMSを始めた時に気に入らないところが1つありました。実はbaserCMSをやり始めた時にはCakePHPの経験が無かったため、CakePHPの書籍などをひと通り読んだりして知識をつけた後で本格的にbaserCMSを始めました。そういった過程を経て改めてbaserCMSのフォルダ構成なんかを見ると「なんだこりゃ?」「そもそもなんでこんな作りになってんの?」みたいな教科書どうりでない特徴に不満が出てきました。この先入観を持った人は僕だけじゃないはずです。(CakePHP辞典やKtai Libraryで有名なMASA-Pさんもブログで同じようなことを書かれてますし)
もし僕がbaserCMSのオリジナル開発者で、スクラッチからbaserCMSを作ったとしたら、プラグイン化してそれをCakePHPから読み込んで(RouterなどでURLを変えたりしつつ)CMS機能を実現するような構成にしていたことでしょう。こうしなかった良さは後で説明します。

正直、「うーん。外国製だけどやっぱCroogoにしようかな?」と浮気心が芽生えだしたところに、ふとしたきっかけでbaserCMSのソースを読んでみる気になりました。なんでこんな構成になっていてどんな変態的なことをやってbaserよりappの方を先読みさせているのか?と言ったことを知りたくなったのです。

baserCMSのブートストラップ

CakePHPではアプリケーションの初期化処理をフロントコントローラと呼ばれるindex.phpファイルからbootstrap.phpファイルというファイルを読み込むことで実現しています。このbootstrapファイルは主にConfigure系の設定とか定数群(paths.phpに外出ししてますが)などを記述します。このファイルを読み込みDispacherクラスが起動されるまでの処理を追うことでbaserCMSのイケている特徴の秘密が分かるようになります。

CakePHPのcoreのbootstrapをincludeした後に、Configureクラスのインスタンスを生成します。正確に言えばこのクラスはデザインパターン的に言えばシングルトンになっていますのでオブジェクトが生成されていない場合のみインスタンスが生成されます。

Configure::getInstance();

インスタンス生成時に密かにプライベートメソッドをコールします。このメソッドがキモです。

<?php
 624     function __loadBootstrap($boot) {
 625         $modelPaths = $behaviorPaths = $controllerPaths = $componentPaths = $viewPaths = $helperPaths = $pluginPaths = $vendorPaths = $localePaths = $shellPaths = null;
 〜 省略 〜
 671             if (!include(CONFIGS . 'bootstrap.php')) {
 672                 trigger_error(sprintf(__("Can't find application bootstrap file. Please create %sbootstrap.php, and make sure it is readable by PHP.", true), CONFIGS), E_USER_ERROR);
 673             }
 674
 675             Configure::buildPaths(compact(
 676                 'modelPaths', 'viewPaths', 'controllerPaths', 'helperPaths', 'componentPaths',
 677                 'behaviorPaths', 'pluginPaths', 'vendorPaths', 'localePaths', 'shellPaths'
 678             ));
 679         }

Configure::buildPathsによってモデルやコントローラ等のパスを設定しているんですね。baserはこの仕組みになんらかの方法でフックしてパスを追加しないといけません。

それでは、671行目のincludeに注目しましょう!app/config/bootstrap.phpを読みだしています。そちらのファイルでbaser独自のbootstrap(baser/config/bootstrap.php)を読み出しにいきます。

<?php
 33 /**
 34  * Baserパス追加
 35  */
 36     $modelPaths[] = BASER_MODELS;
 37     $behaviorPaths[] = BASER_BEHAVIORS;
 38     $controllerPaths[] = BASER_CONTROLLERS;
 39     $componentPaths[] = BASER_COMPONENTS;
 40     $viewPaths[] = BASER_VIEWS;
 41     $viewPaths[] = WWW_ROOT;
 42     $helperPaths[] = BASER_HELPERS;
 43     $pluginPaths[] = BASER_PLUGINS;
 44     // Rewriteモジュールなしの場合、/index.php/css/style.css 等ではCSSファイルが読み込まれず、
 45     // $html->css / $javascript->link 等では、/app/webroot/css/style.css というURLが生成される。
 46     // 上記理由により以下のとおり変更
 47     // ・HelperのwebrootメソッドをRouter::urlでパス解決をするように変更し、/index.php/css/style.css というURLを生成させる。
 48     // ・走査URLをvendorsだけではなく、app/webroot内も追加
 49     $vendorPaths[] = WWW_ROOT;
 50     $vendorPaths[] = BASER_VENDORS;
 51     $localePaths[] = BASER_LOCALES;
288 /**
289  * テーマヘルパーのパスを追加する
290  */
291     $themePath = WWW_ROOT.'themed'.DS.Configure::read('BcSite.theme').DS;
292     $helperPaths[] = $themePath.'helpers';

内容的にはbaserのpaths.phpで定義した定数を埋め込んでますがこれだけです。このまま先ほどのCakePHPのコアのConfigure::buildPathsメソッドが実行されればCakePHPのデフォルトパスにbaserのパスがマージされます。

ではパスの内部がどうなっているのかダンプしてみましょう。

<?php
[modelPaths] => Array
 (
     [0] => /your/to/path/docroot/app/models/
     [1] => /your/to/path/docroot/baser/models/
     [2] => /your/to/path/docroot/app/
     [3] => /your/to/path/docroot/cake/libs/model/
 )
[behaviorPaths] => Array
(
    [0] => /your/to/path/docroot/app/models/behaviors/
    [1] => /your/to/path/docroot/baser/models/behaviors/
    [2] => /your/to/path/docroot/cake/libs/model/behaviors/
)
[controllerPaths] => Array
(
    [0] => /your/to/path/docroot/app/controllers/
    [1] => /your/to/path/docroot/baser/controllers/
    [2] => /your/to/path/docroot/app/
    [3] => /your/to/path/docroot/cake/libs/controller/
)
[componentPaths] => Array
(
    [0] => /your/to/path/docroot/app/controllers/components/
    [1] => /your/to/path/docroot/baser/controllers/components/
    [2] => /your/to/path/docroot/cake/libs/controller/components/
)
[viewPaths] => Array
(
    [0] => /your/to/path/docroot/app/views/
    [1] => /your/to/path/docroot/baser/views/
    [2] => /your/to/path/docroot/
    [3] => /your/to/path/docroot/cake/libs/view/
)
[helperPaths] => Array
(
    [0] => /your/to/path/docroot/app/views/helpers/
    [1] => /your/to/path/docroot/baser/views/helpers/
    [2] => /your/to/path/docroot/themed/demo/helpers
    [3] => /your/to/path/docroot/app/
    [4] => /your/to/path/docroot/cake/libs/view/helpers/
)
[pluginPaths] => Array
(
    [0] => /your/to/path/docroot/app/plugins/
    [1] => /your/to/path/docroot/baser/plugins/
)
[vendorPaths] => Array
(
    [0] => /your/to/path/docroot/app/vendors/
    [1] => /your/to/path/docroot/vendors/
    [2] => /your/to/path/docroot/
    [3] => /your/to/path/docroot/baser/vendors/
)
[localePaths] => Array
(
    [0] => /your/to/path/docroot/app/locale/
    [1] => /your/to/path/docroot/baser/locale/
)
[shellPaths] => Array
(
    [0] => /your/to/path/docroot/cake/console/libs/
)

配列のインデックスが優先度の高い順(app, baser, cake)になっているのが分かりましたでしょうか。実は、baserCMSはめちゃくちゃ変態なことをやっている訳ではなく、かなりCakePHPの機能をそのまま素直に使って独自性を出しているのが分かります。
(baserCMS3ではCakePHPAPIが大きく変わってますがApp::buildを使って同じようなことを実現します)

以上の仕組みによりbaserCMSのファイルよりappが優先するようになっています。正直この仕組みを知った時からbaserCMSがもっと好きになりました。「あの人イケてるんだけど、変態らしいわよ。ありえなーい」だったのが実は誠実だったことが分かったら、そら「惚れてまうやろー!」(チャンカワイ) って感じですね。あれ?なんやそりゃ?って感じですか?(笑)

Beyond CMS (CMSを超えてゆけ!)

baserよりappを先読みする仕組みは分かったが、なぜbaserCMSの構成がこうなっているのか?という疑問を解決していないじゃないか!と思ったあなたは鋭いです。

今の構成にしたのはオリジナル開発者の@ryuringさんの最初から狙った戦略的なものなのか、色々試していった結果、偶然現在の形に辿り着いてしまったのか分かりませんがいずれにせよ副産物を生み出してしまったのです。baserCMSと聞いてCMSに反応するのではなくbaserの方に注目して欲しいです。そもそも何でbaserなんて名前付けてるのでしょう?

みんなbaserCMSの真価に気付こうよ! baserCMSというのはその名のとおりCMSではありますが実はプラッガプルなプラットフォームです。いやコンテナと言ってもいいかもしれません。図にすると下記のようになります。

図にしてみることで「羊の皮を被った狼」っぷりが分かりますでしょうか?

CMSに独自に機能を追加したりカスタマイズをしたいのでしたら、appにどんどんコピーしたり新たな機能用のファイルを追加していって下さい。それ以外にもプラグインでも拡張性があります。
例えば、baserCMSの独自のプラグインを作ったり誰かが作ったプラグインgithubなどから見つけたとします。ある案件にはそれはベストマッチでしたが別の案件では似て非なる機能が必要になってきました。さあどうしますでしょうか?baserのプラグインをコピーしてappに入れるだけです。プラグインも大丈夫です。コアのプラグインに手はいれずにapp内のコードだけの修正になります。githubでFolkしてどんどんカスタマイズしていっても構いません。

実は個人的にbaserCMS3ではこの流れを活性化しようと考えています。たくさんのプラグインがあってそれを独自に拡張したりされたり、コーポレートサイトだけに留まらないことが容易に出来るようになる。強いていえばWordpressのようなことをもっとソリッドな方法や規模でやれるというか道のりは長いでしょうが今後はそういう風になっていけばよいと思っていますし、がんばりたいです。

最後に

何年も開発の現場にいますがソフトウェア開発というのは、再利用可能なモジュール群によってプログラミングは必要無くなってくるなどとMDAとか時代によって言葉を変えながら昔から言われ続けていますが全然そんな兆しはなく本当に泥臭いものです。ある程度フレームワークやライブラリーなどにより開発が楽になったり高品質なものを作ることはできるようになっていますが、果たしてプログラマーの作業自体は楽になっているのでしょうか?

これまでそういった厳しい現実と向かいつつ、拡張性や柔軟性がありながらも統一感のあるやり方でサイトを構築したり生産性を上げる方法を模索していました。baserCMSは同じくプラッガブルな環境のXOOPSなどとは違ってフレームワークを使って汎用性を出しつつ尚且つシンプルである1つの理想形を作っていると思っています。ぜひ新たに違った視点からbaserCMSを見なおしてみることをお勧めいたします。

ここまで読んで頂きありがとうございました。