2014年8月31日日曜日

Phalconで強引なSubmodule

ここ最近、自称最速PHPフレームワークを謳っているphalconというのを
使っています。早い根拠はコアな部分をC言語で実装している為だそうです。
(SQLをログに出力すると意図しないクエリーをDBに流しているように見受けられ、遅くならないか? と心配になるが今回の趣旨とは異なるのでこれ以上触れません)

Laravelもチラ見したたけれども、最近のフレームワークはmoduleというくくりがちょっと 大きい単位になってしまっている気がしていまいち自分の意図と合わない。
個人的には関連するcontrollerやviewなどをある程度纏めた単位をsubmoduleとした構成にしたい。
かなり強引だけどnamespaceを使って実現してみる事に。
phalconのインストールからmodule構成まではphalconの公式ページを探せば見つかるので これまたここでは触れません。

先ずファイル構成はこんな感じ

├─app
│  ├─config
│  │      config.ini
│  │      loader.php
│  │      module.php
│  │      service.php                        # route処理を定義。
│  │                                         # ここでURLとmodule、name
│  │                                         # space、controller、acti
│  │                                         # onを関連付け
│  │
│  ├─frontend                               # frontend module
│  │  │  Module.php
│  │  │
│  │  ├─modules
│  │  │  ├─error                          # エラー処理用 submodule 
│  │  │  │  │                             # コントローラーのnamesp
│  │  │  │  │                             # aceは error
│  │  │  │  ├─controllers
│  │  │  │  │      IndexController.php
│  │  │  │  │      
│  │  │  │  ├─forms
│  │  │  │  └─views
│  │  │  │      └─index
│  │  │  │              app.volt
│  │  │  │              notfound.volt
│  │  │  │              system.volt
│  │  │  │              
│  │  │  └─index                          # index処理 submodule 
│  │  │      │                             # コントローラーのnam
│  │  │      │                             # espaceは index
│  │  │      ├─controllers
│  │  │      │      IndexController.php
│  │  │      │      
│  │  │      ├─forms
│  │  │      │      IndexForms.php
│  │  │      │      
│  │  │      ├─language
│  │  │      │  └─exprss
│  │  │      │          ja.php
│  │  │      │          
│  │  │      └─views                      # index module 専用のテ
│  │  │          │                         # ンプレートをここに作
│  │  │          │                         # 成する。
│  │  │          └─index
│  │  │                  index.volt
│  │  │                  
│  │  └─views
│  │      │  base.volt                      # 共通template
│  │      │  
│  │      └─layouts
│  │              main.volt                  # 共通layout
│  │              
│  ├─library
│  │  └─mvc
│  │          Module.php                     # module用親クラス。sab
│  │                                         # moduleのロード等
│  ├─logs
│  └─plugins
│          
├─cache
└─public
        .htaccess
        index.php
            

}}}
[config/service.php] phalconを実装する多くの場合でdependency injectionの各serviceをここで 定義していると思う。 ここでsubmodule構成の為にrouterサービスを定義。
/**
 * Specify routes for modules
 */
$di->set('router', function () {

    $router = new \Phalcon\Mvc\Router();

    // デフォルトモジュールを frontend に設定
    $router->setDefaultModule("frontend");

    /* frontend/modulesの直下にあるディレクトリ名をnamespaceとして定義
       ここで/foo/var/indexとアクセスがあった場合、以下が設定される
       module     = frontend
       namespace  = foo/Controllers
       controller = var
       action     = index
     */
    $modulesDir = APP_ROOT_PATH . DIRECTORY_SEPARATOR . 'frontend/modules';
    $dir = @opendir($modulesDir);
    if($dir){
        while($dir_name = readdir($dir)){
            // exclude [.],[..],[*.php]
            if($dir_name == '.' || $dir_name == '..'
               || strpos($dir_name, ".php") > -1){
                continue;
            }
            $router->add("/" . $dir_name . "/:controller/:action", array(
                    'module'     => 'frontend',
                    'namespace' => $dir_name . '\Controllers',
                    'controller' => 1,
                    'action' => 2,
            ));
        }
    }

    return $router;
});
もしModuleがfrontend以外にもあるのならそのディレクトリも同じように登録すれば 実現可能。


[library/mvc/Module.php] ここでは特に2つの事を行う。

1つはmodules直下を読み込んでいってnamespaceとしてロードする
    /**
     * Register a specific autoloader for the module
     */
    public function registerAutoloaders()
    {

        $loader = new \Phalcon\Loader();

        $namespaces = array();

        $modulesDir = APP_ROOT_PATH . DIRECTORY_SEPARATOR . 
                        $this->module_name .  '/modules';
        $dir = @opendir($modulesDir);
        if($dir){
            while($dir_name = readdir($dir)){
                // exclude [.],[..],[*.php]
                if($dir_name == '.' || $dir_name == '..'
                  || strpos($dir_name, ".php") > -1){
                    continue;
                }
                $namespaces[$dir_name . '\Controllers'] = $modulesDir . 
                   DIRECTORY_SEPARATOR . $dir_name . DIRECTORY_SEPARATOR .
                   'controllers';
                $namespaces[$dir_name . '\Forms'] = $modulesDir . 
                   DIRECTORY_SEPARATOR . $dir_name . DIRECTORY_SEPARATOR .
                   'forms';
            }
        }
        
        $loader->registerNamespaces($namespaces);
        
        $loader->register();
    }
公式サイトにもModuleのautoloadersの実装方法があるが、少し変化させて今回の目的に対応できるようにした。
modulesの直下にあるnamespace用ディレクトリの直下にcontrollersとformsというディレクトリを用意して、 それぞれnamespaceとして定義してローダーに登録している。
modules/indexを例にすると以下のようになる
  └─index
      ├─controllers                # namespaceはindex\Controllers
      │      IndexController.php    
      │      
      ├─forms                      # namespaceはindex\Forms
      │      IndexForms.php
      │      
他にこのnamespaceの単位に組み込みたいものがあれば(modelを追加したい場合はあるかもしれない) ここで定義すれば追加可能。

2つ目はviewの定義。各ファイルはnamespaceを使う方法に依って相互に認識しあえるようにできたが、 viewがちょっと異なる。controllerなどのphpファイルはnamespaceが利用できたが、viewのテンプレートは PHPのクラスで無い為にnamespaceに頼る事が出来ない。
(他の多くのフレームワークでもそうだと思うがテンプレートは通常のファイルとして扱っている)

Moduleでserviceを登録できる方法が提供されているので、viewをここで定義する。
レイアウトとメインのテンプレートは共通のディレクトリを使い、各アクションに依存した画面テンプレートは modules以下を使うようにしたい。

    public function registerServices($di)
    {
        $module_name = $this->module_name;

        //Registering the view component
        $di->set('view', function() use ($di, $module_name){
            $config = $di->get("conf");
            $view = new \Phalcon\Mvc\View();
            $view->setViewsDir(APP_ROOT_PATH . DIRECTORY_SEPARATOR . 
               $module_name . $config->application->viewsDir);

            // レイアウトディレクトリとメインは共通のディレクトリを示
            // すように定義
            $view->setLayoutsDir('../../../views/layouts/');
            $view->setMainView('../../../views/base');
            
            $view->registerEngines(array(
                    ".volt" => 'volt'
            ));

            // change view directory by namespace
            $eventsManager = new \Phalcon\Events\Manager();
            $eventsManager->attach('view', function($event, $view)
               use($module_name) {
                $dispatcher = $view->getDI()->getShared("dispatcher");
                $nName = $dispatcher->getNamespaceName()
                if ('beforeRender' == $event->getType()) {
                    // レンダリング前にviewのディレクトリを
                    // modules/namespace/views になるように設定。
                    $view->setViewsDir(APP_ROOT_PATH . DIRECTORY_SEPARATOR . 
                      $module_name . DIRECTORY_SEPARATOR . 'modules' . 
                      DIRECTORY_SEPARATOR . substr($nName, 0, 
                      strpos($nName, '\\', 0)) . DIRECTORY_SEPARATOR . 
                      'views' . DIRECTORY_SEPARATOR
                    );
                }
            });
            $view->setEventsManager($eventsManager);
            
            return $view;
            
        });

        $di->set('volt', function($view, $di) use ($module_name) {
        
                    $volt = new \Phalcon\Mvc\View\Engine\Volt($view, $di);
                
                    $volt->setOptions(array(
                            "compiledPath" => APP_ROOT_PATH
                               . "/../cache/volt/" . $module_name
                               . DIRECTORY_SEPARATOR
                    ));
            return $volt;
        }, true);
            
    }



[frontend/modules/index/IndexController.php]
/index/index/indexでアクセスされた場合にrouterによって紐付けされるように実装する。 注意する点はnamespace。routerとModuleで定義した内容とマッチするように定義する
namespace index\Controllers;

class IndexController extends \Phalcon\Mvc\Controller
{

    public function indexAction()
    {
    }
}


どうしてこうなったのか?

色々考えた結果、phalconのフローの関係で今回挙げた処理は前述の箇所となっり、このスタイルに 落ち着いた。デフォルトのModuleの単位をこのレベルに引き落とす方法も見当したが、エラーハンド リングがどうしても共通で行う方法にしっくりくる物が無かった。
routerの定義、namespaceの登録とロードはdispatchがされる前で、viewサービスはdispatch後に実行 されるようなのであのような形になった。


もっと良い実装方法 or 実はphalconの機能で実現する方法が存在するかもしれない。
今のところはこんな方法だろうか。

2014年8月22日金曜日

PHP5.3系のEOL

PHP5.3系が2014/08/15にリリースされた5.3.29をもってEOL(End of Life)と宣言されました。
今後の5.3系でリリースは予定されていないそうです。
またすみやかな 5.4 o r5.5 への移行が推奨されています。

php.netを読み返してみると2009/06/30に5.3.0がリリースされてからおよそ5年が経過していました。 なんかしみじみするなぁ。

詳しくはphp.netのアナウンスにて。