laravelのpivotいいなと思ったの巻
yii ActiveRecord の弱点
yii ActiveRecord は、many-manyの中間テーブルに属性を付けにくい弱点があります。たとえばこんな問題とか。
Accessing data in a join table with the related models
いまだにキレイな解決方法がない。と思う。ほんとにないの?
laravel は pivot で解決
一方、laravelならこう書く。
テーブル
CREATE TABLE viewer ( id INT NOT NULL PRIMARY KEY, name VARCHAR(45)); CREATE TABLE movie ( id INT NOT NULL PRIMARY KEY, title VARCHAR(45)); CREATE TABLE viewer_watched_movie ( id INT NOT NULL, -- <--- ここ見逃さない viewer_id INT NOT NULL, movie_id INT NOT NULL, liked TINYINT(1), PRIMARY KEY (id), CONSTRAINT fk_viewer_watched FOREIGN KEY (movie_id) REFERENCES movie (id), CONSTRAINT fk_movie_watched_by FOREIGN KEY (viewer_id) REFERENCES viewer (id));
viewerモデル
<?php class Viewer extends Eloquent { public static $table = 'viewer'; public function movies() { return $this->has_many_and_belongs_to('Movie', 'viewer_movies') ->with('liked'); // <--- この with が大事 } }
movieモデル
<?php class Movie extends Eloquent { public static $table = 'movie'; }
コントローラー
<?php $viewer = Viewer::find(1); foreach ($viewer->movies()->get() as $movie) echo $viewer->name . ($movie->pivot->liked ? ' liked ' : ' didn’t like ') // <--- pivot で with したカラムを参照できる . $movie->title ."<br>" ;
うん、素直に書けた。しかも冒頭挙げたページのproblemコードと酷似したコードで書けてる。
注意
pivot テーブルに created_at, updated_at がないと、こんなエラーがでる。
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'viewer_movies.created_at' in 'field list'
そんなときは、 application/start.php にこれを書いておく。
Laravel\Database\Eloquent\Pivot::$timestamps = false;
pivot テーブルに created_at, updated_at を要求するのはお節介なような気が。
参考
Eloquent ORM - laravel 日本語ドキュメント
Disable timestamps in pivot table ? - github/laravel/issues/543
laravelを使おうかと思った理由の一つがコレ
laravel はじめました
公式ドキュメントを購入したのは先月だったか。ようやくはじめた。
はじめて書いたコード
見よう見まねで書いたログインフォームがこれ。 action_login と action_dologin で同じものをviewに渡そうとゴニョゴニョしていますね。
controllers/auth.php
<?php class Auth_Controller extends Base_Controller { public function action_login() { $input = array('username' => ''); return View::make('auth.login') ->with('input', $input) ->with('errors', new Laravel\Messages()); } public function action_dologin() { $input = Input::all(); $rules = array( 'username' => 'required', 'password' => 'required', ); $validation = Validator::make($input, $rules); if ($validation->fails()) { return View::make('auth.login') ->with('input', $input) ->with('errors', $validation->errors); } return "authed!"; } }
views/auth/login.blade.php
{{ Form::open('auth', 'post') }} {{ Form::text('username', $input['username']) }} @if ($errors->has('username')) {{ $errors->first('username') }} @endif {{ Form::password('password') }} @if ( $errors->has('password')) {{ $errors->first('password') }} @endif {{ Form::submit() }} {{ Form::close() }}
書き直したコード
ドキュメントとかソースとか読み直して、書き直したのがこれ。action_login からバリデーション失敗時のコードがなくなりすっきりしました。
controllers/auth.php
<?php class Auth_Controller extends Base_Controller { public function action_login() { return View::make('auth.login'); } public function action_dologin() { $input = Input::all(); $rules = array( 'username' => 'required', 'password' => 'required', ); $validation = Validator::make($input, $rules); if ($validation->fails()) { return Redirect::back() ->with_input() ->with_errors($validation); } return "authed!"; } }
views/auth/login.blade.php
{{ Form::open('auth', 'post') }} {{ Form::text('username', Input::old('username')) }} {{ $errors->first('username') }} {{ Form::password('password') }} {{ $errors->first('password') }} {{ Form::submit() }} {{ Form::close() }}
わかったこと
バリデーションエラーで前のページに戻るときは、Redirect::back()
あわせて with_input(), with_errors() も使う
Input::old() で前回入力した値を得られる
そのために with_input() しておく
view に空の errors を渡す必要はない
Viewコンストラクタ で $this->date['errors'] = new Messages() している。
エラーメッセージのキーは errors とするのが laravel の流儀っぽい
バリデーションメッセージ表示は {{$errors->first('fieldname')}}
エラーがなければ何もしないから if いらない
play1.2をJavassistで改造してみた
先日の第3回PlayFramework勉強会でこんなことを考えた。
play1系はメンテナンスモードか、今後の更新はあまり期待できないな、じゃあJavassistで改造すればいいか。
普通に考えると、自分でPlayFrameworkをビルドしろってことでしょうが、当日は道に迷ったせいかおかしくなってました。
今回は実際Javassistで改造できるものなのか試してみました。
ここをこうしたい
play1系はGsonでJSONエンコードしますが、GsonでなくJSONICとかJackson使いたいんだよ、ってケースだとします。ControllerをOverrideするとか、JSONエンコード後のString渡せばいいじゃんとかありますが、これはまあ例ってことで。
JSONエンコードはRenderJsonのコンストラクタでやってます。これを
package play.mvc.results; public class RenderJson extends Result { public RenderJson(Object o) { json = new Gson().toJson(o); } }
こうしたい。
public RenderJson(Object o) {
json = net.arnx.jsonic.JSON.encode(o);
}
RenderJsonを書き換える
前回作ったMyPluginを利用してonApplicationStartで書き換えてみます。
public class MyPlugin extends PlayPlugin { public void onApplicationStart() { play.Logger.info("@@@ MyPlugin.onApplicationStart"); try { enhanceRenderJson(); } catch (Exception e) { play.Logger.error(e, ""); } } protected void enhanceRenderJson() throws NotFoundException, CannotCompileException, IOException { // クラスプールから RenderJson を得る ClassPool classPool = ClassPool.getDefault(); CtClass cc = classPool.get("play.mvc.results.RenderJson"); // コンストラクタのSignatureを得る // CtConstructor[] cnsts = cc.getConstructors(); // for (CtConstructor cnst : cnsts) { // play.Logger.info("@@@ CtConstructor:%s", cnst.getSignature()); // } // コンストラクタ書き換え cc.defrost(); CtConstructor m = cc.getConstructor("(Ljava/lang/Object;)V"); m.setBody("{" + " play.Logger.info(\"@@@ jsonic encode!\", null);" + " json = net.arnx.jsonic.JSON.encode($1);" // $1:1つ目のメソッド引数のこと + "}" ); cc.writeFile(); // クラスローダーに登録 Class thisClass = cc.getClass(); ClassLoader loader = thisClass.getClassLoader(); ProtectionDomain domain = thisClass.getProtectionDomain(); cc.toClass(loader, domain); } }
onApplicationStart ならプラグインでなくても、JobをExtendsしてOnApplicationStartアノテーション付けてもいいですよね。
動作確認
モデル
public class Hoge { public Long id; public String name; @net.arnx.jsonic.JSONHint(format="yyyy-MM-dd'T'HH:mm:ssZZ") public Date createdAt; }
コントローラー
public class MyController extends Application { public static void index() { Hoge h = new Hoge(); h.id = 1000L; h.name = "hogehoge"; h.createdAt = new Date(); renderJSON(h); } }
改造前:Gsonでレンダリング
{ id: 1000, name: "hogehoge", createdAt: "Jul 26, 2012 6:47:41 AM" }
改造後:JSONICでレンダリング
JSONHintアノテーションによってcreatedAtがフォーマットされている
{ createdAt: "2012-07-26T06:48:33+09:00", id: 1000, name: "hogehoge" }
まとめ
play1.2をJavassistで改造してみました。
自前ビルドしろよとか改造後のテストはどうすんのとか思いますが、とりあえず置いておきます...
参考
Javassist - ひしだま's ホームページ
Javassistチュートリアル - Acroquest Technology
書き換えたクラスのクラスローダー登録を忘れてはまりました。いやー感謝感謝。
play1.2系の自作プラグイン
PlayFrameworkといえばトレンドはplay2.0なのでしょうが、うちはまだplay1.2系です。play1.2系で自作プラグインを作ってみました。
plugins.MyPlugin 作成
- play.PlayPluginをextends
- onなんたらをOverride
- パッケージは適当に
package plugins; import play.PlayPlugin; public class MyPlugin extends PlayPlugin { public void onLoad() { play.Logger.info("@@@ MyPlugin.onLoad"); } public void onApplicationReady() { play.Logger.info("@@@ MyPlugin.onApplicationReady"); } public void onApplicationStart() { play.Logger.info("@@@ MyPlugin.onApplicationStart"); } public void enhance(ApplicationClass applicationClass) throws Exception { play.Logger.info("@@@ MyPlugin.enhance : %s", applicationClass.name); } }
conf/play.plugins 作成
- プライオリティ:プラグインクラス
- プライオリティはデフォルトのものを参考にしつつ決める
こんな感じに。
100001:plugins.MyPlugin
デフォルトプラグインのプライオリティはここ参照
動作確認
アプリケーションを起動するとこうなり、play.Logger.infoできていることがわかります。
>play test ~ _ _ ~ _ __ | | __ _ _ _| | ~ | '_ \| |/ _' | || |_| ~ | __/|_|\____|\__ (_) ~ |_| |__/ ~ ~ play! 1.2.5, http://www.playframework.org ~ framework ID is test ~ ~ Running in test mode ~ Ctrl+C to stop ~ INFO ~ Starting INFO ~ @@@ MyPlugin.onLoad WARN ~ You're running Play! in DEV mode ~ ~ Go to http://localhost:9000/@tests to run the tests ~ INFO ~ @@@ MyPlugin.onApplicationReady INFO ~ Listening for HTTP on port 9000 (Waiting a first request to start) ... INFO ~ @@@ MyPlugin.onLoad INFO ~ Connected to jdbc:mysql://localhost/plugintest?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8_general_ci INFO ~ @@@ MyPlugin.onApplicationStart INFO ~ Application 'plugintest' is now started !
参考
How to extend the playframework? - stackoverflow
Play framework 2.0 での自作プラグイン - Scala版 - なんとなくな Developer のメモ
プラグインの作り方は1.2系も2.0系も同じですね。感謝
PHPTALのカスタムモディファイア if, eq を作ってみた
PHPTALと格闘すること数日。PHPTALにはif文っぽい表現をするにはPHPモディファイアを使わざるをえないことに悩みました。
condとeqがあればいいのに。なぜだろう。これが TAL-way なのか。でも欲しい。PHPモディファイアは使いたくない。でも。でも。。。
んが作った。
if, eq モディファイア
function phptal_tales_if( $src, $nothrow ) { list($if_part, $right) = explode('?', $src, 3); if (strpos($right,'|')) { list($then_part, $else_part) = explode('|', $right, 2); } else { $then_part = $right; $else_part = 'null'; } return '('.phptal_tales($if_part, $nothrow).') ? '.phptal_tales($then_part, $nothrow).' : '.phptal_tales($else_part, $nothrow).''; } function phptal_tales_eq( $src, $nothrow ) { list($left, $right) = explode(' ', $src, 2); return '('.phptal_tales($left, $nothrow).' == '.phptal_tales($right, $nothrow).' )'; }
condでなくifにした。
使い方
コントローラー
$people = array(); $people[] = (object)array("name"=>"foo", "phone"=>"01-344-121-021"); $people[] = (object)array("name"=>"bar", "phone"=>"05-999-165-541"); $people[] = (object)array("name"=>"baz", "phone"=>"01-389-321-024"); $people[] = (object)array("name"=>"quz", "phone"=>"05-321-378-654"); $template->people = $people; echo $template->execute();
テンプレート
<span tal:replace="if: eq:people/0/name people/0/name ? string:then " >default</span> :then <br/> <span tal:replace="if: eq:people/0/name people/1/name ? string:then " >default</span> :null <br/> <span tal:replace="if: eq:people/0/name people/0/name ? string:then | string:else" /> :then <br/> <span tal:replace="if: eq:people/0/name people/1/name ? string:then | string:else"/> :else <br/> <span tal:replace="if: exists:people/1 ? string: people/1 exists | string: people/1 not exists " >default</span> :people/1 exists <br/> <span tal:replace="if: exists:people/100 ? string: people/100 exists | string: people/100 not exists " >default</span> :people/100 not exists <br/>
というわけで
PHPTALの道から外れるかもしれないが作ってすっきりした。これならいろいろ表現できそう。もとからあるexistsも活かせるし、neもすぐ作れるね。
YiiFrameworkでPHPTALしてみた
PHPTALやってみようかと思ったので、YiiFrameworkで動くPHPTALViewRenderer作ってみた。
インストール
PHPTAL
phptalとかgithubからzipをダウンロードし解凍し、classes ディレクトリ下の PHPTALディレクトリと PHPTAL.phpを prorected/vendor/PHPTAL へコピーする。
PHPTALViewRenderer
次をコピペする。ETwigViewRenderer を参考に作成しました。
prorected/extension/PHPTALViewRenderer.php
<?php class PHPTALViewRenderer extends CApplicationComponent implements IViewRenderer { /** * @var string Path alias to PHPTAL.php */ public $PHPTALPathAlias = 'application.vendors.PHPTAL'; /** * @var string Template files extension */ public $fileExtension = '.html'; private $_basePath; private $_basePathLength; function init() { require_once Yii::getPathOfAlias($this->PHPTALPathAlias).'/PHPTAL.php'; Yii::registerAutoloader(array('PHPTAL', 'autoloadRegister'), true); $app = Yii::app(); /** @var $theme CTheme */ $theme = $app->getTheme(); if ($theme === null) { $this->_basePath = $app->getBasePath(); } else { $this->_basePath = $theme->getBasePath(); } $this->_basePathLength = strlen($this->_basePath) ; return parent::init(); } public function renderFile($context, $sourceFile, $data, $return) { $sourceFile = "protected/".substr($sourceFile, $this->_basePathLength); $phptal = new PHPTAL($sourceFile); $phptal->this = $context; foreach($data as $key => $val) { $phptal->$key = $val; } $result = $phptal->execute(); if ($return) { return $result; } echo $result; } }
設定
PHPTALViewRenderer を設定
prorected/config/main.php
'components'=>array( 'viewRenderer'=>array( 'class'=>'ext.PHPTALViewRenderer', 'fileExtension' => '.html', ),
PHPTALのサンプルを試す
コントローラー
PHPTALのサンプルのパクリ。Personクラス作るのが面倒なので(object)で代用。
protected/controllers/TestController.php
<?php class TestController extends Controller { public $layout='column1'; public function actionIndex() { $title = 'The title value'; $people = array(); $people[] = (object)array("name"=>"foo", "phone"=>"01-344-121-021"); $people[] = (object)array("name"=>"bar", "phone"=>"05-999-165-541"); $people[] = (object)array("name"=>"baz", "phone"=>"01-389-321-024"); $people[] = (object)array("name"=>"quz", "phone"=>"05-321-378-654"); $this->render('index', compact('title', 'people')); } }
ビュー
PHPTALのサンプルそのまんま。
protected/views/test/index.html
<?xml version="1.0"?> <html> <head> <title tal:content="title"> Place for the page title </title> </head> <body> <h1 tal:content="title">sample title</h1> <table> <thead> <tr> <th>Name</th> <th>Phone</th> </tr> </thead> <tbody> <tr tal:repeat="person people"> <td tal:content="person/name">person's name</td> <td tal:content="person/phone">person's phone</td> </tr> <tr tal:replace=""> <td>sample name</td> <td>sample phone</td> </tr> <tr tal:replace=""> <td>sample name</td> <td>sample phone</td> </tr> </tbody> </table> </body> </html>
結果
得られたhtmlの一部
layouts/main.php, layouts/column1.php の内部に↓が出力される。
<?xml version="1.0"?> <html> <head> <title>The title value</title> </head> <body> <h1>The title value</h1> <table> <thead> <tr> <th>Name</th> <th>Phone</th> </tr> </thead> <tbody> <tr> <td>foo</td> <td>01-344-121-021</td> </tr><tr> <td>bar</td> <td>05-999-165-541</td> </tr><tr> <td>baz</td> <td>01-389-321-024</td> </tr><tr> <td>quz</td> <td>05-321-378-654</td> </tr> </tbody> </table> </body> </html>
課題
- もっとPHPTALを学ばなきゃ
- CWebUserへのアクセスは $template->user = Yii::app()->user; とすれば可能
- layouts/main.php, layouts/column1.php をどうにかする
- renderPartialは macroを使うのかな
- formatterはモディファイアに組み込むのかな
- yii::t()は PHPTAL_TranslationService に組み込むのかな
- フラグメントキャッシュは PHPTAL_Trigger を使って実現するのかな
次は TAL-way じゃないのでサポートしなくていいよね
- widget
- CHTML
yiiのアクセス制御を学ぶ yii-rightsを導入するの巻
前エントリのロール管理はこれでもロール管理かという代物でした。今回はyii-rightsを使いGUIで管理できるようにします。
blogデモにyii-rightsを組み込んだソースやデモも用意されている。手っ取り早く確認したいならはここから。
導入
- とりあえずドキュメント参照
- ロール関連のテーブルをドロップ (DROP TABLE `authassignment`, `authitem`, `authitemchild`;)
- ダウンロードして protected/modules/rights へ展開
- main.php 修正 ドキュメント参照
- adminロールをつけたいユーザーでログイン
- http://path/to/index.php/rights へアクセスしインストールする( rights/components/RInstaller.php参照)
- main.php 'install'=true を削除
- adminロールを持つユーザーでロール管理する
前エントリの権限を回復したい場合、http://path/to/index.php/site/loadroles へアクセスする。これで簡易ロール管理は不要になる。あるいは、yii-rightsインストール手順をせずにrightsテーブル(rights/data/schema.sql参照)をcreateする。デフォルトでは管理者権限はadminとなっているので異なる場合は注意。
yii-rights管理ページにはdescriptionが表示されるが、前エントリのロールに説明を設定していなかったので加えておく。
protected/coltrollers/SiteController.php
$role=$auth->createRole('reader', 'reader'); ... ↑↑↑↑ $role=$auth->createRole('author', 'author'); ... ↑↑↑↑ $role=$auth->createRole('editor', 'editor'); ... ↑↑↑↑ $role=$auth->createRole('admin', 'admin'); ... ↑↑↑↑
yii-rightsのクラス
書きかけ。後で書き加えたい。
RDbAuthManager
CDbAuthManager を継承している。rightsテーブル weight カラムを使っている
RWebUser
isSuperuser が追加されている。