FuelPHP動作実験 - 実験くんソースをModulesに閉じ込めてモジュール分割してみる。

皆様こんにちは! FuelPHP Advent Calendar 2011 12日目 は私 @mataga が担当します。
昨日は @madmamor さんの 「FuelPHPでFacebookアプリを作ってみよう。実装編。 」でした。

本日のお題は『実験くんソースをModulesに閉じ込めモジュール分割してみる。』です。

  • FuelPHPのソース群を機能毎にモジュール分割してみる。
  • HMVC機構のおかげでコントローラAの中で別モジュールのコントローラーBが呼び出せるでよ。

のサワリ部分を体験をしていきます。
(ドキュメントの http://docs.fuelphp.com/general/modules.html , http://docs.fuelphp.com/general/hmvc.html が今回該当する箇所になります。)


※1 モジュール分割(Modules内へのディレクトリ構成移動)を行わなくても、
http://docs.fuelphp.com/general/controllers/base.html#/controller_in_subdir で解説されている様に

なんて芸当は出来るわけですが、私はModulesを愛してやまないのでここでは無視します。


I:HMVCってなんぞや?名前だけ無駄にカッコイイんだけど?

の詳細ついてはHMVC…"Hierarchical Model View Controller"でググってもらうとして(丸投げ)、

日本語超俺訳で言うと「機能毎にMVC構造をモジュール分割(フォルダ分け)して階層化してしまえ+モジュールの中から別のモジュールを呼べるようにしてしまえ。」って事です。
ソースのモジュール化を推し進めることでHMVC…"Hierarchical Model View Controller"をガシガシ使えまっせ。
※1の通りコントローラーの階層化なら別にモジュール分割しなくても実現出来るといえば出来るのですが、折角なら美しくモジュール分割したいじゃなイカ。そこの所は深く突っ込まないでください。(一部の機能だけモジュール化することも可能です。)

"ブロック構造で機能を切り出したツギハギ可能なCMSを作る場合に、どのようなディレクトリ構成にすればうまく分業して作業を進められるか?"…を想像してみるとわかりやすいかなと思います。建築で言うと2x4工法みたいな。机の中をビシっと間仕切りしたい性格な私にピッタリ!

モジュール化した時のディレクトリ構造の違い:

考えるより感じろって事でtreeコマンドの画面ダンプをペタリ。(以後画像A

(画像Aクリックで拡大可)
左側はサンプルソースstationwagon ( https://github.com/abdelm/stationwagon 、関連記事 http://d.hatena.ne.jp/dix3/20111119/1321684421)のソース群に、FuelPHP標準添付のapp/classes/controller/welcome.php(コントローラ)とapp/views/welcome/index.php(ビュー)を混ぜたもの。
右側はstationwagon由来のソースを app/modules(モジュール用ディレクトリ)/sbox(モジュール名:俺命名/以下に移動し、FuelPHP標準添付のwelcome画面をapp/modules/welcome/以下に移動したもの。

分割の仕方をここでは簡単のためソースの由来で分けていますが、実戦では機能で分ける事 (adminモジュール・cartモジュール・blogモジュール・usersモジュール・widgetモジュールetc)を想像すればモジュール化するメリットが想像しやすいかと。(cart,widgetモジュールはblogモジュール内から呼び出せる複雑な構成)


II:Modules化作業手順

というわけでモジュール化の素晴らしさをご理解頂けたとして、作業手順を書いていきます。
丁度実験君環境 stationwagonのソースが公開されているので( https://github.com/abdelm/stationwagon )これらのソースをModulesに移動する手順を追いながら動作実験してみましょう。

出来ればこちらの記事 等を参考に予めstationwagon環境を準備してひと通り触られた後の方が分かり良いと思います。

手順1:app/modules/sbox(任意のモジュール名)/以下に色んなディレクトリを作成する


root@star:/var/vhosts/sandbox/stationwagon/fuel/app/modules# tree -d
.
└── sbox(モジュール名:好きに命名
├── classes(必須)
│   ├── controller(コントローラー用ディレクトリ)
│   ├── model(モデル用ディレクトリ)
│   └── view(optional:ViewModels用ディレクトリ:今回使わないので無くても良い。というかモジュール化したあとViewModelsは使えなかった。要継続調査)
├── config(設定ファイル用ディレクトリ)
├── lang(言語ファイル用ディレクトリ)
└── views(ビューファイル用ディレクトリ)
├── articles (views内のサブディレクトリ。今回使用ソースstationwagonのviews構成でこうなっている為)
├── categories(上に同じ)
└── users(上に同じ)
ポイント:

  1. 上記の緑・赤・青の太字色つき部分のディレクトリ構成が基本ですが、使用しないディレクトリは不要です。(langやconfigディレクトリ等)
  2. モジュール名は解説用にsboxと名前をつけました。この場合URLはttp://example.com/sbox/hogehoge(アクション名) の様になります。(Routingを弄った場合はこの限りではない。)

また、今回はstationwagonのソース群をsboxという1モジュールにまるごと閉じ込めましたが、

  • articles機能をmodules/mod1(任意の名前)モジュール配下に
  • users機能をmodules/mod2モジュール配下に
  • categoriesをmodules/mod3モジュール配下に

という分割の仕方も大いにアリです(というかそっちが本流か)。

手順2:app/modules/sbox(任意のモジュール名)/以下にソースファイルを移動する

移動前・移動後のファイル構成の差異は上述した画像Aの通りです。移動後はこんな感じ

root@star:/var/vhosts/sandbox/stationwagon/fuel/app/modules/sbox# tree
.
├── classes
│   ├── controller
│   │   ├── articles.php
│   │   ├── categories.php
│   │   ├── common.php
│   │   └── users.php
│   ├── model
│   │   ├── article.php
│   │   └── category.php
│   └── view
├── config
├── lang
└── views
├── 404.php
├── articles
│   ├── add.php
│   ├── edit.php
│   └── index.php
├── categories
│   ├── add.php
│   ├── edit.php
│   └── index.php
├── template.php
└── users
├── index.php
├── login.php
└── signup.php
特にコメント無し。とりあえず移動すればヨロシ。
(404ページのビューや、ビューの外枠テンプレートのtemplate.phpはモジュール外がいいとかそのへんの細かい調整はあとで考えましょう。)

手順3:app/config/config.php 内でmodule_pathsの指定をおこなう

'module_paths' => array(
        APPPATH.'modules'.DS
),

160-180行目付近にあります。コメントアウト//を外して有効にすることでapp/modules/ がモジュール用ディレクトリとして使えるようになります。
(ドキュメントの http://docs.fuelphp.com/general/modules.html#/module_config

手順4:移動したソース内のソースを一部書き換えていく。

ディレクトリを移動したことで各ソース内の一部書き換えが必要となります。
(ドキュメントの http://docs.fuelphp.com/general/modules.html#/module_namespace , http://docs.fuelphp.com/general/modules.html#/static_calls
基本単純作業ですがnamespaceの絡みもあり、CodeIgniterのモジュール化(ファイルを移動すればほぼ作業完了)の時と多少勝手が違うのでちょいと悩みました。公式のドキュメント整備が進んでくるとよりよいチュートリアルが載ってくると思います。

(実際に書き換えた箇所はstationwagonのソースの数を見てもらえればわかる通り結構多いので、変更パターンのみ以下に記述します。)
ソース書き換えの例

実例A とあるコントローラー

root@star:/var/vhosts/sandbox/stationwagon# hg  diff -r 61a788776698 fuel/app/modules/sbox/classes/controller/common.php
diff -r 61a788776698 fuel/app/modules/sbox/classes/controller/common.php
--- a/fuel/app/modules/sbox/classes/controller/common.php       Tue Dec 06 18:30:18 2011 +0900
+++ b/fuel/app/modules/sbox/classes/controller/common.php       Sat Dec 10 19:26:44 2011 +0900
@@ -1,6 +1,6 @@
 <?php
-
-class Controller_Common extends Controller_Template {
+namespace Sbox;
+class Controller_Common extends \Controller_Template {

        public function before()
        {
@@ -16,20 +16,21 @@
         }

         // Check user access
-        $access = Auth::has_access(array(
+        $access = \Auth::has_access(array(
             $this->request->controller,
             $this->request->action
         ));

                if ($access != true)
         {
-            Response::redirect('users/login');
+
+           \Response::redirect('sbox/users/login');
         }
         else
         {
-            if (Auth::check())
+            if (\Auth::check())
                    {
-                       $this->user_id = Auth::instance()->get_user_id();
+                       $this->user_id = \Auth::instance()->get_user_id();
                        $this->user_id = $this->user_id[1];
                    }
         }
@@ -42,7 +43,7 @@

                // Set a HTTP 404 output header
                $this->response->status = 404;
-               $this->template->content = View::factory('404', $data);
+               $this->template->content = \View::factory('404', $data);
        }
 }

実例B とあるモデル

root@star:/var/vhosts/sandbox/stationwagon# hg  diff -r 61a788776698 fuel/app/modules/sbox/classes/model/article.php
diff -r 61a788776698 fuel/app/modules/sbox/classes/model/article.php
--- a/fuel/app/modules/sbox/classes/model/article.php   Tue Dec 06 18:30:18 2011 +0900
+++ b/fuel/app/modules/sbox/classes/model/article.php   Sat Dec 10 20:58:44 2011 +0900
@@ -1,16 +1,16 @@
 <?php
+namespace Sbox;
+class Model_Article extends \Orm\Model {

-class Model_Article extends Orm\Model {

(中略)

@@ -22,7 +22,7 @@

     public static function validate($factory)
     {
-        $val = Validation::factory($factory);
+        $val = \Validation::factory($factory);

         $val->add('category_id', 'Category');

(上記実例Aのstationwagonの共通コントローラーcommon.phpは、fuel/core/classes/controllerのController_Templateを継承したController_Commonというクラスで、このモジュール内の他のコントローラー(articles.phpなど)はcommon.phpを継承しています。common.phpにはコントローラー内各アクション(URL)呼び出し直前の共通処理before()や、404ページ表示用アクション action_404() が書かれています。 旧:factory()→新:forge()へのメソッド名書き換えは今回の主題からは外れるのでそのまま)
それでは要変更箇所を書いていきます。

変更1 : コントローラ・モデルファイルではモジュール名(=ディレクトリ名)の namespace を必ず追加する
コントローラでは、 namespace モジュール名;の宣言を行う。(ここでは namespace Sbox;)

参考: http://docs.fuelphp.com/general/modules.html#/module_namespace
名前空間命名規則 PSR-0云々についての深い議論については http://fuelphp.com/blog/2011/04/classnames-autoloading-namespaces 等もご参考ください、今回はFuelPHPの仕様についての私見は省略します。core/classes/autoloader.php のalias_to_namespace($class, $namespace = '')のclass_alias($class, $root_class);周りでglobal namespaceにエイリアスを作っているようなのでスーパーハカーはこの辺でも弄って上手いことやればお好みの形になるかと。

変更2 : コントローラ・モデル内でコアのクラスを呼び出しているところの頭に\を追加
ポイント1でモジュールのnamespaceを宣言したことに伴い、ソース内微調整を行ないます。
上記実例Aと実例Bでは

class Controller_Common extends Controller_Template {

class Controller_Common extends \Controller_Template {
class Model_Article extends Orm\Model {

class Model_Article extends \Orm\Model {
Auth::

\Auth::
Response::

\Response::
View::

\View::
Validation::

\Validation::
と至る所で\が付けられている事が解るかと思います。

また、今回のサンプルソースでは該当箇所は存在しませんがoilコマンド等で自動生成されたモデルの場合モデルのクラス名が

namespace Model;
class Contact extends Orm\Model {

の様にnamespace Model; 、class モデル名 extends…となっている場合があります。(というかこっちのほうがマニュアルに載っている書き方)(参考 http://docs.fuelphp.com/general/models.html
この場合には


//namespace Model;

namespace Sbox\Model;
//class Contact extends Orm\Model {

class Contact extends \Orm\Model {
という様な書き換えが必要となります。

尚、コントローラから呼び出された先のViewファイル内(例:sbox/views/articles/edit.php)などで
<?php echo Form::input( と記述されている箇所を\Form::input(にする必要はありません。

変更3 : コントローラ内やビューファイル内で\Response::redirect('飛び先');や、Html::anchor('飛び先');としている箇所の飛び先をモジュール名付きの適切なパスに書き換える
例えば categories/add だったリンク先は今回モジュールsboxの下にソース群を設置したので、sbox/categories/add への書き換えが必要となります。

変更4 : ユーザー認証にSimpleAuthを使っている場合のconfig/simpleauth.phpのroles設定書き換え
stationwagonのソースではユーザー認証にFuelPHP標準搭載の SimpleAuth - http://docs.fuelphp.com/packages/auth/simpleauth/intro.html (2011/12/10現在公式のマニュアルがまだイントロ部分しか無い)を使っているのですがここのroles設定にもモジュール名の追加が必要となります。

root@star:/var/vhosts/sandbox/stationwagon# diff  ../stationwagon-nohmvc/fuel/app/config/simpleauth.php  fuel/app/config/simpleauth.php
59c59
<             '\Controller_Users' => array(
---
>             '\Sbox\Controller_Users' => array(
64c64
<             '\Controller_Users'  => array(
---
>             '\Sbox\Controller_Users'  => array(
71c71
<             '\Controller_Users'  => array(
---
>             '\Sbox\Controller_Users'  => array(
74c74
<             '\Controller_Articles'   => array(
---
>             '\Sbox\Controller_Articles'   => array(
80c80
<             '\Controller_Categories' => array(
---
>             '\Sbox\Controller_Categories' => array(

…というわけで説明を書くとタイヘンなんですがやることは単純で、

  1. コントローラー、モデルには namespace モジュール名をつける。
  2. コントローラー、モデルのファイル内でfuel/core/classes/配下のコアのクラスを呼び出している部分には\を頭につける
  3. リダイレクト先やリンク先が変わるのでモジュール名追記分を考慮して訂正する。
  4. 認証周りなどの設定もモジュール名追記分を考慮して書き換える

という所がこの作業のキモとなります。


III:モジュール化済のwelcomeモジュールからsboxモジュールのlogin画面を呼び出してみる

ここまで書くのに疲れたので後は惰性の画面ショットで…

welcome/action_index (ttp://example.com/welcome/) にてsbox/users/login/ をモジュールとして呼び出しているソース部分




welcome画面にTumblrっぽいログインブロック(sbox/users/login/)が出現!




(普通のwelcome画面はこんな感じ)




(sboxモジュール単体でのsbox/users/login/画面はこんな感じ)


HMVCを使う雰囲気がなんとなくわかりますでしょうか?

(※このソースで実験用にはなんとなく別モジュールのコントローラーを呼び出して動かしているだけですので、実際の実装では呼び出される側・呼び出す側にちゃんとしたエラー処理&モジュール単体で呼ばれないようにする方策等が必要になるかと思います。)
stationwagonはFuelPHPの雰囲気を味わう練習ソースレベルなので実務レベルに持って行くにはもっと例外処理・実装の追加が必要です。あくまでも安全なローカル環境の下で試す砂場・実験場として考えてください。くどいですが再度書いておきます。


IV:今回モジュール化実験で使ったソース

改造前ソースについては、https://github.com/abdelm/stationwagon より取得してください。
改造後ソースについてはstationwagonのライセンスが明記されていない為公開は控えておきます。

一度書き換えて動くようになるまで試してみればFuelPHPの構造が色々見えてくるので、この機会にモジュール化にチャレンジしてみては如何でしょう?

(モジュール化作業途中でAuth::を\Auth::にし忘れて、そんなクラス見つからねーヨと怒られてる図)



モジュール化済みでのソース実装は現在 http://contact.pizw.net/http://contact.pizw.net/contact/srcwindow にてお問い合わせフォーム作成実験をゼロからジワジワ進行中です。そちらの方でなんとなく雰囲気を掴んでもらえればと思います。


明日は @ounziw さんの「PHP5.3 の名前空間入門」になります。よろしくおねがいします。

ではでは!