Yiiでも中間テーブルにアクセスしたい

またこのネタです。

YiiFrameworkのCManyManyRelationは中間テーブルへのアクセスをサポートしていません。ならばオレオレリレーションによってそれを可能にしてみます。前エントリのまとめと反するのですが、まあ、あそびばってことで。
実現のためにCJoinElementを改造します。まだまだ中途半端な状態ですが載せておきます。気がむいたら更新するかもしれません。

2012.10.25 実験段階 とりあえずレイジーローディング

リレーション

まずオレオレリレーションモデルです。

protected/components/ManyManyPivotRelation.php

<?php
class ManyManyPivotRelation extends CManyManyRelation
{
    public $pivotModel = null;
    public $pivotWith = array();
}

モデル

続いてモデル

<?php
class Viewer extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'viewer';
    }
    public function relations()
    {
        return array(
            'movies_with_pivot' => array("ManyManyPivotRelation", 'Movie', 'viewer_watched_movie(viewer_id, movie_id)',
                'pivotModel' => 'ViewerWatchedMovie', // <- 中間テーブルモデル
                'pivotWith' => array('id','viewer_id', 'movie_id', 'liked',), // <-中間デーブルのカラム
            ),
        );
    }
}

viewer_watched_movie テーブルのPKはIDです。

<?php
class ViewerWatchedMovie extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'viewer_watched_movie';
    }
}
<?php
class Movie extends CActiveRecord
{
    public $pivot;   // <- ここに中間テーブルのモデルが入る
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'movie';
    }
}

CJoinElement

次はCJoinElementに手を入れます。hack begin - hack end を挿入してください。
リレーションがManyManyPivotRelationならば、selectするカラムを追加し、select結果からpivotインスタンスを生成し、子モデルの $record->pivot にセットしています。

YiiFramework/framework/db/ar/CActiveFinder.php

<?php
class CJoinElement
{
	private function applyLazyCondition($query,$record)
	{
		$schema=$this->_builder->getSchema();
		$parent=$this->_parent;
		if($this->relation instanceof CManyManyRelation)
		{
        ....
			if($parentCondition!==array() && $childCondition!==array())
			{
				$join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON ';
				$join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')';
				if(!empty($this->relation->on))
					$join.=' AND ('.$this->relation->on.')';
				$query->joins[]=$join;
				foreach($params as $name=>$value)
					$query->params[$name]=$value;
			}
			else
				throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
					array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name)));

            // hack begin ----------
            if($this->relation instanceof ManyManyPivotRelation) {
                // add pivot columns 
                // TODO table.column -> t0_c0
	            foreach ($this->relation->pivotWith as $col) {
		            $query->selects[]=$joinAlias.'.'.$schema->quoteColumnName($col);
	            }
            }
            // hack end ----------
		}
		else
		{
        ....
		}
	}

	private function populateRecord($query,$row)
	{
        ....
        
		if(isset($this->records[$pk]))
			$record=$this->records[$pk];
		else
		{
			$attributes=array();
			$aliases=array_flip($this->_columnAliases);
			foreach($row as $alias=>$value)
			{
				if(isset($aliases[$alias]))
					$attributes[$aliases[$alias]]=$value;
			}
			$record=$this->model->populateRecord($attributes,false);
			foreach($this->children as $child)
			{
				if(!empty($child->relation->select)) {
					$record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation);
				}
			}
			$this->records[$pk]=$record;
			
            // hack begin ----------
            if($this->relation instanceof ManyManyPivotRelation) {
                // make pivot instance
	            //  TODO t0_c0 -> column_name
	            $p = new $this->relation->pivotModel;
 	            $record->pivot = $p->populateRecord($row,false);
            }
            // hack end ----------
			
		}

        ....
    }
}

ビュー

<?php
$viewers = Viewer::model()->findAll();
foreach($viewers as $viewer) {
    foreach ($viewer->movies_with_pivot as $movie) {
  	    echo $viewer->name . ($movie->pivot->liked ? ' liked ' : ' didn’t like ')
 	        . $movie->title;
    }
}

結果

hoge didn’t like foo
hoge liked bar
piyo liked foo
piyo didn’t like bar

Yiiで中間テーブルに属性を持たせたいときはHasMany, BelongsToを使う

YiiFrameworkのCManyManyRelationは中間テーブルへのアクセス手段を用意してくれません。なので、中間テーブルに属性を持たせたいときはHasManyとBelongsToを使うのが、今のところエレガントな解決方法のようです。

http://www.yiiframework.com/wiki/285/accessing-data-in-a-join-table-with-the-related-models/#c6007
http://www.yiiframework.com/wiki/285/accessing-data-in-a-join-table-with-the-related-models/#c7483

この問題のサンプルコードにはtypoがたくさんあるので、検証コードを載せておきます。

テーブル

CREATE TABLE IF NOT EXISTS `viewer` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `movie` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `viewer_watched_movie` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `viewer_id` int(11) NOT NULL,
  `movie_id` int(11) NOT NULL,
  `liked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_viewer_watched` (`movie_id`),
  KEY `fk_movie_watched_by` (`viewer_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

データ

INSERT INTO `viewer` (`id`, `name`) VALUES
(1, 'v1'),
(2, 'v2');
INSERT INTO `movie` (`id`, `title`) VALUES
(1, 'm1'),
(2, 'm2');
INSERT INTO `viewer_watched_movie` (`id`, `viewer_id`, `movie_id`, `liked`) VALUES
(1, 1, 1, 1),
(2, 1, 2, 0),
(3, 2, 1, 1);

モデル

<?php
class Viewer extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'viewer';
    }
    public function relations()
    {
        return array(
            'watched_movies' => array(self::HAS_MANY, 'ViewerWatchedMovie', 'viewer_id'),
        );
    }	
}
<?php
class Movie extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'movie';
    }
}
<?php
class ViewerWatchedMovie extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function tableName()
    {
        return 'viewer_watched_movie';
    }
    public function relations()
    {
        return array(
            'movie' => array(self::BELONGS_TO, 'Movie', 'movie_id'),
        );
    }
}

レイジーローディング

<?php
$viewer = Viewer::model()->findByPk(1);
foreach ($viewer->watched_movies as $watched) {
    echo $viewer->name . ($watched->liked ? ' liked ' : ' didn’t like ')
    . $watched->movie->title;
}

イーガーローディング

<?php
$viewers = Viewer::model()->with(array(
    'watched_movies'=>array(
        'condition'=>'watched_movies.liked=:liked',
        'params'=>array(':liked'=>true),
    ),
))->findAll();
foreach ($viewers as $viewer) {
    foreach ($viewer->watched_movies as $watched) {
        echo $viewer->name . ($watched->liked ? ' liked ' : ' didn’t like ')
        . $watched->movie->title;
    }
}

まとめのような

CActiveRecordの MANY_MANY, HAS_MANY...は単なるクラス名なので、次のようにオレオレリレーションのクラス名「OreOreRelation」でもいいんです。

<?php
class Viewer extends CActiveRecord {
    ....
    public function relations()	{
        return array(  
            'movies' => array('OreOreRelation', 'Movie', 'viewer_watched_movie(viewer_id, movie_id)', 
                'pivot_model'=>'ViewerWatchedMovie',
            ),
        );
    }	
}

で、オレオレリレーションでSQL書き換えればいいかと思ったらさにあらず、SELECT..JOIN...生成はリレーションクラスでなく、CActiveFinderというかCJoinElementがやっています。
中間テーブルを取得するリレーションを作ってみたのですが、CJoinElementの 「if (relation instanseof CManyManyRelation) 」している辺りを探りながらパッチ当てることで、レイジーローディングだけどうにかできました。書くまでもなく茨の道です。
そんなわけで、いまのところエレガントな解決方法はHasMany, BelongsToだな、というところに落ち着きました。まえに弱点と書きましたが、今回のような設計で使ってくれと言うことなんだと思います。

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>

layouts/main.php, layouts/column1.php はとりあえずそのまんま使う。

結果

f:id:tjtjtjofthedead:20120404212858p:image:w640

得られた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 じゃないのでサポートしなくていいよね

yiiのアクセス制御を学ぶ yii-rightsを導入するの巻

前エントリのロール管理はこれでもロール管理かという代物でした。今回はyii-rightsを使いGUIで管理できるようにします。

f:id:tjtjtjofthedead:20120328071108p:image

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 が追加されている。

参考

感謝。

ようやくyii-rightsに辿りついた。CakePHPaclと同じレベルのことをしようとするとacl - yii extensionsを導入することになるかな。

yiiのアクセス制御を学ぶ アクションで権限チェックしないの巻

前エントリで触れなかったupdateOwnPost関連のアクセス制御について。アクションに書いてしまいがちな権限チェックをフィルタに書いてアクションをシンプルに保っていきたい。

パラメータ付き権限アイテム

ここの図でupdateOwnPostを再確認する。updateOwnPost自身はパラメータ付き権限アイテムで、updatePost がぶら下がっている。

updateOwnPostの権限チェックは次のように実装する。

if(!Yii::app()->user->checkAccess('updateOwnPost', array('post'=>$post)))
  Yii::app()->end();
// または
if(!Yii::app()->user->checkAccess('updatePost', array('post'=>$post)))
  Yii::app()->end();

CAccessControlFilterの権限チェックを調べる

CAccessControlFilterがユーザーと権限をどう扱うか調べた。ユーザーと権限は前エントリ参照。

検証コードの一部

protected/controllers/PostController.php

<?php
public function filters()
{
    return array(
        'accessControl', // perform access control for CRUD operations
    );
}
public function accessRules()
{
    return array(
        array('allow', 
            'actions'=>array('update'),
            'roles'=>array('author'),
            //'roles'=>array('editor'),
            //'roles'=>array('updatePost'),
            //'roles'=>array('updateOwnPost'),

           // ユーザー毎にrolesを切り替えて検証する

        ),
    );
}
検証結果
filter のroles \ user usera userb userc userd
author x o x o
editor x x o o
updatePost x x o o
updateOwnPost x x x x

o : 200 OK, x : 403 forbidden

ロールに updateOwnPost を設定したとき、全ユーザーがアクセス不可。フィルタリング時点では updateOwnPostに必要なパラメータを渡せないから仕方ないか。

問題は userb の updatePost。userbは自身のポストはupdatePostしたいのだが、フィルタにupdatePostを指定すると403になってしまう。これではロールにupdatePost指定できない。
フィルタで権限チェックせず、アクションで権限チェックする手もあるが。。。

<?php
public function accessRules()
{
    return array(
        array('allow',
            'actions'=>array('update'),
            'roles'=>array(), //ロールなし
        ),
        array('deny',  // deny all users
            'users'=>array('*'),
        ),
    );
}
public function update()
{
    // 複雑な権限チェック
    if(!Yii::app()->user->checkAccess('updatePost', array('post'=>$post)))
        Yii::app()->end();

    // updateの仕事
    ...
}

嫌だなー権限チェックはフィルタに任せたい。アクションはアクションの仕事に集中したい。

フィルタでもパラメータ付き権限チェックできる

expressionを使えば!

<?php
public function accessRules()
{
    return array(
        array('allow',
            'actions'=>array('update'),
            'expression'=>array($this, "checkUpdatePost"), 
        ),
        array('deny',  // deny all users
            'users'=>array('*'),
        ),
    );
}
protected function checkUpdatePost($user, $rule)
{
    // 複雑な権限チェック  $user は Yii::app()->user のこと
    return $user->checkAccess('updatePost', array('post'=>$this->loadModel());
}
public function actionUpdate()
{
    // updateの仕事
    ...
}
protected function loadModel() 
{
    ...
}

これでいける。checkUpdatePostでloadしたモデルはアクションで利用できるオマケつきつき。

beforeActionでも

「アクションはアクションの仕事に集中」するならば、beforeActionでチェックしてもいいか。

yiiのアクセス制御を学ぶ CDbAuthManager導入の巻

ロールベースアクセスコントロール(RBAC)を導入するのは面倒で後回しにしてきました。ロールのデザイン、ロール管理の実装、ユーザー管理の実装、権限付与の実装には時間がかかりそうで躊躇してしまうのです。そこでこれらをできる限り省略してRBACを導入することにフォーカスしてみます。

これからやることが想像できない場合はロールベースアクセスコントロールを読む。やりたいことはなんとなくわかった、でもなんだか面倒だなーと思うくらいでOK。とりあえずやってみる。

手順

  1. アプリケーション作成
  2. ユーザー管理
  3. ユーザー認証
  4. 権限付与マネージャの導入
  5. ロール管理
  6. 操作に権限を割り当てる

アプリケーション作成

blogデモを利用します。

  • yii/demo/blogを適当なところにコピー
  • protected/config/main.php にDB設定
  • DBを作成
  • protected/data/schema.mysql.sql とかでスキーマを整える

ユーザー管理

blogデモにはユーザー管理機能がありません。前のポストを参考にしてもいいですが、とりあえずのユーザー生成スクリプトを用意したので使ってください。
4ユーザー usera, userb, userc, userd のパスワードはすべてdemoです。

INSERT INTO tbl_user (id, username, password, salt, email) VALUES (901, 'usera','2e5c7db760a33498023813489cfadc0b','28b206548469ce62182048fd9cf91760','webmaster@example.com');
INSERT INTO tbl_user (id, username, password, salt, email) VALUES (902, 'userb','2e5c7db760a33498023813489cfadc0b','28b206548469ce62182048fd9cf91760','webmaster@example.com');
INSERT INTO tbl_user (id, username, password, salt, email) VALUES (903, 'userc','2e5c7db760a33498023813489cfadc0b','28b206548469ce62182048fd9cf91760','webmaster@example.com');
INSERT INTO tbl_user (id, username, password, salt, email) VALUES (904, 'userd','2e5c7db760a33498023813489cfadc0b','28b206548469ce62182048fd9cf91760','webmaster@example.com');

ユーザー認証

blogデモに実装済みなのでパス。パス。

権限付与マネージャの導入

権限付与マネージャには CDbAuthManager と CPhpAuthManager がありますが今回使うのは前者です。

CDbAuthManager の設定
  • config/main.phpに次を追加する

protected/config/main.php

    'components'=>array(
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'connectionID'=>'db',
        ),
    ),
CDbAuthManager が必要とするテーブルを生成
  • framework/web/auth/schema-*.sql のスクリプトで作成する

ロール管理

yiiガイドにあるロール構造を登録します。権限付与アイテムとは、ロール構造図をあわせて読むと理解しやすい。

権限付与アイテム

権限付与アイテムとは何かをする許可のことです。(例:新しいブログ記事を作る、ユーザを管理する) 粒度と対象者によって、権限付与アイテムはオペレーション、タスク、ロールに分類されます。 ロールは複数のタスクからなり、タスクは複数のオペレーションからなります。 そして、オペレーションが一番小さな許可単位です。

http://www.yiiframework.com/doc/guide/1.1/ja/topics.auth
ロール構造図

ここのロール構造図を参照。

簡易ロール管理

ロール管理はUIで管理したいところですが、今回は SiteController.phpにロールを初期化/登録するアクションを追加します。追加したら http:/path/to/index.php/site/loadroles にアクセスしロール定義をロードします。

protected/controllers/SiteController.php

<?php
    public function actionLoadRoles()
    {
        $auth=Yii::app()->authManager;

        // ロール初期化
        $auth=Yii::app()->clearAll();

        // ロール定義
        $auth->createOperation('createPost','create a post');
        $auth->createOperation('readPost','read a post');
        $auth->createOperation('updatePost','update a post');
        $auth->createOperation('deletePost','delete a post');

        $bizRule='return Yii::app()->user->id==$params["post"]->author_id;';
        $task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule);
        $task->addChild('updatePost');

        $role=$auth->createRole('reader','reader');
        $role->addChild('readPost');

        $role=$auth->createRole('author','author');
        $role->addChild('reader');
        $role->addChild('createPost');
        $role->addChild('updateOwnPost');

        $role=$auth->createRole('editor','editor');
        $role->addChild('reader');
        $role->addChild('updatePost');

        $role=$auth->createRole('admin','admin');
        $role->addChild('editor');
        $role->addChild('author');
        $role->addChild('deletePost');

        // ユーザーに権限付与
        $auth->assign('reader','1');   //demo
        $auth->assign('reader','901'); //usera
        $auth->assign('author','902'); //userb
        $auth->assign('editor','903'); //userc
        $auth->assign('admin', '904'); //userd
    }

ロール作成時、descriptionを設定しました。

各ユーザーの権限

  • demo 記事の参照
  • usera 記事の参照
  • userb 記事の参照と作成 + 自身が作成した記事の編集
  • userc 記事の参照と作成と編集
  • userd 記事の参照と作成と編集と削除

権限チェック

ここまでできたら次はいよいよユーザーに付与した権限を元にBlogデモが動作するよう改修していきます。ログインユーザーが権限を持っている/いないは CWebUser#checkAccess で得られます。

Yii::app()->user->checkAccess('createPost')

view上で判定する

protected/components/views/userMenu.php

<?php
<ul>
    <?php if (Yii::app()->user->checkAccess('createPost')) : ?>
        <li><?php echo CHtml::link('Create New Post',array('post/create')); ?></li>
    <?php endif; ?>
    <?php if (Yii::app()->user->checkAccess('admin')) : ?>
        <li><?php echo CHtml::link('Manage Posts',array('post/admin')); ?></li>
    <?php endif; ?>
    <?php if (Yii::app()->user->checkAccess('editor')) : ?>
        <li><?php echo CHtml::link('Approve Comments',array('comment/index')) . ' (' . Comment::model()->pendingCommentCount . ')'; ?></li>
    <?php endif; ?>
    <li><?php echo CHtml::link('Logout',array('site/logout')); ?></li>
</ul>
Postコントローラーのアクセスコントロールフィルター

protected/controllers/PostController.php

<?php
    public function accessRules()
    {
        return array(
            array('allow',
                'actions'=>array('index','view'),
                'roles'=>array('readPost'),
            ),
            array('allow',
                'actions'=>array('create'),
                'roles'=>array('createPost'),
            ),
            array('allow',
                'actions'=>array('update'),
                'roles'=>array('updatePost'),
            ),
            array('allow',
                'actions'=>array('delete'),
                'roles'=>array('deletePost'),
            ),
            array('deny',  // deny all users
                'users'=>array('*'),
            ),
        );
    }

まとめ

説明しきれていないことが多々あるけれど、CDbAuthManagerは導入できたと思う。

  • 権限付与マネージャは CDbAuthManager と CPhpAuthManager がある
  • 権限アイテムにはロール、タスク、オペレーションがある。オペレーションが最小単位。
  • ロール管理は権限付与マネージャを使う。が、将来的にはyii-Rightsなどのエクステンションを使って管理すると思う
  • 権限チェックは CwebUser#checkAccess からできる
  • アクセスコントロールフィルターから権限チェックを利用できる

blogデモにユーザー管理機能を設ける

blogデモにユーザー管理機能を追加したときのメモ。gii-Crud GeneratorでUserモデルメンテナンス機能を生成し、生成したコードを調整します。

Userモデルメンテナンス機能を生成する

gii を有効にする

ついでにgiiにたどり着けるよう urlManager も調整する。なんか勘違いしていた。

protected/config/main.php

<?php
	'modules'=>array(
		'gii'=>array(
			'class'=>'system.gii.GiiModule',
			'password'=>'gii',
		 	// If removed, Gii defaults to localhost only. Edit carefully to taste.
		),
	),
user の crud を作成

http://path/to/index.php/gii へアクセスしuser の crud を作成する。

giiで生成したコードを調整する

password, saltは表示したくない。saltは入力したくないので次を修正する。

protected/views/user/_form.php

saltは入力不要。salt をカット

protected/views/user/_search.php

saltは表示不要。salt をカット

protected/views/user/_view.php

password, saltは表示不要。password, salt をカット

protected/views/user/admin.php

password, saltは表示不要。password, salt をカット

protected/views/user/view.php

password, saltは表示不要。password, salt をカット

protected/models/user.php

saltは入力しないので rules から salt をカット。beforeSave を追加する。

<?php
    public function rules()
    {
        return array(
            array('username, password, email', 'required'),
            array('username, password, email', 'length', 'max'=>128),
            array('profile', 'safe'),
        );
    }

    protected function beforeSave()
    {
        $this->salt = $this->generateSalt();
        $this->password = $this->hashPassword($this->password, $this->salt);
        return true;
    }