別なappのroutingやpartialを読み込めるsfAppChange

はじめまして。KAYAC技術部に留学中のshinoutです。

今日はsymfony1.4系で動作するプラグイン(もどき)、sfAppChangeについて紹介します。 symfonyでは異なるapp同士での共有はmodelのみに限られており、 たとえば 「backend処理だけど、front側のURLを取得したい」といったことや 「このパーツは別のappでも使いたい」といったことがあったと思います。 そんな時に便利になるのが今回作成したこのsfAppChangeです。

sfAppChangeでできること

  1. 別なappのroutingが使えて、URLを生成できる。
  2. 別なappのpartialが使える。

ダウンロード、インストール

ダウンロードは以下の2つのファイルをコピペしてください。

  • AppChange.class.php
<?php
class AppChange{
    
    private $app;
    private $app_old;
    private static $routings=array();

    private function __construct($app){
        $this->app = $app;
        $this->app_old = sfContext::getInstance()->getConfiguration()->getApplication();
        if( !sfContext::hasInstance($app)){
            sfContext::createInstance(ProjectConfiguration::getApplicationConfiguration(
                    $app,
                    sfContext::getInstance()->getConfiguration()->getEnvironment(),
                    sfContext::getInstance()->getConfiguration()->isDebug()
            ));
        }
        $this->start();
    }

    public static function getInstance($app){
        return new AppChange($app);
    }

    private function start(){
        sfContext::switchTo($this->app);
    }

    private function end(){
        sfContext::switchTo($this->app_old);
    }

    public function __call($method,$args){
        $method = '_'.$method;
        $this->start();
        $ret = call_user_func_array(array($this,$method),$args);
        $this->end();
        return $ret;
    }

    /* for the use of app routing */
    private function _getURL($format,$show_controller=true,$protocol=false){
        $env = sfContext::getInstance()->getConfiguration()->getEnvironment();
        if($env == 'prod'){
            $phpname = ($show_controller) ? '/'.$this->app.'.php' : '';
        }else{
            $phpname = '/'.$this->app.'_'.$env.'.php';
        }
        
        $url = $phpname;
        if($protocol===true){
            $protocol = 'http';
        }
        if($protocol){
            $url = $protocol.'://'.sfContext::getInstance()->getRequest()->getHost().$url;
        }

        if(!isset(self::$routings[$this->app])){
            $config = new sfRoutingConfigHandler();
            $routes = $config->evaluate(array(sfConfig::get('sf_apps_dir').'/'.$this->app.'/config/routing.yml'));
            $routing =new sfPatternRouting(new sfEventDispatcher());
            $routing->setRoutes($routes);
            self::$routings[$this->app] = $routing;
        }

        $routing_old =sfContext::getInstance()->getRouting();
        sfContext::getInstance()->set('routing',self::$routings[$this->app]);
        $ret = $url.url_for($format);
        sfContext::getInstance()->set('routing',$routing_old);
        return $ret;
    }

    /* use other app's partial */
    private function _getPartial($templateName,$parameter=array(),$app){

        // partial is in another module?
        if (false !== $sep = strpos($templateName, '/'))
        {
            $moduleName   = substr($templateName, 0, $sep);
            $templateName = substr($templateName, $sep + 1);
        }
        else
        {
            $moduleName = sfContext::getInstance()->getActionStack()->getLastEntry()->getModuleName();
        }
        $actionName = '_'.$templateName;

        $view = new sfPartialview(sfContext::getInstance(),$moduleName,$actionName,'');
        $view ->setPartialVars($parameter);
        return $view->render();
    }
}
  • AppChangeHelper.php
<?php
function url_for_app($format,$show_controller=true,$appname='frontend',$protocol=false){
    return AppChange::getInstance($appname)->getURL($format,$show_controller,$protocol);
}

function include_partial_app($name,$parameters=array(),$appname='frontend'){
    echo AppChange::getInstance($appname)->getPartial($name,$parameters);
}

これらを

  • /path/to/sfRoot/lib/AppChange.class.php
  • /path/to/sfRoot/lib/helper/AppChangeHelper.php

に配置しましょう。 autoload文化の発達したsymfonyですから、別な場所に配置してみてもきっとうまくいきます、好きに配置しちゃってください。 plugins/sfAppChangePlugin/lib/ 下に置いてenablePluginするのが一番かっこいいですかね。

使い方

  1. ヘルパーの呼び出し@view

    <?php use_helper('AppChange') ?>
    

  2. 別なappのURLを使いたいときは...

    <a href="<?php echo url_for_app('@rule1', true, 'frontend') ?>">
    frontend appのrouting使ったリンクです
    </a>
    

  3. 別なappのpartialを使いたいときは...

    <?php include_partial_app('modname/partialname', array('a'=>'b'), 'frontend') ?>
    

この二つだけです。link_to_app()やget_partial_app()などのインターフェイスは備えておりません。 欲しい方は改良してみてください。

詳しい使い方

  1. url_for_app($format, $show_controller = true, $appname = 'frontend', $protocol = 'http' )

  2. $format : url_for()の第一引数と全く同じものをいれてください。

  3. $show_controller : フロントコントローラ名を出力するかどうか、のフラグです。 ここみんな勘違いします。きっと。
    url_for_app('@path1', true, 'frontend');
    /
    prod環境では
    http://example.com/frontend.php/path1 と返します。
    dev環境では
    http://example.com/frontend_dev.php/path1 と返します。
    */
    url_for_app('@path1', false, 'frontend') ;
    /*
    prod環境では
    http://example.com/path1 と返します。
    dev環境では http://example.com/frontend_dev.php/path1 と返します。 (dev環境では$absolute=falseでもフロントコントローラ名が出力される!)
    /
  4. $appname : appの名前。 デフォルトがfrontendなのは僕がJobeetからsymfonyを学んだからです。 それ以上の意味はないので、都合に応じて書き変えてもいいです。
  5. $protocol : 絶対パスにするかどうかのフラグ。絶対パスの場合、プロトコルを指定。  trueの場合、httpに設定される。 例:

        url_for_app('@rule1', true, 'frontend', false) // /frontend.php/path1
        url_for_app('@rule1', true, 'frontend', 'http')// http://example.com/frontend.php/path1
        url_for_app('@rule1', true, 'frontend', true) // http://example.com/frontend.php/path1
    

  6. include_partial_app($name, $vars = array(), $appname = 'frontend' )

  7. $name: include_partial()の第一引数と同じですが、必ずmodule名から指定してください。 たとえばkayacというモジュールの中にあるshinoutというpartialを指定する場合、 'kayac/shinout'と指定してください。単に'shinout'だとどこの誰だかわからないので。

  8. $vars : include_partial()の第二引数と全く同じ。
  9. $appname : appの名前。 デフォルトはやっぱりfrontend。これ変えたいときは lib/helper/AppChangeHelper.php のfrontendという文字列を置換するだけでOK。

どうやって実現した?

sfContext::switchTo($appname) でコンテキストを変えています。 各種関数実行前にswitchToして、実行後に元のに戻す、という単純な原理です。

ただしroutingに関しては、それだけではうまくいかなかったので、加えてrouting.ymlも読ませています。 これはsfWebRequestの値がsfRoutingに影響を及ぼすというsymfonyの仕様にさかのぼるのですが、 興味があればまできいてください。

困ること

  • include_javascripts()やinclude_stylesheets()などは、読み込み先partialに書いてあった場合正常に動作しません。 それらをパース中のsfContextに登録されたもののみが表示されます。

最後に

symfonyはprotectedな文化でもあります。 不満ならextendsしてしまえばいいのです。 symfonyはなんでも自分で取り揃えて、記述するルールを規定してはいますが 独自になんでも拡張出来てしまうという柔軟性も備えています。 みんなでちょっと便利なツールを交換したりとかしたいですね。

カヤックではフレームワークを拡張する技術者を募集しています!