relationのないテーブルをleft joinする。

最近symfony絡みのバッドノウハウばっかりだな・・・


海外サイトでもソリューションが見つからなかったけど良く考えたらできたのでメモ。
keyword: doctrine left join without relation


symfony/doctrineでleftJoinを使うには、テーブル間にrelationが必要になる。
しかし、relationをschema.ymlやDoctrine_Record::setUpで定義してしまうと、

symfony doctrine:build --all

などとしたときに外部キー制約が設定されてしまう。


今回は、集計したログの値を基にorderByを発行したかった。
あくまでログなので外部キー制約は付けたくない。
でもleft joinはしたい。

そんなときは

実行コンテキストでrelationしちゃえばいいじゃなーい!

// in some Doctrine_Table class
$this->bind(
  array('SomeLog as SomeLogs', array(
    'local'   => 'id',
    'foreign' => 'record_id',
  )),
  Doctrine_Relation::MANY // means hasMany
);

$this->createQuery('r')->select('r.*')
                       ->leftJoin('r.SomeLogs as l')
                       ->orderBy('SUM(l.weight) DESC')
                       ->execute();

これでschemaを汚染せず、リクエストコンテキストでのみrelationが付く。

というわけで

(’・ω・`)はいはいBKBK

symfonyで複数のアプリケーションを作るとき。

symfonyはモジュールのネストができないため、
機能ごとに分類したくなったときはアプリケーション単位で分けることになる。


そのときに困るのが、「URLがダサい」こと。
メインのアプリケーションを除いて、「front.php」とかフロントコントローラ名がURLに入ってくる。


(’・ω・)何がユーザーフレンドリーなroutingなんだか


symfonyのダメさ加減を嘆いても仕方ないのでなんとか対策してみる。

mod_rewrite

まずは恒例の黒魔術。

/front.php/module/action

でアクセスしなければいけないので、

RewriteCond %{REQUEST_URI} ^/front/
RewriteRule ^([^/]+)/(.*)$ $1.php/$2 [QSA,L]

とする。


これで

/front/module/action

でアクセスできる。

genUrl

これでぱっと見完成したように見えるが、実際にアクセスしてみると
ページ内のリンク先に「front.php」って書いてある。
これはsymfony内部でURLを生成する際、no_script_nameでない限りはフロントコントローラ名を付けるため。


これも気にくわないのでオーバーライドする。

class myFrontWebController extends sfFrontWebController
{
  public function genUrl($parameters = array(), $absolute = false)
  {
    $url = parent::genUrl($parameters, $absolute);
    
    $request = sfContext::getInstance()->getRequest();
    if (preg_match('/\.php/', $request->getHttpHeader('REQUEST_URI', null))) return $url;
    
    return preg_replace('/\.php/', '', $url);
  }
}


一応フロントコントローラ明示してアクセスした場合には書き換えないようにした。
dev, test環境は一切考慮してないぜ。


これをfactories.ymlに追加して、完了。

おわりに

PHPって奴はどうしてこうも・・・

UbuntuからWindows共有フォルダを使う。

忘れないようにメモ。

samba, smbfsを入れた状態で。

共有サーバーのホスト:server
共有サービス名:service
ユーザー名:hoge
パスワード:fuga
ローカルのマウントポイント:/mnt/smb

として、

$ sudo mount -t cifs -o username=hoge,password=fuga,iocharset=utf8 //server/service /mnt/smb

でおk。

sfFormのヘンな実装。

フォームの描画フォーマットをカスタムしようとしてほげほげやったら
原因の掴みづらいエラーが出まくって疲れた。


やったことといえば

sfWidgetFormSchemaFormatterを継承したフォーマッターを作る(フォーマット文字列をspanにしただけ)

sfFormを継承したクラスを作って
    sfWidgetFormSchema->addFormFormatter
    sfWidgetFormSchema->setDefaultFormFormatterName

actionでsfFormを継承したクラスを使う

って感じ。

ハマったこと1個目

actionでsfForm->setWidgets(複数形の方)を使うと、
内部的にsfWidgetFormSchemaのインスタンスを作り直してる。
これがcloneじゃなくてnewなので、sfWidgetFormSchema->addFormFormatterした内容が失われる。


なのでsfFormを継承したクラスでsetWidgetsを↓でオーバーライド。

public function setWidgets(array $widgets)
{
  parent::setWidgets($widgets);
  
  $this->widgetSchema->addFormFormatter('foo', $fooFormatter);
  
  return $this;
}

$fooFormatterはsetupで作って保持しといたインスタンス

ハマったこと2個目

1個目を解決したら今度はsfForm->setWidgetsで入れたwidgetがないとか言われた。
どうもsfWidgetFormSchemaFormatterはsfWidgetFormSchemaと
インスタンスレベルで相互参照してるらしい。


フォーマッターがsfForm->setWidgets前のsfWidgetFormSchemaを参照してるせいで
widgetがないことになってしまっていたらしい。


オーバーライドしたsetWidgetsの実装を↓に変更。

public function setWidgets(array $widgets)
{
  parent::setWidgets($widgets);
  
  $fooFormatter->setWidgetSchema($this->widgetSchema); // この行追加
  $this->widgetSchema->addFormFormatter('foo', $fooFormatter);
  
  return $this;
}

これでおk。

あー疲れた。

なぜかよくやるミス。

var foo = new foo();

先にfooが宣言されるから
「foo is not a constructor.」で怒られる。
そもそもクラス定義壊しちゃダメだろww

var foo = new Foo();

クラス名と(文脈的にはレキシカルな)変数名の命名規則を守りましょうって話。

やったー、require.jsできたよー!

論よりコード。

(function() {
    if (!window.XMLHttpRequest) {
        alert('no xhr');
        
        XMLHttpRequest = function() {
            try {
                return new ActiveXObject("Msxml2.XMLHTTP.6.0");
            } catch(e) {}
            try {
                return new ActiveXObject("Msxml2.XMLHTTP.3.0");
            } catch(e) {}
            try {
                return new ActiveXObject("Msxml2.XMLHTTP");
            } catch(e) {}
            try {
                return new ActiveXObject("Microsoft.XMLHTTP");
            } catch(e) {}
            
            throw new Error("no valid xhr");
        };
    }
})();

var require = function() {
    if (arguments.length < 1) return true;
    
    var scripts = document.getElementsByTagName('script');
    
    for (var i = 0; i < scripts.length; i++) {
        require.loaded[scripts[i].src] = 1;
    }
    
    var self = arguments.callee;
    var xhr = new XMLHttpRequest();
    
    for (var i = 0; i < arguments.length; i++) {
        var src = arguments[i];
        
        if (src.match(/^\w+:\/\//) === null) {
            if (src.indexOf("/") === 0) {
                src = self.origin + src;
            } else {
                src = self.origin + self.base + self.path + "/" + src;
            }
        }
        
        if (self.loaded[src] || self.loading[src]) continue;
        
        self.loading[src] = 1;
        
        var t = null;
        if (self.timeout > 0) {
            t = setInterval(function() {
                clearInterval(t);
                
                xhr.abort();
                self.loading[src] = undefined;
                
                throw new Error(src + " timeout");
            }, self.timeout);
        }
        
        xhr.open('GET', src, false);
        xhr.send();
        
        if (t !== null) clearInterval(t);
        
        if (xhr.status !== 200) throw new Error(src + " " + xhr.statusText);
        
        window.eval(xhr.responseText);
        
        arguments.callee.loaded[src] = 1;
        self.loading[src] = undefined;
        
        xhr.abort();
    }
    
    return true;
};

require.loaded  = {};
require.loading = {};
require.origin  = location.protocol + "//" + location.host;
require.base    = (location.pathname.match(/^(.+)\/.*$/)) ? location.pathname.match(/^(.+)\/.*$/)[0] : '/';
require.path    = '';
require.timeout = 5000;

使い方は以下。

<script type="text/javascript" src="hoge.js"></script>
<script type="text/javascript" src="require.js"></scirpt><!-- これだけは必要 -->
<script type="text/javascript"><!--

require("jquery.js"); // htmlファイルからの相対パス
$(function() {
    // ほげほげ
});

require("/js/util.js"); // 絶対パス

require.timeout = 1000; // タイムアウト時間(ms)を変更(デフォルトは5000ms)
require.path = 'js'; // パスのprefixを指定
require("app.js"); // ./js/app.jsをロード

require("a.js", "b.js", "c.js"); // 引数の順に複数ロード

require("b.js"); // ロード済みのファイルが指定されたときは何もしない
require("hoge.js"); // require.jsより先に読まれたファイルが指定されたときも何もしない
--></script>

欠点はxhrを使っているためクロスドメインに対応してないことと、
require()より後に書かれているscriptタグは普通に読まれてしまうので、再ロード防止ができないこと。

長所は同期的に読み込めるのでインデントやコールバックが深くならないこと、かな。


何にせよこれでperl likeなモジュール作成ができるってもんよ!