tjtjtjのメモ

自分のためのメモです

yiiのアクセス制御を学ぶ ログインの巻

YiiFrameworkのアクセスコントロールを学習したときのメモ。
ログインから始めロールベースアクセスコントロール、YiiRightsに辿り着きたい。素直にcookbook買うべきなのかもしれない。

ログイン

ユーザー認証。これがなくちゃ話にならない。最初に注意すべきなのは

情報: identity クラスと user アプリケーションコンポーネントはしばしば混同されます。前者は認証を行う方法のことであり、後者は現在のユーザに関する情報をあらわします。

http://www.yiiframework.com/doc/blog/1.1/ja/prototype.auth

というところか。

identity クラスとはCUserIdentityのこと。user アプリケーションコンポーネントとは CWebUserのこと。そして config/main.php の user ってのは、CWebUser のこと。
protected/config/main.php

<?php
	'components'=>array(
		'user'=>array(
			// enable cookie-based authentication
			'allowAutoLogin'=>true,
		),
	),

userはアプリケーションでひとつだけだが、identity:認証方法は複数持てる。

認証の流れはガイド/チュートリアル/Blogデモ実装されているから迷わない。。。と思ったら迷った。SiteController#actionLogin -> LoginForm#login -> UserIdentity#authenticate, CWebuser#login と辿るうちに login の多さにめまいがする。Yii::app()->user->login() している protected/models/LoginForm.php を軸に辿れば迷わない。

protected/models/LoginForm.php

<?php
	public function login()
	{
		if($this->_identity===null)
		{
			$this->_identity=new UserIdentity($this->username,$this->password);   // 認証情報を「認証方法」に渡す
			$this->_identity->authenticate();                                     // 認証
		}
		if($this->_identity->errorCode===UserIdentity::ERROR_NONE)                    // 結果を判断
		{
			$duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
			Yii::app()->user->login($this->_identity,$duration);                  // 「認証方法」からユーザー情報を引き渡す
			return true;
		}
		else
			return false;
	}

CUserIdentity を介してuser.id, user.name を CWebUser.login に引き渡していることが理解できればOKだと思う。

ログアウト

CWebUserのLogoutメソッドをコールする。これはコントローラに実装されている。
protected/controllers/SiteController.php

<?php
	public function actionLogout()
	{
		Yii::app()->user->logout();
		$this->redirect(Yii::app()->homeUrl);
	}

ログイン済/未ログインの判断

次のコードで判断できる

<?php
if (Yii::app()->user->isGuest) {
	echo '未ログイン:'.Yii::app()->user->name;
} else {
	echo 'ログイン済:'.Yii::app()->user->name;
}

この判断は layouts/main.php などで見られる。
protected/views/layouts/main.php

    <div id="mainmenu">
        <?php $this->widget('zii.widgets.CMenu',array(
            'items'=>array(
                array('label'=>'Home', 'url'=>array('post/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 -->

yiiext/twig-renderer でビューをフラグメントキャッシュしたい

{{void(...)}}でなく{%do...%}で

前のエントリで{{void(this.endWedgit)}}と書きましたが、{%do this.endWidget %}でいいことがわかり修正しました。ごめんなさい。

フラグメントキャッシュとは

フラグメントキャッシュはページの断片をキャッシュする事を指します。 たとえば、ページ中に年間売り上げサマリの表がある場合、 リクエスト毎にこれを生成する時間をなくすために、この表をキャッシュに保持できます。

http://www.yiiframework.com/doc/guide/1.1/ja/caching.fragment

yiiのフラグメントキャッシュのいいところは1テンプレート内にキャッシュされるところ/されないところが混在できるのでキャッシュの都合でテンプレートを分離せず済むことです。CakePHPのエレメントキャッシュではテンプレートを分離しなければならなかった、ような記憶があるような。

twig-rendererでもフラグメントキャッシュが働くかどうか、テンプレートとして読みやすいかどうかを検証します。

書き換え

php のフラグメントキャッシュ
<?php if($this->beginCache($id)) { ?>
...キャッシュされるコンテンツ...
<?php $this->endCache(); } ?>
twig-renderer のフラグメントキャッシュ
{% if (this.beginCache(id)) %}
...キャッシュされるコンテンツ...
{% do this.endCache %}
{% endif %}

いびつなコードですね。できれば beginCache/endCache だけで書きたいところです。テンプレートとしては読みにくいですか。

検証コード

protected/config/main.php - ファイルキャッシュを有効にする

	'components'=>array(
		'cache'=>array(
			'class'=>'system.caching.CFileCache',
		),
	),

protected/views/site/index.twig - フラグメントキャッシュのテスト

<h3>予想</h3>
#1 : uncached == cached<br>
#2 : uncached != cached<br>
#3 : uncached != cached<br>
#4 : ..<br>

uncache : {{ date|date('Y-m-d\TH:i:sP') }}

{% if (this.beginCache(1)) %} 
cached  : {{ date|date('Y-m-d\TH:i:sP') }}
{% do this.endCache %}
{% endif %}

検証結果

予想通り、1回目の表示では2つの日時が一致し、2回目以降は2つの日時がズレる、のように動作すると思います。
というわけで、twig-renderer でもフラグメントキャッシュが機能することがわかりました。テンプレートとしてはちょっと読みにくいか。

yiiext/twig-rendererのビューで独自の書式を使いたい

twig-rendererのfiltersに関数を指定するとtwigのfilter構文でその関数が使えるようになります。twigのfilterとバッティングしたらどうなるんだろ。
こんな感じ。

        'viewRenderer'=>array(
            'class'=>'ext.etwigviewrenderer.ETwigViewRenderer',
            'filters' => array(
                'bold' => 'bold',
            ),
        ),
function bold ($value)
{
    return "<b>".$value."</b>";
}
{% set value = "hogehoge" %}
{{ value |bold}} 
↓
<b>hogehoge</b>

独自の書式を使えると便利

Yiiのビューで独自の書式が欲しいときは CFormatter を拡張するといい | tipshare.infoのMyFormatterのstaticメソッドがフィルター構文で使えると便利ですよね。warekiをfiltersに登録するとこうなります。

        'viewRenderer'=>array(
            'class'=>'ext.etwigviewrenderer.ETwigViewRenderer',
            'filters' => array(
                'wareki' => 'MyFormatter::wareki',
            ),
        ),
<?php echo Yii::app()->format->wareki($date)); ?>
↓↓↓↓
{{ date|wareki }}

これは便利!!

yiiext/twig-rendererのビューでメッセージを扱いたい

これをどうするか

<?php echo Yii::t('yii','{attribute} is invalid.', array('{attribute}'=>'hoge')); ?>

Yii クラスをアクセス可能にする

Yiiクラス を globals に登録する

protected/config/main.php

        'viewRenderer'=>array(
            'class'=>'ext.etwigviewrenderer.ETwigViewRenderer',
            'globals' => array(
                'Yii' => 'Yii',
            ),
        ),

ビュー

{{ Yii.t('yii','{attribute} is invalid.', {'{attribute}':'hoge'}) }}

あるいは Yii::t メソッドをアクセス可能にする

Yii::tメソッド を functions に登録する

config/main.php

        'viewRenderer'=>array(
            'class'=>'ext.etwigviewrenderer.ETwigViewRenderer',
            'functions' => array(
                't' => 'Yii::t',
            ),
        ),

ビュー

{{ t('app','{attribute} is invalid.', {'{attribute}':'hoge'}) }}


どちらも Yii::t と短かったのでお得感は少ないかな。

yiiext/twig-rendererでページの一部分をテンプレート化したい

yii は $this->rendarPartial ですが、Twig はincludeタグで書きます。

<?php $this->renderPartial('_view', array('data'=>$model)); ?>
↓↓↓↓↓
{% include '/views/post/_view.twig' with {'data':model} %}

見たまんま書き換えできると思います。そしてrenderPartialでなくていいのかとつぶやくはず。私はつぶやきました。

yiiext/twig-rendererでビューテンプレートを継承したい

追記 2012.03.15

やだなーと思っていた{{void(...)}}ですが、{%do...%}で書くべきと気がつき、修正しました。


yiiのスケルトンappでは ページレイアウトの大枠を layouts/main.php、内部のカラム構造を layouts/column1.php, column2.php に実装しています。Twig ではこれをテンプレート継承で実装します。yiiext/twig-rendererというかTwigを使うならやはりテンプレート継承を使って実装したいところ。
yiiのスケルトンappからの書き換えはおもったより簡単です。こちらが参考になりました。

サンプルコード

親テンプレート

子に委譲するところをblockにする

protected/views/layouts/main.twig のごく一部

    <?php echo $content; ?>
↓↓↓↓↓↓↓
    {% block container %}
    {% endblock container %}
子テンプレート

親の上書きしたいブロックを実装する。{{content|raw}}にはコントローラで$this->render()した結果をはめ込みます。ここはyii風。

protected/views/layouts/column1.twig

{% extends 'views/layouts/main.twig' %}

{% block container %}
<div class="container">
    <div id="content">
    {% block content %}
    {{content|raw}}
    {% endblock content %}
    </div><!-- content -->
</div>
{% endblock container %}

protected/views/layouts/column2.twig

{% extends 'views/layouts/main.twig' %}

{% block container %}
<div class="span-19">
    <div id="content">
        {% block content %}
        {{content|raw}}
        {% endblock content %}
    </div><!-- content -->
</div>
<div class="span-5 last">
    <div id="sidebar">
    {% do this.beginWidget('zii.widgets.CPortlet', {
        'title':'Operations',
    }) %}
    {% do this.widget('zii.widgets.CMenu', {
        'items':this.menu,
        'htmlOptions':{'class':'operations'}
    }) %}
    {% do this.endWidget %}
    </div><!-- sidebar -->
</div>
{% endblock container %}

yiiext/twig-rendererでWidgetを使いたい

追記 2012.03.15

やだなーと思っていた{{void(...)}}ですが、{%do...%}で書くべきと気がつき、修正しました。


ほぼ前のと同じですが、素直に this.beginWidgetでもうまくいきました。
前の{% set form = this.createWidget() %}...{{ form.run }}は何かの役に立つかもしれないので残しておきます。

サンプルコード

ポイント

  • void を利用して {% do %}でコントローラー pageTile, breadcrumbs に代入
  • Controller#setBreadcrumbs を追加する
  • this.beginWidget の 戻り値を form に代入する
  • this.endWidget は{% do %} void中 に書く

protected/views/site/login.twig

{% do this.setPageTitle(App.name~' - Login') %}
{% do this.setBreadcrumbs({0:'Login'}) %}

<h1>Login</h1>

<p>Please fill out the following form with your login credentials:</p>

<div class="form">

{% set form = this.beginWidget('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>

{% do this.endWidget %} 
</div><!-- form -->


void はphpコードが必要なとき多用するかもしれない。contactページではこんな感じで使います。すみません {% do ... %} でお願いします。

<?php $this->widget('CCaptcha'); ?>
↓
{{ void(this.widget('CCaptcha')) |raw }}
↓
{% do this.widget('CCaptcha') %}