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って奴はどうしてこうも・・・
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なモジュール作成ができるってもんよ!