yiiext/twig-renderer
beignWidget/endWidgetでも書けます。->こちら
ここのcreateWidget/wedgit.run は何かの役に立つ(アンチパターンとか)かもしれないので残しておきます。
検証中のコードですがおいておきます。
{% set form = this.createWidget() %} すればwidgetをあきらめなくてもよさげです。this.endWidget のかわりに form.run してます。
<h1>Login</h1> <p>Please fill out the following form with your login credentials:</p> <div class="form"> {% set form = this.createWidget('system.web.widgets.CActiveForm', { 'id':'login-form', 'enableClientValidation':true, 'clientOptions':{ 'validateOnSubmit':true, }, }) %} <p class="note">Fields with <span class="required">*</span> are required.</p> <div class="row"> {{ form.labelEx(model,'username')|raw }} {{ form.textField(model,'username')|raw }} {{ form.error(model,'username')|raw }} </div> <div class="row"> {{ form.labelEx(model,'password') |raw}} {{ form.passwordField(model,'password')|raw }} {{ form.error(model,'password')|raw }} <p class="hint"> Hint: You may login with <tt>demo/demo</tt> or <tt>admin/admin</tt>. </p> </div> <div class="row rememberMe"> {{ form.checkBox(model,'rememberMe')|raw }} {{ form.label(model,'rememberMe')|raw }} {{ form.error(model,'rememberMe')|raw }} </div> <div class="row buttons"> {{ C.html.submitButton('Login') |raw }} </div> {{ form.run }} </div><!-- form -->
モデルを使わないCGridViewサンプル
続いてモデルを使わずzii.widgets.CGridtViewを使ってみる。
サンプルコード
ボタンカラムは、データ各要素にprimaryKeyプロパティを要するのでコメントアウトしています。もちろん回避する方法はあるだろうが今回そこまでやらない。
protected/controllers/SiteController.php
<?php public function actionIndex() { $data = array( (object)array('id'=>1,'title'=>'アルファ','create_time'=>time(), 'content'=>'<div>content</div>', 'category'=>(object)array('name'=>'コメディ'), 'author'=>(object)array('username'=>'アルファのauthor'), ), (object)array('id'=>2, 'title'=>'ベータ', 'create_time'=>time(), 'content'=>'<div>content</div>', 'category'=>(object)array('name'=>'アクション'), 'author'=>(object)array('username'=>'ベータのauthor'), ), (object)array('id'=>3, 'title'=>'チャーリー', 'create_time'=>time(), 'content'=>'<div>content</div>', 'category'=>(object)array('name'=>'アニメ'), 'author'=>(object)array('username'=>'チャーリーのauthor'), ), (object)array('id'=>4, 'title'=>'デルタ', 'create_time'=>time(), 'content'=>'<div>content</div>', 'category'=>(object)array('name'=>'ホラー'), 'author'=>(object)array('username'=>'デルタのauthor'), ), ); $dataProvider = new CArrayDataProvider($data); $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider'=>$dataProvider, 'columns'=>array( 'title', 'category.name', // display the 'name' attribute of the 'category' relation 'content:html', // display the 'content' attribute as purified HTML array( 'name'=>'create_time', 'value'=>'date("M j, Y", $data->create_time)', ), array( // display 'author.username' using an expression 'name'=>'authorName', 'value'=>'$data->author->username', ), // array( // display a column with "view", "update" and "delete" buttons // 'class'=>'CButtonColumn', // ), ), )); }
'category.name' としたり、array('value'=>'$data->author->username') としたりしています。
得られたHTML
table が得られた。単純なarrayからtableへの整形にも使えそうです。
<div id="yw0" class="grid-view"> <div class="summary">Displaying 1-4 of 4 result(s).</div> <table class="items"> <thead> <tr> <th id="yw0_c0">title</th><th id="yw0_c1">category.name</th><th id="yw0_c2">content</th><th id="yw0_c3">create_time</th><th id="yw0_c4">authorName</th> </tr> </thead> <tbody> <tr class="odd"><td>アルファ</td><td>コメディ</td><td><div>content</div></td><td>Mar 7, 2012</td><td>アルファのauthor</td></tr> <tr class="even"><td>ベータ</td><td>アクション</td><td><div>content</div></td><td>Mar 7, 2012</td><td>ベータのauthor</td></tr> <tr class="odd"><td>チャーリー</td><td>アニメ</td><td><div>content</div></td><td>Mar 7, 2012</td><td>チャーリーのauthor</td></tr> <tr class="even"><td>デルタ</td><td>ホラー</td><td><div>content</div></td><td>Mar 7, 2012</td><td>デルタのauthor</td></tr> </tbody> </table> <div class="keys" style="display:none" title="/test/"><span>1</span><span>2</span><span>3</span><span>4</span></div> </div>
ソート&ページネーション
これでソート、ページネーションできた。 リレーションフィールドに対してはできなかったが、今回は(略)
<?php $dataProvider = new CArrayDataProvider($dt, array( 'sort'=>array( // ソート設定 'attributes'=>array('title', 'category.name', 'content', 'create_time', 'authorName'), ), 'pagination'=>array( // ページネーション設定 'pageSize'=>10, ), ));
モデルを使わないCListViewサンプル
モデルを使わずzii.widgets.CListViewを使ってみる。
CListViewで何が得られるのか検証したかったのですが、モデルを用意するのが面倒だったので書きました。
サンプルコード
protected/controllers/SiteController.php
<?php public function actionIndex() { $data = array( (object)array('id'=>1, 'title'=>'アルファ', 'create_time'=>time()), (object)array('id'=>2, 'title'=>'ベータ', 'create_time'=>time()), (object)array('id'=>3, 'title'=>'チャーリー', 'create_time'=>time()), (object)array('id'=>4, 'title'=>'デルタ', 'create_time'=>time()), ); $dataProvider = new CArrayDataProvider($data); $this->widget('zii.widgets.CListView', array( 'dataProvider'=>$dataProvider, 'itemView'=>'_post', // refers to the partial view named '_post' 'sortableAttributes'=>array( 'title', 'create_time'=>'Post Time', ), )); }
protected/views/site/_post.php
<div class="post"> <span class="id"><?php echo CHtml::encode($data->id); ?></span> <span class="title"><?php echo CHtml::encode($data->title); ?></span> </div>
得られたHTML
<div id="yw1" class="list-view"> <div class="summary">Displaying 1-4 of 4 result(s).</div> <div class="sorter"> Sort by: <ul> <li>title</li> <li>Post Time</li> </ul> </div> <div class="items"> <div class="post"><span class="id">1</span><span class="title">アルファ</span></div> <div class="post"><span class="id">2</span><span class="title">ベータ</span></div> <div class="post"><span class="id">3</span><span class="title">チャーリー</span></div> <div class="post"><span class="id">4</span><span class="title">デルタ</span></div> </div> <div class="keys" style="display:none" title="/yiiprac4/"><span>1</span><span>2</span><span>3</span><span>4</span></div> </div>
DataProviderを通して渡したデータが_post.phpへ$dataとして渡されます。
ページネーション、ソートはできませんが、_post.php やCListView.templateプロパティ、CListView.summaryTextプロパティなどをカスタマイズしていけば何が得られるのかわかってくると思います。
ソート&ページネーション - 追記
DataProviderを次のようにしてやるとソート、ページネーションしてくれます。すてき。
SQLの order,limit に依存しているから Arrayはできないと思い込んでいた。
<?php $dataProvider = new CArrayDataProvider($data, array( 'sort'=>array( // ソート設定 'attributes'=>array('title', 'create_time',), ), 'pagination'=>array( // ページネーション設定 'pageSize'=>10, ), ));
モデルを使わないCDetailViewサンプル
zii.widgets.CDetailViewて何を出力するんだっけ。モデルを使わないサンプルコードを書いてみた。
サンプルコード
<?php $model = (object) array( "title"=>"たいとる", "owner" => (object) array( "id"=>"owner.id", "name"=>"オーナーの名前", ), "description"=>"<b>descriptiondescription</b>", "city"=> (object) array( "id"=>"city.id", "name"=>"city.name", ), ); $this->widget('zii.widgets.CDetailView', array( 'data'=>$model, 'attributes'=>array( 'title', // title attribute (in plain text) 'owner.name', // an attribute of the related object "owner" 'description:html', // description attribute in HTML array( // related city displayed as a link 'label'=>'City', 'type'=>'raw', 'value'=>CHtml::link(CHtml::encode($model->city->name), array('city/view','id'=>$model->city->id)), ), ), ));
結果
<table class="detail-view" id="yw1"> <tr class="odd"><th>Title</th><td>たいとる</td></tr> <tr class="even"><th>Owner Name</th><td>オーナーの名前</td></tr> <tr class="odd"><th>Description</th><td><b>descriptiondescription</b></td>tr> <tr class="even"><th>City</th><td><a href="/test/index.php?r=city/view&id=city.id">city.name</a></td></tr> </table>
th, tdが tr になってる table なんだね。
UnphptagViewRenderer作ってみた その2
前エントリの続き。
新たに3つのタグ *{}, @{}, @part{} をサポートしてみました。つっても置換を足しただけですが。
UnphptagViewRenderer.php
<?php /** * UnphptagViewRenderer * * configuration: * <pre> * array( * 'components'=>array( * ...... * 'viewRenderer'=>array( * 'class'=>'path.to.class.UnphptagViewRenderer', * ), * ), * ) * </pre> * * echo: * %{ Yii::app()->name} -> <?php echo Yii::app()->name; ?> * encode echo: * %%{Yii::app()->name} -> <?php echo CHtml::encode(Yii::app()->name); ?> * code: * #{ $name = Yii::app()->name } -> <?php $name = Yii::app()->name; ?> * comment: * *{**** comment ****} -> * renderPartial with PHP TAG: * @{'/parts/part1', $param} -> <?php $this->renderPartial('/parts/part1', $param); ?> * renderPartial whthout PHP TAG: * @part{'/parts/part1', $param} -> $this->renderPartial('/parts/part1', $param) * */ class UnphptagViewRenderer extends CViewRenderer { protected function generateViewFile($sourceFile, $viewFile) { $source = file_get_contents($sourceFile); file_put_contents($viewFile, $this->parse($source)); } protected function parse($s) { $s = preg_replace('/\*\{([\s\S]*?)\}/', '', $s); $s = preg_replace('/%%\{([\s\S]*?)\}/', '<?php echo CHtml::encode($1); ?>', $s); $s = preg_replace('/%\{([\s\S]*?)\}/', '<?php echo $1; ?>', $s); $s = preg_replace('/#\{([\s\S]*?)\}/', '<?php $1; ?>', $s); $s = preg_replace('/\@\{([\s\S]*?)\}/', '<?php $this->renderPartial($1); ?>', $s); $s = preg_replace('/\@part\{([\s\S]*?)\}/', '$this->renderPartial($1)', $s); return $s; } }
PlayFrameworkのカスタムテンプレートタグ
yiiから離れますが、PlayFrameworkのカスタムテンプレートタグは使えるなーという印象があります。Aタグを生成する小さなものから、そこそこ凝ったDIVブロックを生成するものまでテンプレート化していました。
一方、yiiではそこそこのものはテンプレートにしますが、小さいのまではテンプレートにしていない。この違いはなんだろう。
$this->renderPartial は長いです。小さいテンプレート達一つ一つに $this->renderPartial, $this->renderPartial, $this->renderPartial と書きたくない。これがテンプレート化しない原因の一つじゃないかと考えました。
前回はview上にあふれるPHPタグを減らしました。今回は $this->renderPartial を減らします。
@{}タグ
「@{}」は$this->renderPartial省略記法その1です。次のように使います。引数はrenderPartialと同じです。というか「@{'aaa', bbb, ccc}」を置換して「$this->renderPartial('aaa', bbb, ccc)」にしているだけ。
サンプルはyii/demos/blogです。ここには3つの $this->renderPartial がありました。
blog/protected/views/post/view.php
#{ $this->breadcrumbs=array( $model->title, ); $this->pageTitle=$model->title; } <?php // $this->renderPartial('_view', array( // 'data'=>$model, //)); ↓↓↓ ?> @{'_view', array('data'=>$model,)} <div id="comments"> #{if($model->commentCount>=1):} <h3> %{$model->commentCount>1 ? $model->commentCount . ' comments' : 'One comment'} </h3> <?php // $this->renderPartial('_comments',array( // 'post'=>$model, // 'comments'=>$model->comments, //)); ↓↓↓ ?> @{'_comments',array( 'post'=>$model, 'comments'=>$model->comments, )} #{endif;} <h3>Leave a Comment</h3> #{if(Yii::app()->user->hasFlash('commentSubmitted')):} <div class="flash-success"> %{Yii::app()->user->getFlash('commentSubmitted')} </div> #{else:} <?php //$this->renderPartial('/comment/_form',array( // 'model'=>$comment, //)); ↓↓↓ ?> @{'/comment/_form',array('model'=>$comment,)} #{endif;} </div><!-- comments -->
パラメータをarrayで包んで渡すのはイケてないですね。「@_view{compact('model')}」などどしたいところです。
@part{}
$this->renderPartial の省略記法その2です。
YiiのCDetailViewに複雑なHTMLを表示させたいとき
↑のように $this->renderPartial で生成した文字列を使う場合もあります。この場合、次のように書けます。勝手に引用してすみません。
<?php $this->widget('zii.widgets.CDetailView', array( 'data'=>$model, 'attributes'=>array( 'id', 'name', //'poor_content', // 文字列表示じゃかっこ悪い array( 'label'=>'Complex Content', 'type'=>'raw', 'value'=> @part{'_complex_content', array('data'=>$model,), true} ), ), )); ?>
こちらも「@_complex_content{compact('model')}」などとしたいですね。
UnphptagViewRenderer作ってみた
yiiのviewにあふれるPHPタグがなんとかならんかと思い作ってみたwww。使いかたはコメント参照。
<?php /** * UnphptagViewRenderer * * configuration: * <pre> * array( * 'components'=>array( * ...... * 'viewRenderer'=>array( * 'class'=>'path.to.class.UnphptagViewRenderer', * ), * ), * ) * </pre> * * echo: * %{ Yii::app()->name} -> <?php echo Yii::app()->name; ?> * encode echo: * %%{Yii::app()->name} -> <?php echo CHtml::encode(Yii::app()->name); ?> * code: * #{ $name = Yii::app()->name } -> <?php $name = Yii::app()->name; ?> * */ class UnphptagViewRenderer extends CViewRenderer { protected function generateViewFile($sourceFile, $viewFile) { $source = file_get_contents($sourceFile); file_put_contents($viewFile, $this->parse($source)); } protected function parse($s) { $s = preg_replace('/%%\{([\s\S]*?)\}/', '<?php echo CHtml::encode($1); ?>', $s); $s = preg_replace('/%\{([\s\S]*?)\}/', '<?php echo $1; ?>', $s); $s = preg_replace('/#\{([\s\S]*?)\}/', '<?php $1; ?>', $s); return $s; } }
こうなる
webapp/protected/views/layouts/main.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="language" content="en" /> <!-- blueprint CSS framework --> <link rel="stylesheet" type="text/css" href="%{Yii::app()->request->baseUrl}/css/screen.css" media="screen, projection" /> <link rel="stylesheet" type="text/css" href="%{Yii::app()->request->baseUrl}/css/print.css" media="print" /> <!--[if lt IE 8]> <link rel="stylesheet" type="text/css" href="%{Yii::app()->request->baseUrl}/css/ie.css" media="screen, projection" /> <![endif]--> <link rel="stylesheet" type="text/css" href="%{Yii::app()->request->baseUrl}/css/main.css" /> <link rel="stylesheet" type="text/css" href="%{Yii::app()->request->baseUrl}/css/form.css" /> <title>%%{$this->pageTitle}</title> </head> <body> <div class="container" id="page"> <div id="header"> <div id="logo">%%{Yii::app()->name}</div> </div><!-- header --> <div id="mainmenu"> #{ $this->widget('zii.widgets.CMenu',array( 'items'=>array( array('label'=>'Home', 'url'=>array('/site/index')), array('label'=>'About', 'url'=>array('/site/page', 'view'=>'about')), array('label'=>'Contact', 'url'=>array('/site/contact')), array('label'=>'Login', 'url'=>array('/site/login'), 'visible'=>Yii::app()->user->isGuest), array('label'=>'Logout ('.Yii::app()->user->name.')', 'url'=>array('/site/logout'), 'visible'=>!Yii::app()->user->isGuest) ), )); } </div><!-- mainmenu --> #{ if(isset($this->breadcrumbs)): } #{ $this->widget('zii.widgets.CBreadcrumbs', array( 'links'=>$this->breadcrumbs, )); }<!-- breadcrumbs --> #{ endif } %{$content} <div class="clear"></div> <div id="footer"> Copyright © %{date('Y')} by My Company.<br/> All Rights Reserved.<br/> %{Yii::powered()} </div><!-- footer --> </div><!-- page --> </body> </html>
webapp/protected/views/site/login.php
#{ $this->pageTitle=Yii::app()->name . ' - Login'; $this->breadcrumbs=array( 'Login', ); } <h1>Login</h1> <p>Please fill out the following form with your login credentials:</p> <div class="form"> #{ $form=$this->beginWidget('CActiveForm', array( 'id'=>'login-form', 'enableClientValidation'=>true, 'clientOptions'=>array( 'validateOnSubmit'=>true, ), )); } <p class="note">Fields with <span class="required">*</span> are required.</p> <div class="row"> %{ $form->labelEx($model,'username') } %{ $form->textField($model,'username') } %{ $form->error($model,'username') } </div> <div class="row"> %{ $form->labelEx($model,'password') } %{ $form->passwordField($model,'password') } %{ $form->error($model,'password') } <p class="hint"> Hint: You may login with <tt>demo/demo</tt> or <tt>admin/admin</tt>. </p> </div> <div class="row rememberMe"> %{ $form->checkBox($model,'rememberMe') } %{ $form->label($model,'rememberMe') } %{ $form->error($model,'rememberMe') } </div> <div class="row buttons"> %{ CHtml::submitButton('Login') } </div> #{ $this->endWidget(); } </div><!-- form -->
yiiカイドを横断してみる リレーション(3/3) - テスト
今回は作成したテーブルにデータを登録し、モデルからDBのデータにアクセス可能かユニットテストを使って検証します。とりあえずテスト概要を読んでおいてください。PHPUnitをインストールしていなければしといてください。
テスト計画
まずどんなテストをするのか決めておきます。今回はモデル間のリレーションが想定どおり働くかどうかを検証します。
- UserのProfileが取得できること
- UserからPost(複数)を取得できること
- Postのauthor(User)を取得できること
- PostのCategory(複数)を取得できること
テストデータ作成 - フィクスチャマネージャの準備
フィクスチャを参照しつつ、main.php を変更します。毎回変更してますね。
/webapp/protrected/config/main.php
<?php return array( 'components'=>array( 'fixture'=>array( // components に fixture を加える 'class'=>'system.test.CDbFixtureManager', ), ), );
テストデータ作成 - フィクスチャを定義する
フィクスチャを定義するを見つつ、テストデータをフィクスチャファイルに書き出します。フィクスチャファイルはprefixを省略せずテーブル名と一致させます。tablePrefixを適用する方法はわからなかった。
/webapp/protected/tests/fixtures/tbl_user.php
<?php return array( 'user1'=>array( 'username'=>'user1', 'password'=>'user1password', 'email'=>'user1@test.test', ), 'user2'=>array( 'username'=>'user2', 'password'=>'user2password', 'email'=>'user2@test.test', ), );
/webapp/protected/tests/fixtures/tbl_profile.php
<?php return array( 'user1'=>array( 'owner_id'=>'1', 'photo'=>NULL, //file_get_contents 'website'=>'user1 website', ), 'user2'=>array( 'owner_id'=>'2', 'photo'=>NULL, //file_get_contents 'website'=>'user2 website', ), );
/webapp/protected/tests/fixtures/tbl_post.php
<?php return array( 'post1'=>array( 'title'=>'post1 title', 'content'=>'post1 content', 'create_time'=>getdate(), 'author_id'=>1, ), 'post2'=>array( 'title'=>'post2 title', 'content'=>'post2 content', 'create_time'=>getdate(), 'author_id'=>1, ), );
/webapp/protected/tests/fixtures/tbl_post_category.php
<?php return array( 'post_category_1_1'=>array( 'post_id'=>'1', 'category_id'=>'1', ), 'post_category_1_2'=>array( 'post_id'=>'1', 'category_id'=>'2', ), );
/webapp/protected/tests/fixtures/tbl_category.php
<?php return array( 'category1'=>array( 'name'=>'category1', ), 'category2'=>array( 'name'=>'category2', ), );
テストケース作成
ユニットテストを参考にしつつ、テストケースを書きます。
/webapp/protected/tests/unit/models/UserTest.php
<?php class UserTest extends CDbTestCase { public $fixtures=array(); public function testProfile() { $user = User::model()->findByPk(1); $this->assertNotNull($user); // UserのProfileが取得できること $this->assertNotNull($user->profile); $this->assertEquals("user1 website", $user->profile->website); } public function testPosts() { $user = User::model()->findByPk(1); $this->assertNotNull($user); // Userが登録したPost(複数)を取得できること $this->assertEquals(2, count($user->posts)); $this->assertEquals("post1 title", $user->posts[0]->title); $this->assertEquals("post2 title", $user->posts[1]->title); } }
/webapp/protected/tests/unit/models/PostTest.php
<?php class PostTest extends CDbTestCase { public $fixtures=array(); public function testAuthor() { $post = Post::model()->findByPk(1); $this->assertNotNull($post); // Postのauthor(User)を取得できること $this->assertNotNull($post->author); $this->assertEquals("user1", $post->author->username); } public function testCategories() { $post = Post::model()->findByPk(1); $this->assertNotNull($post); // PostのCategory(複数)を取得できること $this->assertEquals(2, count($post->categories)); $this->assertEquals("category1", $post->categories[0]->name); $this->assertEquals("category2", $post->categories[1]->name); } }
テスト
テストです。次のようにユニットテストを実行しOKとなれば成功です。
>cd webapp\protected\tests >phpunit unit\models\PostTest.php PHPUnit 3.5.14 by Sebastian Bergmann. .. Time: 2 seconds, Memory: 7.00Mb OK (2 tests, 7 assertions)
>cd webapp\protected\tests >phpunit unit\models\UserTest.php PHPUnit 3.5.14 by Sebastian Bergmann. .. Time: 1 second, Memory: 7.00Mb OK (2 tests, 7 assertions)